/*
* JBoss, a division of Red Hat
* Copyright 2011, Red Hat Middleware, LLC, and individual
* contributors as indicated by the @authors tag. See the
* copyright.txt in the distribution for a full listing of
* individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.gatein.wsrp.consumer.handlers;
import org.gatein.common.net.media.MediaType;
import org.gatein.common.util.ParameterValidation;
import org.gatein.pc.api.PortletInvokerException;
import org.gatein.pc.api.StateString;
import org.gatein.pc.api.invocation.PortletInvocation;
import org.gatein.pc.api.invocation.response.ErrorResponse;
import org.gatein.pc.api.invocation.response.PortletInvocationResponse;
import org.gatein.pc.api.spi.PortletInvocationContext;
import org.gatein.pc.api.spi.SecurityContext;
import org.gatein.pc.api.spi.WindowContext;
import org.gatein.wsrp.WSRPConstants;
import org.gatein.wsrp.WSRPTypeFactory;
import org.gatein.wsrp.WSRPUtils;
import org.gatein.wsrp.api.extensions.ExtensionAccess;
import org.gatein.wsrp.api.extensions.UnmarshalledExtension;
import org.gatein.wsrp.consumer.WSRPConsumerImpl;
import org.gatein.wsrp.consumer.portlet.info.WSRPPortletInfo;
import org.gatein.wsrp.consumer.spi.WSRPConsumerSPI;
import org.gatein.wsrp.payload.PayloadUtils;
import org.gatein.wsrp.spec.v2.WSRP2RewritingConstants;
import org.oasis.wsrp.v2.Extension;
import org.oasis.wsrp.v2.InvalidCookie;
import org.oasis.wsrp.v2.InvalidRegistration;
import org.oasis.wsrp.v2.InvalidSession;
import org.oasis.wsrp.v2.MarkupParams;
import org.oasis.wsrp.v2.ModifyRegistrationRequired;
import org.oasis.wsrp.v2.NavigationalContext;
import org.oasis.wsrp.v2.OperationFailed;
import org.oasis.wsrp.v2.PortletContext;
import org.oasis.wsrp.v2.RegistrationContext;
import org.oasis.wsrp.v2.RuntimeContext;
import org.oasis.wsrp.v2.UserContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.rmi.RemoteException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* Handles a specific type of PortletInvocation, translating it back and forth into WSRP-understable structures.
*
* @param <Invocation> the type of PortletInvocation this InvocationHandler handles
* @param <Request> the type of WSRP request this InvocationHandler can translate to from a portlet container request
* @param <Response> the type of WSRP response this InvocationHandler can translate back to portlet container responses
* @author <a href="mailto:chris.laprun@jboss.com">Chris Laprun</a>
* @version $Revision: 13121 $
* @since 2.4 (May 31, 2006)
*/
public abstract class InvocationHandler<Invocation extends PortletInvocation, Request, Response>
{
/** The consumer owning this handler */
protected final WSRPConsumerSPI consumer;
protected static Logger log = LoggerFactory.getLogger(InvocationHandler.class);
protected static boolean debug = log.isDebugEnabled();
protected static boolean trace = log.isTraceEnabled();
/**
* Value indicating that we should not try further (unrecoverable error) for getMarkup and
* processBlockingInteraction
*/
private static final int DO_NOT_RETRY = -1;
/** Maximum number of tries before giving up. */
private static final int MAXIMUM_RETRY_NUMBER = 3;
protected InvocationHandler(WSRPConsumerSPI consumer)
{
this.consumer = consumer;
}
/**
* Translates a portlet container request into a WSRP request, calls the appropriate WSRP operation and translates the received response back into something the portlet
* container can deal with, taking care of any exception, dealing with the ones we can or transforming them into portlet container exceptions when we can't deal with them
* ourselves. Follows the Template Method design pattern.
*
* @param invocation the initiating portlet container request that will be transformed into a WSRP request
* @return an appropriate PortletInvocationResponse translated from the WSRP response sent by the producer
* @throws PortletInvokerException
*/
public PortletInvocationResponse handle(Invocation invocation) throws PortletInvokerException
{
// Extracts basic, common required information from invocation
RequestPrecursor<Invocation> requestPrecursor = new RequestPrecursor<Invocation>(consumer, invocation);
// create the specific request, customizing it with specific parameters if needed
Request request = prepareRequest(requestPrecursor, invocation);
try
{
// Perform the request and get the response
Response response = performRequest(request, invocation);
// process the response
return processResponse(response, invocation, requestPrecursor);
}
catch (Exception e)
{
// if we didn't get a straight PortletInvokerException (which means we already asserted that the WSRP can't deal with it), try to transform it into something we can deal with
if (!(e instanceof PortletInvokerException))
{
final PortletInvocationResponse response = dealWithError(e, invocation, getRuntimeContextFrom(request));
if (response instanceof ErrorResponse)
{
return unwrapWSRPError((ErrorResponse)response);
}
return response;
}
else
{
throw (PortletInvokerException)e;
}
}
}
/**
* Attempts to perform the specified request, taking care of setting and updating cookies if required, at most {@link #MAXIMUM_RETRY_NUMBER} times to give the consumer the
* opportunity to react to specific errors (such as need to invoke initCookie or modifyRegistration) that can sometimes be recovered from.
*
* @param request the request to perform
* @param invocation the PortletInvocation that initiated the current WSRP request
* @return the producer's reponse
* @throws Exception
*/
protected Response performRequest(Request request, PortletInvocation invocation) throws Exception
{
int retryCount = 0;
Response response = null;
// as long as we don't get a non-null response and we're allowed to try again, try to perform the request
while (response == null && retryCount++ <= MAXIMUM_RETRY_NUMBER)
{
if (debug)
{
log.debug("performRequest: " + retryCount + " attempt(s) out of " + MAXIMUM_RETRY_NUMBER + " possible");
}
SessionHandler sessionHandler = consumer.getSessionHandler();
// prepare everything for the request
RuntimeContext runtimeContext = getRuntimeContextFrom(request);
if (runtimeContext != null)
{
WindowContext windowContext = invocation.getWindowContext();
runtimeContext.setNamespacePrefix(WSRPTypeFactory.getNamespaceFrom(windowContext));
// GTNWSRP-369: InstanceContext doesn't actually provide any useful information, use WindowContext's id instead
/*InstanceContext instanceContext = invocation.getInstanceContext();
runtimeContext.setPortletInstanceKey(WSRPTypeFactory.getPortletInstanceKey(instanceContext));*/
runtimeContext.setPortletInstanceKey(windowContext.getId());
}
try
{
sessionHandler.initCookieIfNeeded(invocation);
response = performRequest(request);
sessionHandler.updateCookiesIfNeeded(invocation);
}
finally
{
// we're done: reset currently held information
sessionHandler.resetCurrentlyHeldInformation();
}
}
if (retryCount >= MAXIMUM_RETRY_NUMBER)
{
throw new RuntimeException("Tried to perform request " + MAXIMUM_RETRY_NUMBER
+ " times before giving up. This usually happens if an error in the WS stack prevented the messages to be " +
"properly transmitted. Look at server.log for clues as to what happened...");
}
if (debug)
{
log.debug("performRequest finished. Response is " + (response != null ? response.getClass().getName() : null));
}
return response;
}
/**
* Deals with common error conditions.
*
* @param error the error that is to be dealt with
* @param invocation the invocation that caused the error to occur
* @param runtimeContext the current WSRP RuntimeContext
* @return an ErrorResponse if the error couldn't be dealt with or <code>null</code> if the error was correctly
* handled
*/
private PortletInvocationResponse dealWithError(Exception error, Invocation invocation, RuntimeContext runtimeContext) throws PortletInvokerException
{
log.error("The portlet threw an exception", error);
SessionHandler sessionHandler = consumer.getSessionHandler();
// recoverable errors
if (error instanceof InvalidCookie)
{
// we need to re-init the cookies
log.debug("Re-initializing cookies after InvalidCookieFault.");
// force a producer info refresh because the invalid cookie might be due to a change of cookie policy on the producer
consumer.refreshProducerInfo();
try
{
sessionHandler.initCookieIfNeeded(invocation);
// re-attempt invocation since we can recover from this error
return handle(invocation);
}
catch (Exception e)
{
log.debug("Couldn't init cookie: " + e.getLocalizedMessage());
return new ErrorResponse(e);
}
}
else if (error instanceof InvalidSession)
{
// invalidate the currently held session information
log.debug("Session invalidated after InvalidSessionFault, will re-send session-stored information.");
sessionHandler.handleInvalidSessionFault(invocation, runtimeContext);
// and re-attempt invocation as we can recover from this
return handle(invocation);
}
else if (error instanceof InvalidRegistration)
{
// invalidate the registration information, we can't recover from this, the user will have to check the admin UI to see what's wrong
consumer.handleInvalidRegistrationFault();
return new ErrorResponse(error);
}
else if (error instanceof ModifyRegistrationRequired)
{
// we can't recover from this, the user will need to check the admin UI to see how to modify the current registration to make it comply with the new producer's requirements
consumer.handleModifyRegistrationRequiredFault();
return new ErrorResponse(error);
}
else
{
// other errors cannot be dealt with: we have an error condition
return new ErrorResponse(error);
}
}
/**
* Attempts to unwrap nested errors to make them more palatable to users.
*
* @param errorResponse the error response we're trying to make simpler
* @return hopefully, a simplified error response, one that more clearly identifies the root issue
*/
protected ErrorResponse unwrapWSRPError(ErrorResponse errorResponse)
{
Throwable cause = errorResponse.getCause();
if (cause != null)
{
// unwrap original exception...
if (cause instanceof OperationFailed && cause.getCause() != null)
{
cause = cause.getCause();
}
else if (cause instanceof RemoteException)
{
cause = ((RemoteException)cause).detail;
}
log.debug("Invocation of action failed: " + cause.getMessage(), cause); // fix-me?
return new ErrorResponse(cause);
}
else
{
log.debug("Invocation of action failed: " + errorResponse.getMessage());
return errorResponse;
}
}
// template method hook points
/**
* Extracts the RuntimeContext from the specific WSRP request.
*
* @param request the request to extract a RuntimeContext from
* @return the RuntimeContext instance associated with the specified request
*/
protected abstract RuntimeContext getRuntimeContextFrom(Request request);
/**
* Performs the actual specific WSRP call for the specified request.
*
* @param request the WSRP request to perform
* @return the producer's response
* @throws Exception
*/
protected abstract Response performRequest(Request request) throws Exception;
/**
* Created and further prepares the specific requests based on common extracted information from the specified RequestPrecursor and the originating portlet invocation.
*
* @param requestPrecursor the common extracted information for this request
* @param invocation the portlet invocation from which we're trying to perform a WSRP call
* @return the fully prepared request
*/
protected abstract Request prepareRequest(RequestPrecursor<Invocation> requestPrecursor, Invocation invocation);
/**
* Converts the WSRP response into a portlet container {@link PortletInvocationResponse} based on its type and on whether other WSRP components need to be informed of potential
* changes from the producer.
*
* @param response the original WSRP response
* @param invocation the PortletInvocation that triggered the WSRP call
* @param requestPrecursor the request precursor information we extracted before performing the request
* @return the appropriate PortletInvocationResponse based on the WSRP producer's response
* @throws PortletInvokerException
*/
protected abstract PortletInvocationResponse processResponse(Response response, Invocation invocation, RequestPrecursor<Invocation> requestPrecursor) throws PortletInvokerException;
/**
* Extracts extensions from the response.
*
* @param response the WSRP response to extract extensions from
* @return a potentially empty list of extensions for the specified response
*/
protected abstract List<Extension> getExtensionsFrom(Response response);
/**
* Processes extensions, making them available if needed to the {@link org.gatein.wsrp.api.extensions.ConsumerExtensionAccessor}. Used by subclasses.
*/
protected void processExtensions(Response response)
{
final List<Extension> extensions = WSRPUtils.replaceByEmptyListIfNeeded(getExtensionsFrom(response));
for (Extension extension : extensions)
{
try
{
final UnmarshalledExtension unmarshalledExtension = PayloadUtils.unmarshallExtension(extension.getAny());
ExtensionAccess.getConsumerExtensionAccessor().addResponseExtension(response.getClass(), unmarshalledExtension);
}
catch (Exception e)
{
log.debug("Couldn't unmarshall extension from producer, ignoring it.", e);
}
}
}
/**
* Extracts basic required elements for all invocation requests.
*
* @author <a href="mailto:chris.laprun@jboss.com">Chris Laprun</a>
* @version $Revision: 13121 $
* @since 2.4
*/
protected static class RequestPrecursor<Invocation extends PortletInvocation>
{
private static final Logger log = LoggerFactory.getLogger(RequestPrecursor.class);
private final PortletContext portletContext;
private final RuntimeContext runtimeContext;
private final MarkupParams markupParams;
private final RegistrationContext registrationContext;
private final UserContext userContext;
private static final String PORTLET_HANDLE = "portlet handle";
private static final String SECURITY_CONTEXT = "security context";
private static final String USER_CONTEXT = "user context";
private static final String INVOCATION_CONTEXT = "invocation context";
private static final String CONTENT_TYPE = "response content type in invocation context";
private static final String USER_AGENT = "User-Agent";
public RequestPrecursor(WSRPConsumerSPI wsrpConsumer, Invocation invocation) throws PortletInvokerException
{
// retrieve handle
portletContext = WSRPUtils.convertToWSRPPortletContext(WSRPConsumerImpl.getPortletContext(invocation));
ParameterValidation.throwIllegalArgExceptionIfNullOrEmpty(getPortletHandle(), PORTLET_HANDLE, null);
if (log.isDebugEnabled())
{
log.debug("About to invoke on portlet: " + getPortletHandle());
}
// registration context
registrationContext = wsrpConsumer.getRegistrationContext();
// create runtime context
SecurityContext securityContext = invocation.getSecurityContext();
ParameterValidation.throwIllegalArgExceptionIfNull(securityContext, SECURITY_CONTEXT);
String authType = WSRPUtils.convertRequestAuthTypeToWSRPAuthType(securityContext.getAuthType());
String portletInstanceKey = WSRPTypeFactory.getPortletInstanceKey(invocation.getInstanceContext());
String namespacePrefix = WSRPTypeFactory.getNamespacePrefix(invocation.getWindowContext(), getPortletHandle());
runtimeContext = WSRPTypeFactory.createRuntimeContext(authType, portletInstanceKey, namespacePrefix);
WSRPPortletInfo info = wsrpConsumer.getPortletInfo(invocation);
// user context
userContext = wsrpConsumer.getUserContextFrom(info, invocation, runtimeContext);
// templates
wsrpConsumer.setTemplatesIfNeeded(info, invocation, getRuntimeContext());
// set the session id if needed
wsrpConsumer.getSessionHandler().setSessionIdIfNeeded(invocation, getRuntimeContext(), getPortletHandle());
// create markup params
org.gatein.pc.api.spi.UserContext userContext = invocation.getUserContext();
ParameterValidation.throwIllegalArgExceptionIfNull(userContext, USER_CONTEXT);
PortletInvocationContext context = invocation.getContext();
ParameterValidation.throwIllegalArgExceptionIfNull(context, INVOCATION_CONTEXT);
final MediaType contentType = context.getResponseContentType();
ParameterValidation.throwIllegalArgExceptionIfNull(contentType, CONTENT_TYPE);
String mode;
try
{
mode = WSRPUtils.getWSRPNameFromJSR168PortletMode(invocation.getMode());
}
catch (Exception e)
{
log.debug("Mode was null in context.");
mode = WSRPConstants.VIEW_MODE;
}
String windowState;
try
{
windowState = WSRPUtils.getWSRPNameFromJSR168WindowState(invocation.getWindowState());
}
catch (Exception e)
{
log.debug("WindowState was null in context.");
windowState = WSRPConstants.NORMAL_WINDOW_STATE;
}
this.markupParams = WSRPTypeFactory.createMarkupParams(securityContext.isSecure(),
WSRPUtils.convertLocalesToRFC3066LanguageTags(userContext.getLocales()),
Collections.singletonList(contentType.getValue()), mode, windowState);
String userAgent = WSRPConsumerImpl.getHttpRequest(invocation).getHeader(USER_AGENT);
getMarkupParams().setClientData(WSRPTypeFactory.createClientData(userAgent));
getMarkupParams().getExtensions().addAll(ExtensionAccess.getConsumerExtensionAccessor().getRequestExtensionsFor(MarkupParams.class));
// navigational state
StateString navigationalState = invocation.getNavigationalState();
Map<String, String[]> publicNavigationalState = invocation.getPublicNavigationalState();
// it is possible to get additional public navigational state from the invocation attributes if the producer used templates:
String publicNS = (String)invocation.getAttribute(WSRP2RewritingConstants.NAVIGATIONAL_VALUES);
if (!ParameterValidation.isNullOrEmpty(publicNS))
{
publicNavigationalState.putAll(WSRPUtils.decodePublicNS(publicNS));
}
NavigationalContext navigationalContext = WSRPTypeFactory.createNavigationalContextOrNull(navigationalState, publicNavigationalState);
getMarkupParams().setNavigationalContext(navigationalContext);
if (log.isDebugEnabled())
{
log.debug(WSRPUtils.toString(getMarkupParams()));
}
}
public String getPortletHandle()
{
return portletContext.getPortletHandle();
}
public PortletContext getPortletContext()
{
return portletContext;
}
public RegistrationContext getRegistrationContext()
{
return registrationContext;
}
public UserContext getUserContext()
{
return userContext;
}
public RuntimeContext getRuntimeContext()
{
return runtimeContext;
}
public MarkupParams getMarkupParams()
{
return markupParams;
}
}
}