/* * 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.cdi.viewscope; import static java.lang.Boolean.TRUE; import static java.lang.String.format; import static java.util.logging.Level.FINE; import static org.omnifaces.config.OmniFaces.LIBRARY_NAME; import static org.omnifaces.config.OmniFaces.SCRIPT_NAME; import static org.omnifaces.config.OmniFaces.UNLOAD_SCRIPT_NAME; import static org.omnifaces.util.Ajax.load; import static org.omnifaces.util.Ajax.oncomplete; import static org.omnifaces.util.BeansLocal.getInstance; import static org.omnifaces.util.Components.addScriptResourceToBody; import static org.omnifaces.util.Components.addScriptResourceToHead; import static org.omnifaces.util.Components.addScriptToBody; import static org.omnifaces.util.FacesLocal.getRequestParameter; import static org.omnifaces.util.FacesLocal.isAjaxRequestWithPartialRendering; import java.util.UUID; import java.util.logging.Logger; import javax.enterprise.context.ApplicationScoped; import javax.enterprise.context.spi.Contextual; import javax.enterprise.context.spi.CreationalContext; import javax.enterprise.inject.spi.Bean; import javax.enterprise.inject.spi.BeanManager; import javax.faces.application.StateManager; import javax.faces.context.FacesContext; import javax.faces.event.PhaseId; import javax.inject.Inject; import org.omnifaces.cdi.BeanStorage; import org.omnifaces.cdi.ViewScoped; import org.omnifaces.resourcehandler.ResourceIdentifier; import org.omnifaces.util.Hacks; /** * Manages view scoped bean creation and destroy. The creation is initiated by {@link ViewScopeContext} which is * registered by {@link ViewScopeExtension} and the destroy is initiated by {@link ViewScopeEventListener} which is * registered in <code>faces-config.xml</code>. * <p> * Depending on {@link ViewScoped#saveInViewState()}, this view scope manager will delegate the creation and destroy * further to either {@link ViewScopeStorageInSession} or {@link ViewScopeStorageInViewState} which saves the concrete * bean instances in respectively HTTP session or JSF view state. * * @author Radu Creanga {@literal <rdcrng@gmail.com>} * @author Bauke Scholtz * @see ViewScoped * @see ViewScopeContext * @since 1.6 */ @ApplicationScoped public class ViewScopeManager { // Public constants ----------------------------------------------------------------------------------------------- /** OmniFaces specific context parameter name of maximum active view scopes in session. */ public static final String PARAM_NAME_MAX_ACTIVE_VIEW_SCOPES = "org.omnifaces.VIEW_SCOPE_MANAGER_MAX_ACTIVE_VIEW_SCOPES"; /** Mojarra specific context parameter name of maximum number of logical views in session. */ public static final String PARAM_NAME_MOJARRA_NUMBER_OF_VIEWS = "com.sun.faces.numberOfLogicalViews"; /** MyFaces specific context parameter name of maximum number of views in session. */ public static final String PARAM_NAME_MYFACES_NUMBER_OF_VIEWS = "org.apache.myfaces.NUMBER_OF_VIEWS_IN_SESSION"; /** Default value of maximum active view scopes in session. */ public static final int DEFAULT_MAX_ACTIVE_VIEW_SCOPES = 20; // Mojarra's default is 15 and MyFaces' default is 20. // Private constants ---------------------------------------------------------------------------------------------- private static final Logger logger = Logger.getLogger(ViewScopeManager.class.getName()); private static final ResourceIdentifier SCRIPT_ID = new ResourceIdentifier(LIBRARY_NAME, SCRIPT_NAME); private static final String SCRIPT_INIT = "OmniFaces.Unload.init('%s')"; private static final int DEFAULT_BEANS_PER_VIEW_SCOPE = 3; private static final String ERROR_INVALID_STATE_SAVING = "@ViewScoped(saveInViewState=true) %s" + " requires web.xml context parameter 'javax.faces.STATE_SAVING_METHOD' being set to 'client'."; // Variables ------------------------------------------------------------------------------------------------------ @Inject private BeanManager manager; @Inject private ViewScopeStorageInSession storageInSession; @Inject private ViewScopeStorageInViewState storageInViewState; // Actions -------------------------------------------------------------------------------------------------------- /** * Create and returns the CDI view scoped managed bean from the current JSF view scope. * @param <T> The expected return type. * @param type The contextual type of the CDI managed bean. * @param context The CDI context to create the CDI managed bean in. * @return The created CDI view scoped managed bean from the current JSF view scope. */ public <T> T createBean(Contextual<T> type, CreationalContext<T> context) { return getBeanStorage(type).createBean(type, context); } /** * Returns the CDI view scoped managed bean from the current JSF view scope. * @param <T> The expected return type. * @param type The contextual type of the CDI managed bean. * @return The CDI view scoped managed bean from the current JSF view scope. */ public <T> T getBean(Contextual<T> type) { return getBeanStorage(type).getBean(type); } /** * This method is invoked during view destroy by {@link ViewScopeEventListener}, in that case destroy all beans in * current active view scope. */ public void preDestroyView() { FacesContext context = FacesContext.getCurrentInstance(); UUID beanStorageId = null; if (isUnloadRequest(context)) { try { beanStorageId = UUID.fromString(getRequestParameter(context, "id")); } catch (Exception ignore) { logger.log(FINE, "Ignoring thrown exception; this can only be a hacker attempt.", ignore); return; } } else if (isAjaxRequestWithPartialRendering(context)) { Hacks.setScriptResourceRendered(context, SCRIPT_ID); // Otherwise MyFaces will load a new one during createViewScope() when still in same document (e.g. navigation). } if (getInstance(manager, ViewScopeStorageInSession.class, false) != null) { // Avoid unnecessary session creation when accessing storageInSession for nothing. if (beanStorageId == null) { beanStorageId = storageInSession.getBeanStorageId(); } if (beanStorageId != null) { storageInSession.destroyBeans(beanStorageId); } } // View scoped beans stored in client side JSF view state are per definition undestroyable, therefore storageInViewState is ignored here. } // Helpers -------------------------------------------------------------------------------------------------------- private <T> BeanStorage getBeanStorage(Contextual<T> type) { ViewScopeStorage storage = storageInSession; Class<?> beanClass = ((Bean<T>) type).getBeanClass(); ViewScoped annotation = beanClass.getAnnotation(ViewScoped.class); if (annotation != null && annotation.saveInViewState()) { // Can be null when declared on producer method. checkStateSavingMethod(beanClass); storage = storageInViewState; } UUID beanStorageId = storage.getBeanStorageId(); if (beanStorageId == null) { beanStorageId = UUID.randomUUID(); if (storage instanceof ViewScopeStorageInSession) { registerUnloadScript(beanStorageId); } } BeanStorage beanStorage = storage.getBeanStorage(beanStorageId); if (beanStorage == null) { beanStorage = new BeanStorage(DEFAULT_BEANS_PER_VIEW_SCOPE); storage.setBeanStorage(beanStorageId, beanStorage); } return beanStorage; } private void checkStateSavingMethod(Class<?> beanClass) { FacesContext context = FacesContext.getCurrentInstance(); if (!context.getApplication().getStateManager().isSavingStateInClient(context)) { throw new IllegalStateException(format(ERROR_INVALID_STATE_SAVING, beanClass.getName())); } } /** * Register unload script. */ private static void registerUnloadScript(UUID beanStorageId) { FacesContext context = FacesContext.getCurrentInstance(); boolean ajaxRequestWithPartialRendering = isAjaxRequestWithPartialRendering(context); if (!Hacks.isScriptResourceRendered(context, SCRIPT_ID)) { if (ajaxRequestWithPartialRendering) { load(LIBRARY_NAME, UNLOAD_SCRIPT_NAME); } else if (context.getCurrentPhaseId() != PhaseId.RENDER_RESPONSE || TRUE.equals(context.getAttributes().get(StateManager.IS_BUILDING_INITIAL_STATE))) { addScriptResourceToHead(LIBRARY_NAME, SCRIPT_NAME); } else { addScriptResourceToBody(LIBRARY_NAME, UNLOAD_SCRIPT_NAME); } } String script = format(SCRIPT_INIT, beanStorageId); if (ajaxRequestWithPartialRendering) { oncomplete(script); } else { addScriptToBody(script); } } /** * Returns <code>true</code> if the current request is triggered by an unload request. * @param context The involved faces context. * @return <code>true</code> if the current request is triggered by an unload request. * @since 2.2 */ public static boolean isUnloadRequest(FacesContext context) { return "unload".equals(getRequestParameter(context, "omnifaces.event")); } }