Permalink
Browse files

New: immediately destroy @ViewScoped on unload (link, close, etc)

  • Loading branch information...
BalusC committed Aug 29, 2015
1 parent b0ffd73 commit 1c912f71a1d7b691dac6e0dde5f9fd9f48d3388f
@@ -15,8 +15,12 @@
*/
package org.omnifaces.application;
import static org.omnifaces.util.Faces.getRequestParameter;
import static org.omnifaces.util.Faces.responseComplete;
import javax.faces.component.UIViewRoot;
import javax.faces.event.AbortProcessingException;
import javax.faces.event.PostRestoreStateEvent;
import javax.faces.event.PreDestroyViewMapEvent;
import javax.faces.event.SystemEvent;
import javax.faces.event.ViewMapListener;
@@ -25,7 +29,7 @@
import org.omnifaces.config.BeanManager;
/**
* Listener for JSF view scope destroy events so that view scope provider implementation can be notified.
* Listener for JSF view scope destroy events so that view scope manager can be notified.
*
* @author Bauke Scholtz
* @see ViewScopeManager
@@ -51,8 +55,16 @@ public boolean isListenerForSource(Object source) {
@Override
public void processEvent(SystemEvent event) throws AbortProcessingException {
if (event instanceof PreDestroyViewMapEvent) {
BeanManager.INSTANCE.getReference(ViewScopeManager.class).preDestroyView();
processPreDestroyView();
}
else if (event instanceof PostRestoreStateEvent && "unload".equals(getRequestParameter("omnifaces.event"))) {
processPreDestroyView();
responseComplete();
}
}
private void processPreDestroyView() {
BeanManager.INSTANCE.getReference(ViewScopeManager.class).preDestroyView();
}
}
@@ -36,8 +36,9 @@
/**
* <p>
* The CDI view scope annotation, intented for use in JSF 2.0/2.1. Just use it the usual way as all other CDI scopes.
* Watch out with IDE autocomplete on import that you don't accidentally import JSF's own one.
* The CDI view scope annotation, with more optimal handling of bean destroy as compared to standard JSF one. Just use
* it the usual way as all other CDI scopes. Watch out with IDE autocomplete on import that you don't accidentally
* import standard JSF's own one.
* <pre>
* import javax.inject.Named;
* import org.omnifaces.cdi.ViewScoped;
@@ -50,18 +51,14 @@
* Please note that the bean <strong>must</strong> implement {@link Serializable}, otherwise the CDI implementation
* will throw an exception about the bean not being passivation capable.
* <p>
* In JSF 2.2, you're supposed to use JSF's own new CDI compatible <code>javax.faces.view.ViewScoped</code> instead;
* not because this CDI view scope annotation is so bad, in contrary, but just because using the standard solutions
* should be preferred over alternative solutions if they solve the same problem.
* <p>
* Under the covers, CDI managed beans with this scope are via {@link ViewScopeManager} stored in the session scope by
* an {@link UUID} based key which is referenced in JSF's own view map as available by {@link UIViewRoot#getViewMap()}.
* They are not stored in the JSF view map itself as that would be rather expensive in case of client side state saving.
* <p>
* In effects, this CDI view scope annotation has exactly the same lifecycle as JSF's own view scope. One important
* thing to know and understand is that any {@link PreDestroy} annotated method on a CDI view scoped bean isn't
* <em>immediately</em> invoked in all cases when the view scope ends. This is not specific to CDI, but to JSF itself.
* For detail, see the following JSF issues related to the matter:
* In effects, this CDI view scope annotation has exactly the same lifecycle as JSF's own view scope. Only the bean
* destroy is more optimal handled. In standard JSF, the {@link PreDestroy} annotated method on a CDI view scoped bean
* isn't <em>immediately</em> invoked in all cases when the view scope ends. For detail, see the following JSF issues
* related to the matter:
* <ul>
* <li><a href="https://java.net/jira/browse/JAVASERVERFACES-1351">Mojarra issue 1351</a>
* <li><a href="https://java.net/jira/browse/JAVASERVERFACES-1839">Mojarra issue 1839</a>
@@ -70,9 +67,15 @@
* <p>
* Summarized, it's only <em>immediately</em> invoked when the view is either explicitly changed by a non-null/void
* navigation on a postback, or when the view is explicitly rebuilt by {@link FacesContext#setViewRoot(UIViewRoot)}.
* It's not <em>immediately</em> invoked on a GET navigation, nor a close of browser tab/window. This CDI view scope
* annotation however guarantees that the {@link PreDestroy} annotated method is also invoked on session expire, while
* JSF 2.0/2.1 doesn't do that (JSF 2.2 does).
* It's not <em>immediately</em> invoked on a GET navigation, nor a close of browser tab/window. In JSF 2.0/2.1, it's
* even not afterwards invoked on session expire (JSF 2.2 does).
* <p>
* This CDI view scope annotation not only guarantees that the {@link PreDestroy} annotated method is also invoked on
* session expire, but it also hooks on the browser <code>beforeunload</code> event so that the bean destroy is yet more
* optimally handled. I.e. when the user navigates away by GET, or closes the browser tab/window, then the
* {@link PreDestroy} annotated method will instantly be invoked. This trick is done by a synchronous XHR request via
* an automatically included helper script <code>omnifaces:unload.js</code>.
*
* <h3>Configuration</h3>
* <p>
* By default, the maximum number of active view scopes is hold in a LRU map with a default size equal to the first
@@ -15,6 +15,8 @@
*/
package org.omnifaces.cdi.viewscope;
import static org.omnifaces.util.Components.addScriptResourceToBody;
import static org.omnifaces.util.Components.addScriptToBody;
import static org.omnifaces.util.Faces.getInitParameter;
import static org.omnifaces.util.Faces.getViewAttribute;
import static org.omnifaces.util.Faces.setViewAttribute;
@@ -182,13 +184,13 @@ private int getMaxActiveViewScopes() {
* CDI bean storage will also be auto-created.
*/
private UUID getBeanStorageId(boolean create) {
UUID id = (UUID) getViewAttribute(ViewScopeManager.class.getName());
UUID id = getViewAttribute(ViewScopeManager.class.getName());
if (id == null || activeViewScopes.get(id) == null) {
id = UUID.randomUUID();
if (create) {
activeViewScopes.put(id, new BeanStorage(DEFAULT_BEANS_PER_VIEW_SCOPE));
createViewScope(id);
}
setViewAttribute(ViewScopeManager.class.getName(), id);
@@ -197,6 +199,12 @@ private UUID getBeanStorageId(boolean create) {
return id;
}
private void createViewScope(UUID id) {
activeViewScopes.put(id, new BeanStorage(DEFAULT_BEANS_PER_VIEW_SCOPE));
addScriptResourceToBody("omnifaces", "unload.js");
addScriptToBody("OmniFaces.Unload.init()");
}
// Nested classes -------------------------------------------------------------------------------------------------
/**
@@ -45,6 +45,7 @@
import javax.faces.component.UIForm;
import javax.faces.component.UIInput;
import javax.faces.component.UINamingContainer;
import javax.faces.component.UIOutput;
import javax.faces.component.UIPanel;
import javax.faces.component.UIParameter;
import javax.faces.component.UIViewRoot;
@@ -623,6 +624,40 @@ public static UIComponent includeCompositeComponent(UIComponent parent, String l
return composite;
}
/**
* Add given JavaScript code as inline script to end of body of the current view.
* Note: this doesn't have any effect during ajax postbacks. Rather use {@link Ajax#oncomplete(String...)}.
* @param script JavaScript code to be added as inline script to end of body of the current view.
* @since 2.2
*/
public static void addScriptToBody(String script) {
UIOutput outputScript = new UIOutput();
outputScript.setRendererType("javax.faces.resource.Script");
UIOutput content = new UIOutput();
content.setValue(script);
outputScript.getChildren().add(content);
addComponentResourceToBody(outputScript);
}
/**
* Add given JavaScript resource to end of body of the current view.
* Note: this doesn't have any effect during non-@all ajax postbacks.
* @param script JavaScript resource to be added to end of body of the current view.
* @since 2.2
*/
public static void addScriptResourceToBody(String libraryName, String resourceName) {
UIOutput outputScript = new UIOutput();
outputScript.setRendererType("javax.faces.resource.Script");
outputScript.getAttributes().put("library", libraryName);
outputScript.getAttributes().put("name", resourceName);
addComponentResourceToBody(outputScript);
}
private static void addComponentResourceToBody(UIComponent resource) {
FacesContext context = FacesContext.getCurrentInstance();
context.getViewRoot().addComponentResource(context, resource, "body");
}
// Forms ----------------------------------------------------------------------------------------------------------
/**
@@ -37,6 +37,11 @@
<system-event-class>javax.faces.event.PreDestroyViewMapEvent</system-event-class>
<source-class>javax.faces.component.UIViewRoot</source-class>
</system-event-listener>
<system-event-listener>
<system-event-listener-class>org.omnifaces.application.ViewScopeEventListener</system-event-listener-class>
<system-event-class>javax.faces.event.PostRestoreStateEvent</system-event-class>
<source-class>javax.faces.component.UIViewRoot</source-class>
</system-event-listener>
</application>
<lifecycle>

Some generated files are not rendered by default. Learn more.

Oops, something went wrong.
@@ -0,0 +1,103 @@
/*
* Copyright 2015 OmniFaces.
*
* 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.
*/
var OmniFaces = OmniFaces || {};
/**
* <p>Fire "unload" event to server side via synchronous XHR when the window is about to be unloaded as result of a
* non-submit event, so that e.g. any view scoped beans will immediately be destroyed when enduser refreshes page,
* or navigates away, or closes browser.
* <p>This script is automatically included when an org.omnifaces.cdi.ViewScoped managed bean is created.
*
* @author Bauke Scholtz
* @since 2.2
*/
OmniFaces.Unload = (function() {
var unload = {};
var disabled = false;
var VIEW_STATE_PARAM = "javax.faces.ViewState";
/**
* Initialize the "unload" event listener on the current document.
*/
unload.init = function() {
if (!window.XMLHttpRequest) {
return; // Native XHR not supported (IE6/7 not supported). End of story. Let session expiration do its job.
}
var viewState = getViewState();
if (!viewState) {
return; // No JSF form in the document? Why is it referencing a view scoped bean then? ;)
}
addEventListener(window, "beforeunload", function() {
if (disabled) {
disabled = false; // Just in case some custom JS explicitly triggered submit event while staying in same DOM.
return;
}
try {
var xhr = new XMLHttpRequest();
xhr.open("POST", document.location, false);
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xhr.send("omnifaces.event=unload&" + VIEW_STATE_PARAM + "=" + viewState);
}
catch (e) {
// Fail silently. You never know.
}
});
addEventListener(document, "submit", function() {
OmniFaces.Unload.disable(); // Disable unload event on any (propagated!) submit event.
});
};
/**
* Disable the "unload" event listener on the current document.
* It will be re-enabled when the DOM has not changed during the "unload" event.
*/
unload.disable = function() {
disabled = true;
}
/**
* Get the view state value from the current document.
*/
function getViewState() {
for (var i = 0; i < document.forms.length; i++) {
var viewStateElement = document.forms[i][VIEW_STATE_PARAM];
if (viewStateElement) {
return viewStateElement.value;
}
}
return null;
}
/**
* Add an event listener on the given event to the given element.
*/
function addEventListener(element, event, listener) {
if (element.addEventListener) {
element.addEventListener(event, listener, false);
}
else if (element.attachEvent) {
element.attachEvent("on" + event, listener);
}
}
return unload;
})();

0 comments on commit 1c912f7

Please sign in to comment.