/* * Copyright 2017 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. */ package org.omnifaces.viewhandler; import static java.lang.String.format; import static javax.faces.component.visit.VisitHint.SKIP_ITERATION; import static org.omnifaces.cdi.viewscope.ViewScopeManager.isUnloadRequest; import static org.omnifaces.taghandler.EnableRestorableView.isRestorableView; import static org.omnifaces.taghandler.EnableRestorableView.isRestorableViewRequest; import static org.omnifaces.util.Components.buildView; import static org.omnifaces.util.Components.forEachComponent; import static org.omnifaces.util.Components.getClosestParent; import static org.omnifaces.util.Faces.responseComplete; import static org.omnifaces.util.FacesLocal.getRenderKit; import static org.omnifaces.util.FacesLocal.getRequestQueryString; import static org.omnifaces.util.FacesLocal.getRequestURI; import static org.omnifaces.util.FacesLocal.isDevelopment; import static org.omnifaces.util.FacesLocal.redirectPermanent; import static org.omnifaces.util.Utils.isEmpty; import java.io.IOException; import java.util.Map; import javax.faces.FacesException; import javax.faces.application.ViewExpiredException; import javax.faces.application.ViewHandler; import javax.faces.application.ViewHandlerWrapper; import javax.faces.component.UIForm; import javax.faces.component.UIViewRoot; import javax.faces.context.FacesContext; import javax.faces.event.PreDestroyViewMapEvent; import javax.faces.render.ResponseStateManager; import org.omnifaces.cdi.ViewScoped; import org.omnifaces.cdi.viewscope.ViewScopeManager; import org.omnifaces.taghandler.EnableRestorableView; import org.omnifaces.util.Callback; import org.omnifaces.util.Hacks; /** * OmniFaces view handler. * This class was before version 2.5 known as <code>RestorableViewHandler</code>. * This view handler performs the following tasks: * <ol> * <li>Since 1.3: Recreate entire view when {@link EnableRestorableView} tag is in the metadata. This effectively * prevents the {@link ViewExpiredException} on the view. * <li>Since 2.2: Detect unload requests coming from {@link ViewScoped} beans. This will create a dummy view and only * restore the view scoped state instead of building and restoring the entire view. * <li>Since 2.5: If project stage is development, then throw an {@link IllegalStateException} when there's a nested * {@link UIForm} component. * </ol> * * @author Bauke Scholtz * @since 1.3 * @see EnableRestorableView * @see ViewScopeManager */ public class OmniViewHandler extends ViewHandlerWrapper { // Constants ------------------------------------------------------------------------------------------------------ private static final NestedFormsChecker NESTED_FORMS_CHECKER = new NestedFormsChecker(); private static final String ERROR_NESTED_FORM_ENCOUNTERED = "Nested form with ID '%s' encountered inside parent form with ID '%s'. This is illegal in HTML."; // Properties ----------------------------------------------------------------------------------------------------- private ViewHandler wrapped; // Constructors --------------------------------------------------------------------------------------------------- /** * Construct a new OmniFaces view handler around the given wrapped view handler. * @param wrapped The wrapped view handler. */ public OmniViewHandler(ViewHandler wrapped) { this.wrapped = wrapped; } // Actions -------------------------------------------------------------------------------------------------------- /** * If the current request is an unload request from {@link ViewScoped}, then create a dummy view, restore only the * view root state and then immediately explicitly destroy the view, else restore the view as usual. If the * <code><o:enableRestoreView></code> is used once in the application, and the restored view is null and the * current request is a postback, then recreate and rebuild the view from scratch. If it indeed contains the * <code><o:enableRestoreView></code>, then return the newly created view, else return <code>null</code>. */ @Override public UIViewRoot restoreView(FacesContext context, String viewId) { if (isUnloadRequest(context)) { return unloadView(context, viewId); } UIViewRoot restoredView = super.restoreView(context, viewId); if (isRestorableViewRequest(context, restoredView)) { return createRestorableViewIfNecessary(viewId); } return restoredView; } @Override public void renderView(FacesContext context, UIViewRoot viewToRender) throws IOException { if (isDevelopment(context)) { validateComponentTreeStructure(context, viewToRender); } super.renderView(context, viewToRender); } /** * Create a dummy view, restore only the view root state and then immediately explicitly destroy the view. Or, if * there is no view root state and the current request is triggered by a beacon, then explicitly send a permanent * redirect to base URI in order to strip off all unload related query parameters. */ private UIViewRoot unloadView(FacesContext context, String viewId) { UIViewRoot createdView = createView(context, viewId); ResponseStateManager manager = getRenderKit(context).getResponseStateManager(); if (restoreViewRootState(context, manager, createdView)) { context.setProcessingEvents(true); context.getApplication().publishEvent(context, PreDestroyViewMapEvent.class, UIViewRoot.class, createdView); Hacks.removeViewState(context, manager, viewId); } else if (!isEmpty(getRequestQueryString(context))) { redirectPermanent(context, getRequestURI(context)); } responseComplete(); return createdView; } /** * Restore only the view root state. This ensures that the view scope map and all view root component system event * listeners are also restored (including those for {@link PreDestroyViewMapEvent}). This is done so because calling * <code>super.restoreView()</code> would implicitly also build the entire view and restore state of all other * components in the tree. This is unnecessary during an unload request. */ @SuppressWarnings("rawtypes") private boolean restoreViewRootState(FacesContext context, ResponseStateManager manager, UIViewRoot view) { Object state = manager.getState(context, view.getViewId()); if (state == null || !(state instanceof Object[]) || ((Object[]) state).length < 2) { return false; } Object componentState = ((Object[]) state)[1]; Object viewRootState = null; if (componentState instanceof Map) { // Partial state saving. if (view.getId() == null) { // MyFaces. view.setId(view.createUniqueId(context, null)); view.markInitialState(); } viewRootState = ((Map) componentState).get(view.getClientId(context)); } else if (componentState instanceof Object[]) { // Full state saving. viewRootState = ((Object[]) componentState)[0]; } if (viewRootState != null) { view.restoreState(context, viewRootState); context.setViewRoot(view); return true; } return false; } /** * Create and build the view and return it if it indeed contains {@link EnableRestorableView}, else return null. */ private UIViewRoot createRestorableViewIfNecessary(String viewId) { try { UIViewRoot createdView = buildView(viewId); return isRestorableView(createdView) ? createdView : null; } catch (IOException e) { throw new FacesException(e); } } private void validateComponentTreeStructure(FacesContext context, UIViewRoot view) { checkNestedForms(context, view); } private void checkNestedForms(FacesContext context, UIViewRoot view) { forEachComponent(context).fromRoot(view).ofTypes(UIForm.class).withHints(SKIP_ITERATION).invoke(NESTED_FORMS_CHECKER); } @Override public ViewHandler getWrapped() { return wrapped; } // Inner classes ------------------------------------------------------------------------------------------------- private static class NestedFormsChecker implements Callback.WithArgument<UIForm> { @Override public void invoke(UIForm form) { UIForm nestedParent = getClosestParent(form, UIForm.class); if (nestedParent != null && (!Hacks.isNestedInPrimeFacesDialog(form) || Hacks.isNestedInPrimeFacesDialog(form, nestedParent))) { throw new IllegalStateException( format(ERROR_NESTED_FORM_ENCOUNTERED, form.getClientId(), nestedParent.getClientId())); } } } }