Skip to content

Commit

Permalink
Backport ViewScope implementation (#489)
Browse files Browse the repository at this point in the history
* Backport ViewScope implementation

* Backport ViewScope tests

* fix pmd

* backport coverage limits
  • Loading branch information
larsgrefer committed Jun 13, 2018
1 parent e9b2a84 commit 3b3fd23
Show file tree
Hide file tree
Showing 7 changed files with 497 additions and 87 deletions.
12 changes: 6 additions & 6 deletions jsf-spring-boot-autoconfigure/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@
<duplicate.ignoredClassPattern>(javax.faces.*$|javax.persistence.(PersistenceContext|PersistenceContextType|PersistenceContexts|PersistenceProperty|PersistenceUnit|PersistenceUnits|SynchronizationType))</duplicate.ignoredClassPattern>
<duplicate.ignoredResourcePattern>(javax/faces/Messages.*\.properties)|(about.html)</duplicate.ignoredResourcePattern>

<jacoco.totalLineRate>0.98</jacoco.totalLineRate>
<jacoco.totalBranchRate>0.93</jacoco.totalBranchRate>
<jacoco.packageLineRate>0.90</jacoco.packageLineRate>
<jacoco.packageBranchRate>0.83</jacoco.packageBranchRate>
<jacoco.lineRate>0.75</jacoco.lineRate>
<jacoco.branchRate>0.83</jacoco.branchRate>
<jacoco.totalLineRate>0.95</jacoco.totalLineRate>
<jacoco.totalBranchRate>0.88</jacoco.totalBranchRate>
<jacoco.packageLineRate>0.50</jacoco.packageLineRate>
<jacoco.packageBranchRate>0.33</jacoco.packageBranchRate>
<jacoco.lineRate>0.33</jacoco.lineRate>
<jacoco.branchRate>0.33</jacoco.branchRate>
</properties>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,59 +16,248 @@

package org.joinfaces.integration;

import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import javax.faces.component.UIViewRoot;
import javax.faces.context.FacesContext;
import javax.faces.event.PreDestroyViewMapEvent;
import javax.faces.event.SystemEvent;
import javax.faces.event.ViewMapListener;
import javax.servlet.http.HttpSessionBindingEvent;
import javax.servlet.http.HttpSessionBindingListener;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;

import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.config.Scope;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.util.Assert;
import org.springframework.web.context.request.FacesRequestAttributes;

/**
* Implementation of view scope.
*
* This class exposes the JSF {@link UIViewRoot#getViewMap() view map} as spring {@link Scope}.
*
* @author Marcelo Fernandes
* @author Lars Grefer
*/
@Slf4j
public class ViewScope implements Scope {

/**
* Scope identifier for view scope: "view".
*
* @see org.springframework.web.context.WebApplicationContext#SCOPE_SESSION
* @see org.springframework.web.context.WebApplicationContext#SCOPE_REQUEST
*/
public static final String SCOPE_VIEW = "view";

/**
* Constant identifying the {@link String} prefixed to the name of a
* destruction callback when it is stored in a {@link UIViewRoot#getViewMap() view map}.
*
* @see org.springframework.web.context.request.ServletRequestAttributes#DESTRUCTION_CALLBACK_NAME_PREFIX
*/
public static final String DESTRUCTION_CALLBACK_NAME_PREFIX = ViewScope.class.getName() + ".DESTRUCTION_CALLBACK.";

@Getter(AccessLevel.PACKAGE)
private PreDestroyViewMapListener preDestroyViewMapListener = new PreDestroyViewMapListener();

@Override
public Object get(String name, ObjectFactory objectFactory) {
Map<String, Object> viewMap = FacesContext.getCurrentInstance().getViewRoot().getViewMap();
Map<String, Object> viewMap = getViewRoot().getViewMap();

if (viewMap.containsKey(name)) {
return viewMap.get(name);
}
else {
Object object = objectFactory.getObject();
viewMap.put(name, object);
Object bean = viewMap.get(name);

return object;
if (bean == null) {
bean = objectFactory.getObject();
viewMap.put(name, bean);
}

return bean;
}

@Override
public Object remove(String name) {
return FacesContext.getCurrentInstance().getViewRoot().getViewMap().remove(name);
UIViewRoot viewRoot = getViewRoot();
Object bean = viewRoot.getViewMap().remove(name);
DestructionCallbackWrapper destructionCallbackWrapper = (DestructionCallbackWrapper) viewRoot.getViewMap().remove(DESTRUCTION_CALLBACK_NAME_PREFIX + name);

if (destructionCallbackWrapper != null) {
getSessionListener().unregister(destructionCallbackWrapper);
}

return bean;
}

@Override
public String getConversationId() {
getFacesContext(); //maybe throws an Exception
return null;
}

@Override
public void registerDestructionCallback(String name, Runnable callback) {
//Not supported
DestructionCallbackWrapper wrapper = new DestructionCallbackWrapper(name, callback);

getFacesContext().getApplication().subscribeToEvent(PreDestroyViewMapEvent.class, this.preDestroyViewMapListener);
getViewRoot().getViewMap().put(DESTRUCTION_CALLBACK_NAME_PREFIX + name, wrapper);
getSessionListener().register(wrapper);
}

@Override
public Object resolveContextualObject(String key) {
RequestAttributes attributes = RequestContextHolder.currentRequestAttributes();
return attributes.resolveReference(key);
return new FacesRequestAttributes(getFacesContext()).resolveReference(key);
}

private UIViewRoot getViewRoot() {
UIViewRoot viewRoot = getFacesContext().getViewRoot();
if (viewRoot == null) {
throw new IllegalStateException("No ViewRoot found");
}

return viewRoot;
}

private FacesContext getFacesContext() {
FacesContext facesContext = FacesContext.getCurrentInstance();
if (facesContext == null) {
throw new IllegalStateException("No FacesContext found.");
}
return facesContext;
}

private SessionListener getSessionListener() {
Map<String, Object> sessionMap = getFacesContext()
.getExternalContext()
.getSessionMap();

SessionListener sessionListener = (SessionListener) sessionMap.get(SessionListener.class.getName());

if (sessionListener == null) {
sessionListener = new SessionListener();
sessionMap.put(SessionListener.class.getName(), sessionListener);
}

return sessionListener;
}

/**
* This class acts as "session-destroyed" listener, which call the destruction callbacks
* of all view scoped beans that have not been destructed yet.
*
* @author Lars Grefer
* @see org.springframework.web.context.request.DestructionCallbackBindingListener
*/
@Getter
static class SessionListener implements HttpSessionBindingListener {

private List<DestructionCallbackWrapper> destructionCallbackWrappers = new LinkedList<DestructionCallbackWrapper>();

void register(DestructionCallbackWrapper destructionCallbackWrapper) {
cleanup();
this.destructionCallbackWrappers.add(destructionCallbackWrapper);
}

void unregister(DestructionCallbackWrapper destructionCallbackWrapper) {
cleanup();
this.destructionCallbackWrappers.remove(destructionCallbackWrapper);
}

synchronized void cleanup() {
for (Iterator<DestructionCallbackWrapper> iterator = this.destructionCallbackWrappers.iterator(); iterator.hasNext(); ) {
DestructionCallbackWrapper destructionCallbackWrapper = iterator.next();
if (destructionCallbackWrapper.isCallbackCalled()) {
iterator.remove();
}
}
}

@Override
public void valueBound(HttpSessionBindingEvent httpSessionBindingEvent) {
}

@Override
public void valueUnbound(HttpSessionBindingEvent httpSessionBindingEvent) {
for (DestructionCallbackWrapper destructionCallbackWrapper : this.destructionCallbackWrappers) {
destructionCallbackWrapper.onSessionDestroy();
}
cleanup();
}
}

/**
* This class acts as {@link PreDestroyViewMapEvent}-listener, which calls all destruction callbacks
* which are stored in the view map to be destroyed.
*
* @author Lars Grefer
* @see #registerDestructionCallback(String, Runnable)
*/
class PreDestroyViewMapListener implements ViewMapListener {

@Override
public void processEvent(SystemEvent event) {
UIViewRoot root = (UIViewRoot) event.getSource();

for (Object object : root.getViewMap(false).values()) {
if (object instanceof DestructionCallbackWrapper) {
((DestructionCallbackWrapper) object).onViewDestroy();
}
}

getSessionListener().cleanup();
}

@Override
public boolean isListenerForSource(Object source) {
return source instanceof UIViewRoot;
}
}

/**
* Wrapper around the {@link ViewScope#registerDestructionCallback(String, Runnable) destruction callback} of
* view scoped beans.
*
* @author Lars Grefer
* @see #registerDestructionCallback(String, Runnable)
*/
static class DestructionCallbackWrapper {

@Getter
private final String beanName;

private Runnable callback;

DestructionCallbackWrapper(String beanName, Runnable callback) {
Assert.hasText(beanName, "beanName must not be null or empty");
Assert.notNull(callback, "callback must not be null");
this.beanName = beanName;
this.callback = callback;
}

void onViewDestroy() {
doRunCallback(false);
}

void onSessionDestroy() {
doRunCallback(true);
}

private synchronized void doRunCallback(boolean session) {
if (this.callback != null) {
log.debug("Calling destruction callbacks for bean {} because the {} is destroyed", getBeanName(), session ? "session" : "view map");
this.callback.run();
this.callback = null;
}
}

boolean isCallbackCalled() {
return this.callback == null;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright 2016-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.joinfaces.integration;

import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;

import static org.assertj.core.api.Assertions.assertThat;

public class DestructionCallbackWrapperTest {

private ViewScope.DestructionCallbackWrapper destructionCallbackWrapper;
private Runnable callback;

@Before
public void setUp() {
this.callback = Mockito.mock(Runnable.class);
this.destructionCallbackWrapper = new ViewScope.DestructionCallbackWrapper("bean", this.callback);
}

@Test
public void onSessionDestroy() {
assertThat(this.destructionCallbackWrapper.isCallbackCalled()).isFalse();
this.destructionCallbackWrapper.onSessionDestroy();
Mockito.verify(this.callback).run();
assertThat(this.destructionCallbackWrapper.isCallbackCalled()).isTrue();
}

@Test
public void testRunCalledOnlyOnce() {
this.destructionCallbackWrapper.onViewDestroy();
this.destructionCallbackWrapper.onSessionDestroy();
Mockito.verify(this.callback).run();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright 2016-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.joinfaces.integration;

import javax.faces.event.PreDestroyViewMapEvent;

import org.joinfaces.test.mock.JsfIT;
import org.junit.Before;
import org.junit.Test;

import static org.mockito.BDDMockito.mock;
import static org.mockito.BDDMockito.verify;
import static org.mockito.BDDMockito.when;

public class PreDestroyViewMapListenerTest extends JsfIT {

private ViewScope.PreDestroyViewMapListener preDestroyViewMapListener;

@Before
public void setUp() {
ViewScope viewScope = new ViewScope();

this.preDestroyViewMapListener = viewScope.getPreDestroyViewMapListener();
}

@Test
public void testProcessEvent() {
PreDestroyViewMapEvent event = mock(PreDestroyViewMapEvent.class);

when(event.getSource()).thenReturn(getJsfMock().getMockViewRoot());
when(getJsfMock().getMockViewRoot().getViewMap(false))
.thenReturn(getJsfMock().getMockViewMap());

Runnable runnable = mock(Runnable.class);
ViewScope.DestructionCallbackWrapper destructionCallbackWrapper = new ViewScope.DestructionCallbackWrapper(
"foo", runnable
);

getJsfMock().getMockViewMap()
.put(ViewScope.DESTRUCTION_CALLBACK_NAME_PREFIX + "foo", destructionCallbackWrapper);

this.preDestroyViewMapListener.processEvent(event);

verify(runnable).run();
}
}

0 comments on commit 3b3fd23

Please sign in to comment.