Permalink
Browse files

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

  • Loading branch information...
1 parent b0ffd73 commit 1c912f71a1d7b691dac6e0dde5f9fd9f48d3388f @BalusC BalusC committed Aug 29, 2015
@@ -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>
@@ -0,0 +1,2 @@
+var OmniFaces=OmniFaces||{};
+OmniFaces.Unload=function(){function n(){for(var n=0;n<document.forms.length;n++){var e=document.forms[n][o];if(e)return e.value}return null}function e(n,e,t){n.addEventListener?n.addEventListener(e,t,!1):n.attachEvent&&n.attachEvent("on"+e,t)}var t={},a=!1,o="javax.faces.ViewState";return t.init=function(){if(window.XMLHttpRequest){var t=n();t&&(e(window,"beforeunload",function(){if(a)return void(a=!1);try{var n=new XMLHttpRequest;n.open("POST",document.location,!1),n.setRequestHeader("Content-type","application/x-www-form-urlencoded"),n.send("omnifaces.event=unload&"+o+"="+t)}catch(e){}}),e(document,"submit",function(){OmniFaces.Unload.disable()}))}},t.disable=function(){a=!0},t}();
@@ -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.