/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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.apache.wicket.cdi; import javax.enterprise.context.Conversation; import javax.enterprise.context.ConversationScoped; import javax.enterprise.context.NonexistentConversationException; import javax.inject.Inject; import org.apache.wicket.Application; import org.apache.wicket.Component; import org.apache.wicket.MetaDataKey; import org.apache.wicket.Page; import org.apache.wicket.core.request.handler.BufferedResponseRequestHandler; import org.apache.wicket.core.request.handler.IPageClassRequestHandler; import org.apache.wicket.core.request.handler.IPageRequestHandler; import org.apache.wicket.core.request.mapper.StalePageException; import org.apache.wicket.request.IRequestHandler; import org.apache.wicket.request.IRequestHandlerDelegate; import org.apache.wicket.request.Url; import org.apache.wicket.request.component.IRequestablePage; import org.apache.wicket.request.cycle.IRequestCycleListener; import org.apache.wicket.request.cycle.RequestCycle; import org.apache.wicket.request.handler.resource.ResourceReferenceRequestHandler; import org.apache.wicket.request.mapper.parameter.PageParameters; import org.apache.wicket.request.resource.PackageResourceReference; import org.apache.wicket.util.lang.Args; import org.apache.wicket.util.lang.Objects; import org.apache.wicket.util.visit.IVisit; import org.apache.wicket.util.visit.IVisitor; import org.apache.wicket.util.visit.Visits; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A request cycle listener that takes care of propagating persistent conversations. * * @see ConversationScoped * * @author igor */ public class ConversationPropagator implements IRequestCycleListener { private static final Logger logger = LoggerFactory.getLogger(ConversationPropagator.class); private static final MetaDataKey<String> CID_KEY = ConversationIdMetaKey.INSTANCE; private static final MetaDataKey<Boolean> CONVERSATION_STARTED_KEY = new MetaDataKey<Boolean>() { }; public static final String CID = "cid"; private final CdiContainer container; /** propagation mode to use */ private final IConversationPropagation propagation; private final Application application; private final boolean auto; @Inject Conversation conversation_; @Inject AutoConversation autoConversation; /** * Constructor * * @param container * @param propagation */ public ConversationPropagator(Application application, CdiContainer container, IConversationPropagation propagation) { this(application, container, propagation, false); } /** * Constructor * * @param container * @param propagation */ public ConversationPropagator(Application application, CdiContainer container, IConversationPropagation propagation, boolean auto) { Args.notNull(application, "application"); Args.notNull(container, "container"); Args.notNull(propagation, "propagation"); if (propagation == ConversationPropagation.NONE) { throw new IllegalArgumentException( "If propagation is NONE do not set up the propagator"); } this.application = application; this.container = container; this.propagation = propagation; this.auto = auto; container.getNonContextualManager().postConstruct(this); } private Conversation getConversation(RequestCycle cycle) { return Boolean.TRUE.equals(cycle.getMetaData(CONVERSATION_STARTED_KEY)) ? conversation_ : null; } @Override public void onRequestHandlerResolved(RequestCycle cycle, IRequestHandler handler) { String cid = cycle.getRequest().getRequestParameters().getParameterValue(CID).toString(); Page page = getPage(handler); if (cid == null && page != null) { cid = page.getMetaData(CID_KEY); } Conversation current = getConversation(cycle); if (current != null && !Objects.isEqual(current.getId(), cid)) { logger.info("Conversation {} has expired for {}", cid, page); throw new ConversationExpiredException(null, cid, page, handler); } activateConversationIfNeeded(cycle, handler, cid); } @Override public IRequestHandler onException(RequestCycle cycle, Exception ex) { // if we are handling a stale page exception then use its conversation since we are most // likely about to rerender it. if (ex instanceof StalePageException) { IRequestablePage requestable = ((StalePageException)ex).getPage(); if (requestable instanceof Page) { String cid = container.getConversationMarker((Page)requestable); if (cid != null) { try { activateConversationIfNeeded(cycle, null, cid); return null; } catch (ConversationExpiredException e) { // ignore, we will start a new one below } } } } activateConversationIfNeeded(cycle, null, null); return null; } private void activateConversationIfNeeded(RequestCycle cycle, IRequestHandler handler, String cid) { Conversation current = getConversation(cycle); if (current != null || !activateForHandler(handler)) { return; } logger.debug("Activating conversation {}", cid); try { container.activateConversationalContext(cycle, cid); fireOnAfterConversationStarted(cycle); } catch (NonexistentConversationException e) { logger.info("Unable to restore conversation with id {}", cid, e.getMessage()); logger.debug("Unable to restore conversation", e); fireOnAfterConversationStarted(cycle); throw new ConversationExpiredException(e, cid, getPage(handler), handler); } cycle.setMetaData(CONVERSATION_STARTED_KEY, true); } private void fireOnAfterConversationStarted(RequestCycle cycle) { for (IRequestCycleListener listener : application.getRequestCycleListeners()) { if (listener instanceof ICdiAwareRequestCycleListener) { ((ICdiAwareRequestCycleListener)listener).onAfterConversationActivated(cycle); } } } @Override public void onRequestHandlerExecuted(RequestCycle cycle, IRequestHandler handler) { Conversation conversation = getConversation(cycle); if (conversation == null) { return; } Page page = getPage(handler); if (page == null) { return; } // apply auto semantics autoEndIfNecessary(page, handler, conversation); autoBeginIfNecessary(page, handler, conversation); if (propagation.propagatesViaPage(page, handler)) { // propagate a conversation across non-bookmarkable page instances setConversationOnPage(conversation, page); } } @Override public void onRequestHandlerScheduled(RequestCycle cycle, IRequestHandler handler) { // propagate current non-transient conversation to the newly scheduled page Conversation conversation = getConversation(cycle); if (conversation == null || conversation.isTransient()) { return; } Page page = getPage(handler); if (page != null) { if (propagation.propagatesViaPage(page, handler)) { // propagate a conversation across non-bookmarkable page instances setConversationOnPage(conversation, page); } } if (propagation.propagatesViaParameters(handler)) { // propagate cid to a scheduled bookmarkable page logger.debug( "Propagating non-transient conversation {} via page parameters of handler {}", conversation.getId(), handler); PageParameters parameters = getPageParameters(handler); if (parameters != null) { parameters.set(CID, conversation.getId()); } } } protected void setConversationOnPage(Conversation conversation, Page page) { if (conversation == null || conversation.isTransient()) { logger.debug("Detaching transient conversation {} via meta of page instance {}", (conversation == null ? "null" : conversation.getId()), page); page.setMetaData(CID_KEY, null); } else { logger.debug("Propagating non-transient conversation {} via meta of page instance {}", conversation.getId(), page); page.setMetaData(CID_KEY, conversation.getId()); } } @Override public void onUrlMapped(RequestCycle cycle, IRequestHandler handler, Url url) { // no need to propagate the conversation to packaged resources, they should never change if (handler instanceof ResourceReferenceRequestHandler) { if (((ResourceReferenceRequestHandler)handler).getResourceReference() instanceof PackageResourceReference) { return; } } Conversation conversation = getConversation(cycle); if (conversation == null || conversation.isTransient()) { return; } if (propagation.propagatesViaParameters(handler)) { // propagate cid to bookmarkable pages via urls logger.debug("Propagating non-transient conversation {} via url", conversation.getId()); url.setQueryParameter(CID, conversation.getId()); } } @Override public void onDetach(RequestCycle cycle) { Conversation conversation = getConversation(cycle); if (conversation != null) { logger.debug("Deactivating conversation {}", conversation.getId()); for (IRequestCycleListener listener : application.getRequestCycleListeners()) { if (listener instanceof ICdiAwareRequestCycleListener) { ((ICdiAwareRequestCycleListener)listener).onBeforeConversationDeactivated(cycle); } } container.deactivateConversationalContext(cycle); cycle.setMetaData(CONVERSATION_STARTED_KEY, null); } } /** * Determines whether or not a conversation should be activated fro the specified handler. This * method is used to filter out conversation activation for utility handlers such as the * {@link BufferedResponseRequestHandler} * * @param handler * @return {@code true} iff a conversation should be activated */ protected boolean activateForHandler(IRequestHandler handler) { if (handler != null) { if (handler instanceof BufferedResponseRequestHandler) { // we do not care about pages that are being rendered from a buffer return false; } } return true; } protected void autoBeginIfNecessary(Page page, IRequestHandler handler, Conversation conversation) { if (!auto || conversation == null || !conversation.isTransient() || page == null || !propagation.propagatesViaPage(page, handler) || !hasConversationalComponent(page)) { return; } // auto activate conversation conversation.begin(); autoConversation.setAutomatic(true); logger.debug("Auto-began conversation {} for page {}", conversation.getId(), page); } protected void autoEndIfNecessary(Page page, IRequestHandler handler, Conversation conversation) { if (!auto || conversation == null || conversation.isTransient() || page == null || !propagation.propagatesViaPage(page, handler) || hasConversationalComponent(page) || autoConversation.isAutomatic() == false) { return; } // auto de-activate conversation String cid = conversation.getId(); autoConversation.setAutomatic(false); conversation.end(); logger.debug("Auto-ended conversation {} for page {}", cid, page); } protected boolean hasConversationalComponent(Page page) { Boolean hasConversational = Visits.visit(page, new IVisitor<Component, Boolean>() { @Override public void component(Component object, IVisit<Boolean> visit) { if (object instanceof ConversationalComponent) { visit.stop(true); } } }); return hasConversational == null ? false : hasConversational; } /** * Resolves a page instance from the request handler iff the page instance is already created * * @param handler * @return page or {@code null} if none */ public static Page getPage(IRequestHandler handler) { while (handler instanceof IRequestHandlerDelegate) { handler = ((IRequestHandlerDelegate)handler).getDelegateHandler(); } if (handler instanceof IPageRequestHandler) { IPageRequestHandler pageHandler = (IPageRequestHandler)handler; if (pageHandler.isPageInstanceCreated()) { return (Page)pageHandler.getPage(); } } return null; } /** * Resolves page parameters from a request handler * * @param handler * @return page parameters or {@code null} if none */ protected PageParameters getPageParameters(IRequestHandler handler) { if (handler instanceof IPageClassRequestHandler) { IPageClassRequestHandler pageHandler = (IPageClassRequestHandler)handler; return pageHandler.getPageParameters(); } return null; } }