/* * 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.request.cycle; import java.util.Optional; import org.apache.wicket.Application; import org.apache.wicket.MetaDataEntry; import org.apache.wicket.MetaDataKey; import org.apache.wicket.Page; import org.apache.wicket.Session; import org.apache.wicket.ThreadContext; import org.apache.wicket.WicketRuntimeException; import org.apache.wicket.core.request.handler.BookmarkablePageRequestHandler; import org.apache.wicket.core.request.handler.IPageProvider; import org.apache.wicket.core.request.handler.PageProvider; import org.apache.wicket.core.request.handler.RenderPageRequestHandler; import org.apache.wicket.event.IEvent; import org.apache.wicket.event.IEventSink; import org.apache.wicket.protocol.http.IRequestLogger; import org.apache.wicket.request.IExceptionMapper; import org.apache.wicket.request.IRequestCycle; import org.apache.wicket.request.IRequestHandler; import org.apache.wicket.request.IRequestMapper; import org.apache.wicket.request.Request; import org.apache.wicket.request.RequestHandlerExecutor; import org.apache.wicket.request.RequestHandlerExecutor.ReplaceHandlerException; import org.apache.wicket.request.Response; import org.apache.wicket.request.Url; import org.apache.wicket.request.UrlRenderer; import org.apache.wicket.request.component.IRequestablePage; import org.apache.wicket.request.handler.resource.ResourceReferenceRequestHandler; import org.apache.wicket.request.handler.resource.ResourceRequestHandler; import org.apache.wicket.request.mapper.parameter.PageParameters; import org.apache.wicket.request.resource.IResource; import org.apache.wicket.request.resource.ResourceReference; import org.apache.wicket.request.resource.caching.IStaticCacheableResource; import org.apache.wicket.util.lang.Args; import org.apache.wicket.util.lang.Exceptions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * {@link RequestCycle} consists of two steps: * <ol> * <li>Resolve request handler * <li>Execute request handler * </ol> * During {@link IRequestHandler} execution the handler can schedule another {@link IRequestHandler} * to run after it is done, or replace all {@link IRequestHandler}s on stack with another * {@link IRequestHandler}. * * @see #scheduleRequestHandlerAfterCurrent(IRequestHandler) * @see #replaceAllRequestHandlers(IRequestHandler) * * @author Matej Knopp * @author igor.vaynberg */ public class RequestCycle implements IRequestCycle, IEventSink { private static final Logger log = LoggerFactory.getLogger(RequestCycle.class); /** * An additional logger which is used to log extra information. * Could be disabled separately than the main logger if the application developer * does not want to see this extra information. */ private static final Logger logExtra = LoggerFactory.getLogger("RequestCycleExtra"); /** * Returns request cycle associated with current thread. * * @return request cycle instance or <code>null</code> if no request cycle is associated with * current thread. */ public static RequestCycle get() { return ThreadContext.getRequestCycle(); } /** * * @param requestCycle */ private static void set(RequestCycle requestCycle) { ThreadContext.setRequestCycle(requestCycle); } private Request request; private final Response originalResponse; private final IRequestMapper requestMapper; private final IExceptionMapper exceptionMapper; private final RequestCycleListenerCollection listeners; private UrlRenderer urlRenderer; /** MetaDataEntry array. */ private MetaDataEntry<?>[] metaData; /** the time that this request cycle object was created. */ private final long startTime; private final RequestHandlerExecutor requestHandlerExecutor; private Response activeResponse; /** * Construct. * * @param context */ public RequestCycle(RequestCycleContext context) { Args.notNull(context, "context"); Args.notNull(context.getRequest(), "context.request"); Args.notNull(context.getResponse(), "context.response"); Args.notNull(context.getRequestMapper(), "context.requestMapper"); Args.notNull(context.getExceptionMapper(), "context.exceptionMapper"); listeners = new RequestCycleListenerCollection(); startTime = System.currentTimeMillis(); requestHandlerExecutor = new HandlerExecutor(); activeResponse = context.getResponse(); request = context.getRequest(); originalResponse = context.getResponse(); requestMapper = context.getRequestMapper(); exceptionMapper = context.getExceptionMapper(); } /** * * @return a new url renderer */ protected UrlRenderer newUrlRenderer() { // All URLs will be rendered relative to current request (can be overridden afterwards) return new UrlRenderer(getRequest()); } /** * Get the original response the request was created with. Access to the original response may * be necessary if the response has been temporarily replaced but the components require methods * from original response (i.e. cookie methods of WebResponse, etc). * * @return The original response object. */ public Response getOriginalResponse() { return originalResponse; } /** * Returns {@link UrlRenderer} for this {@link RequestCycle}. * * @return UrlRenderer instance. */ @Override public final UrlRenderer getUrlRenderer() { if (urlRenderer == null) { urlRenderer = newUrlRenderer(); } return urlRenderer; } /** * Resolves current request to a {@link IRequestHandler}. * * @return RequestHandler instance */ protected IRequestHandler resolveRequestHandler() { return requestMapper.mapRequest(request); } /** * @return How many times will Wicket attempt to render the exception request handler before * giving up. */ protected int getExceptionRetryCount() { int retries = 10; if (Application.exists()) { retries = Application.get().getRequestCycleSettings().getExceptionRetryCount(); } return retries; } /** * Convenience method that processes the request and detaches the {@link RequestCycle}. * * @return <code>true</code> if the request resolved to a Wicket request, <code>false</code> * otherwise. */ public boolean processRequestAndDetach() { boolean result; try { result = processRequest(); } finally { detach(); } return result; } /** * Processes the request. * * @return <code>true</code> if the request resolved to a Wicket request, <code>false</code> * otherwise. */ public boolean processRequest() { try { set(this); listeners.onBeginRequest(this); onBeginRequest(); IRequestHandler handler = resolveRequestHandler(); if (handler == null) { // Did not find any suitable handler, thus not executing the request log.debug( "No suitable handler found for URL {}, falling back to container to process this request", request.getUrl()); } else { execute(handler); return true; } } catch (Exception exception) { executeExceptionRequestHandler(exception, getExceptionRetryCount()); return true; } finally { set(null); } return false; } /** * Execute a request handler and notify registered {@link IRequestCycleListener}s. * * @param handler */ private void execute(IRequestHandler handler) { Args.notNull(handler, "handler"); while (handler != null) { try { listeners.onRequestHandlerResolved(this, handler); IRequestHandler next = requestHandlerExecutor.execute(handler); listeners.onRequestHandlerExecuted(this, handler); handler = next; } catch (RuntimeException e) { ReplaceHandlerException replacer = Exceptions.findCause(e, ReplaceHandlerException.class); if (replacer == null) { throw e; } if (replacer.getRemoveScheduled()) { requestHandlerExecutor.schedule(null); } handler = replacer.getReplacementRequestHandler(); } } } /** * Execute a requestHandler for the given exception. * * @param exception * @param retryCount */ private void executeExceptionRequestHandler(Exception exception, int retryCount) { scheduleRequestHandlerAfterCurrent(null); IRequestHandler handler = handleException(exception); if (handler == null) { log.error("Error during request processing. URL=" + request.getUrl(), exception); return; } try { listeners.onExceptionRequestHandlerResolved(this, handler, exception); execute(handler); } catch (Exception e) { if (retryCount > 0) { executeExceptionRequestHandler(exception, retryCount - 1); } else { log.error("Exception retry count exceeded", e); } } } /** * Return {@link IRequestHandler} for the given exception. * * @param e exception to handle * @return RequestHandler instance * * @see IRequestCycleListener#onException(RequestCycle, Exception) * @see IExceptionMapper#map(Exception) */ protected IRequestHandler handleException(final Exception e) { if (Application.exists() && Application.get().usesDevelopmentConfig()) { /* * Call out the fact that we are processing an exception in a loud way, helps to notice * them when developing even if they get wrapped or processed in a custom handler. */ logExtra.warn("********************************"); logExtra.warn("Handling the following exception", e); logExtra.warn("********************************"); } IRequestHandler handler = listeners.onException(this, e); if (handler != null) { return handler; } return exceptionMapper.map(e); } /** * @return current request */ @Override public Request getRequest() { return request; } /** * INTERNAL This method is for internal Wicket use. Do not call it yourself unless you know what * you are doing. * * @param request */ public void setRequest(Request request) { // It would be mighty nice if request was final. However during multipart it needs to be set // to // MultipartServletWebRequest by Form. It can't be done before creating the request cycle // (in wicket filter) // because only the form knows max upload size this.request = request; } /** * Sets the metadata for this request cycle using the given key. If the metadata object is not * of the correct type for the metadata key, an IllegalArgumentException will be thrown. For * information on creating MetaDataKeys, see {@link MetaDataKey}. * * @param key * The singleton key for the metadata * @param object * The metadata object * @param <T> * @throws IllegalArgumentException * @see MetaDataKey */ public final <T> RequestCycle setMetaData(final MetaDataKey<T> key, final T object) { metaData = key.set(metaData, object); return this; } /** * Gets metadata for this request cycle using the given key. * * @param <T> * The type of the metadata * * @param key * The key for the data * @return The metadata or null if no metadata was found for the given key * @see MetaDataKey */ public final <T> T getMetaData(final MetaDataKey<T> key) { return key.get(metaData); } /** * Returns URL for the request handler or <code>null</code> if the handler couldn't have been * encoded. * <p> * <strong>Note</strong>: The produced URL is relative to the filter path. Application code most * probably need URL relative to the currently used page, for this use * {@linkplain #urlFor(org.apache.wicket.request.IRequestHandler)} * </p> * * @param handler * the {@link IRequestHandler request handler} for which to create a callback url * @return Url instance or <code>null</code> */ public Url mapUrlFor(IRequestHandler handler) { final Url url = requestMapper.mapHandler(handler); listeners.onUrlMapped(this, handler, url); return url; } /** * Returns a {@link Url} for the resource reference * <p> * <strong>Note</strong>: The produced URL is relative to the filter path. Application code most * probably need URL relative to the currently used page, for this use * {@linkplain #urlFor(org.apache.wicket.request.resource.ResourceReference, org.apache.wicket.request.mapper.parameter.PageParameters)} * </p> * * @param reference * resource reference * @param params * parameters for the resource or {@code null} if none * @return {@link Url} for the reference */ public Url mapUrlFor(ResourceReference reference, PageParameters params) { return mapUrlFor(new ResourceReferenceRequestHandler(reference, params)); } /** * Returns a bookmarkable URL that references a given page class using a given set of page * parameters. Since the URL which is returned contains all information necessary to instantiate * and render the page, it can be stored in a user's browser as a stable bookmark. * <p> * <strong>Note</strong>: The produced URL is relative to the filter path. Application code most * probably need URL relative to the currently used page, for this use * {@linkplain #urlFor(Class, org.apache.wicket.request.mapper.parameter.PageParameters)} * </p> * * @param <C> * The type of the page * @param pageClass * Class of page * @param parameters * Parameters to page or {@code null} if none * @return Bookmarkable URL to page */ public final <C extends Page> Url mapUrlFor(final Class<C> pageClass, final PageParameters parameters) { IRequestHandler handler = new BookmarkablePageRequestHandler(new PageProvider(pageClass, parameters)); return mapUrlFor(handler); } /** * Returns a rendered {@link Url} for the resource reference * * @param reference * resource reference * @param params * parameters for the resource or {@code null} if none * @return {@link Url} for the reference */ public final CharSequence urlFor(ResourceReference reference, PageParameters params) { ResourceReferenceRequestHandler handler = new ResourceReferenceRequestHandler(reference, params); return urlFor(handler); } /** * Returns a rendered bookmarkable URL that references a given page class using a given set of * page parameters. Since the URL which is returned contains all information necessary to * instantiate and render the page, it can be stored in a user's browser as a stable bookmark. * * @param <C> * * @param pageClass * Class of page * @param parameters * Parameters to page or {@code null} if none * @return Bookmarkable URL to page */ public final <C extends Page> CharSequence urlFor(final Class<C> pageClass, final PageParameters parameters) { IRequestHandler handler = new BookmarkablePageRequestHandler(new PageProvider(pageClass, parameters)); return urlFor(handler); } /** * Returns the rendered URL for the request handler or <code>null</code> if the handler couldn't * have been rendered. * <p> * The resulting URL will be relative to current page. * * @param handler * @return Url String or <code>null</code> */ public CharSequence urlFor(IRequestHandler handler) { try { Url mappedUrl = mapUrlFor(handler); CharSequence url = renderUrl(mappedUrl, handler); return url; } catch (Exception x) { throw new WicketRuntimeException(String.format( "An error occurred while generating an Url for handler '%s'", handler), x); } } private String renderUrl(Url url, IRequestHandler handler) { if (url != null) { boolean shouldEncodeStaticResource = Application.exists() && Application.get().getResourceSettings().isEncodeJSessionId(); String renderedUrl = getUrlRenderer().renderUrl(url); if (handler instanceof ResourceReferenceRequestHandler) { ResourceReferenceRequestHandler rrrh = (ResourceReferenceRequestHandler)handler; IResource resource = rrrh.getResource(); if (resource != null && !(resource instanceof IStaticCacheableResource) || shouldEncodeStaticResource) { renderedUrl = getOriginalResponse().encodeURL(renderedUrl); } } else if (handler instanceof ResourceRequestHandler) { ResourceRequestHandler rrh = (ResourceRequestHandler)handler; IResource resource = rrh.getResource(); if (resource != null && !(resource instanceof IStaticCacheableResource) || shouldEncodeStaticResource) { renderedUrl = getOriginalResponse().encodeURL(renderedUrl); } } else { renderedUrl = getOriginalResponse().encodeURL(renderedUrl); } return renderedUrl; } else { return null; } } /** * Detaches {@link RequestCycle} state. Called after request processing is complete */ public final void detach() { set(this); try { onDetach(); } finally { try { onInternalDetach(); } finally { set(null); } } } private void onInternalDetach() { if (Session.exists()) { Session.get().internalDetach(); } if (Application.exists()) { IRequestLogger requestLogger = Application.get().getRequestLogger(); if (requestLogger != null) requestLogger.performLogging(); } } /** * Called after request processing is complete, usually takes care of detaching state */ public void onDetach() { try { onEndRequest(); listeners.onEndRequest(this); } catch (RuntimeException e) { log.error("Exception occurred during onEndRequest", e); } try { requestHandlerExecutor.detach(); } catch (RuntimeException exception) { handleDetachException(exception); } finally { listeners.onDetach(this); } if (Session.exists()) { Session.get().detach(); } } /** * Called to handle a {@link java.lang.RuntimeException} that might be * thrown during detaching phase. * * @param exception */ private void handleDetachException(RuntimeException exception) { boolean isBufferedResponse = true; if (Application.exists()) { isBufferedResponse = Application.get().getRequestCycleSettings().getBufferResponse(); } //if application is using a buffered response strategy, //then we display exception to user. if (isBufferedResponse) { throw exception; } else { log.error("Error detaching RequestCycle", exception); } } /** * Convenience method for setting next page to be rendered. * * @param page */ public void setResponsePage(IRequestablePage page) { if (page instanceof Page) { ((Page) page).setStatelessHint(false); } scheduleRequestHandlerAfterCurrent(new RenderPageRequestHandler(new PageProvider(page), RenderPageRequestHandler.RedirectPolicy.AUTO_REDIRECT)); } /** * Convenience method for setting next page to be rendered. * * @param pageClass * The class of the page to render */ public void setResponsePage(Class<? extends IRequestablePage> pageClass) { setResponsePage(pageClass, null, RenderPageRequestHandler.RedirectPolicy.ALWAYS_REDIRECT); } /** * Convenience method for setting next page to be rendered. * * @param pageClass * The class of the page to render * @param redirectPolicy * The policy to use when deciding whether to redirect or not */ public void setResponsePage(Class<? extends IRequestablePage> pageClass, RenderPageRequestHandler.RedirectPolicy redirectPolicy) { setResponsePage(pageClass, null, redirectPolicy); } /** * Convenience method for setting next page to be rendered. * * @param pageClass * The class of the page to render * @param parameters * The query parameters for the page to be rendered */ public void setResponsePage(Class<? extends IRequestablePage> pageClass, PageParameters parameters) { setResponsePage(pageClass, parameters, RenderPageRequestHandler.RedirectPolicy.ALWAYS_REDIRECT); } /** * Convenience method for setting next page to be rendered. * * @param pageClass * The class of the page to render * @param parameters * The query parameters for the page to be rendered * @param redirectPolicy * The policy to use when deciding whether to redirect or not */ public void setResponsePage(Class<? extends IRequestablePage> pageClass, PageParameters parameters, RenderPageRequestHandler.RedirectPolicy redirectPolicy) { IPageProvider provider = new PageProvider(pageClass, parameters); scheduleRequestHandlerAfterCurrent(new RenderPageRequestHandler(provider, redirectPolicy)); } /** * @return The start time for this request */ public final long getStartTime() { return startTime; } /** {@inheritDoc} */ @Override public void onEvent(IEvent<?> event) { } /** * Called when the request cycle object is beginning its response */ protected void onBeginRequest() { } /** * Called when the request cycle object has finished its response */ protected void onEndRequest() { } /** * @return listeners */ public RequestCycleListenerCollection getListeners() { return listeners; } /** * {@inheritDoc} */ @Override public Response getResponse() { return activeResponse; } /** * {@inheritDoc} */ @Override public Response setResponse(final Response response) { Response current = activeResponse; activeResponse = response; return current; } /** * {@inheritDoc} */ @Override public void scheduleRequestHandlerAfterCurrent(IRequestHandler handler) { // just delegating the call to {@link IRequestHandlerExecutor} and invoking listeners requestHandlerExecutor.schedule(handler); // only forward calls to the listeners when handler is null if (handler != null) listeners.onRequestHandlerScheduled(this, handler); } /** * @see RequestHandlerExecutor#getActive() * @return active handler on executor */ public IRequestHandler getActiveRequestHandler() { return requestHandlerExecutor.getActive(); } /** * @see RequestHandlerExecutor#next() * @return the handler scheduled to be executed after current by the executor */ public IRequestHandler getRequestHandlerScheduledAfterCurrent() { return requestHandlerExecutor.next(); } /** * @see RequestHandlerExecutor#replaceAll(IRequestHandler) * @param handler */ public void replaceAllRequestHandlers(final IRequestHandler handler) { requestHandlerExecutor.replaceAll(handler); } /** * Finds a IRequestHandler which is either the currently executing handler or is scheduled to be * executed. * * @return the found IRequestHandler or {@link Optional#empty()} */ @SuppressWarnings("unchecked") public <T extends IRequestHandler> Optional<T> find(final Class<T> type) { if (type == null) { return Optional.empty(); } IRequestHandler result = getActiveRequestHandler(); if (type.isInstance(result)) { return (Optional<T>)Optional.of(result); } result = getRequestHandlerScheduledAfterCurrent(); if (type.isInstance(result)) { return (Optional<T>)Optional.of(result); } return Optional.empty(); } /** * Adapts {@link RequestHandlerExecutor} to this {@link RequestCycle} * * @author Igor Vaynberg */ private class HandlerExecutor extends RequestHandlerExecutor { @Override protected void respond(IRequestHandler handler) { Response originalResponse = getResponse(); try { handler.respond(RequestCycle.this); } finally { setResponse(originalResponse); } } @Override protected void detach(IRequestHandler handler) { handler.detach(RequestCycle.this); } } }