/*
* Copyright 2005-2014 the original author or authors.
*
* 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.springframework.ws.server;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.OrderComparator;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.ws.FaultAwareWebServiceMessage;
import org.springframework.ws.NoEndpointFoundException;
import org.springframework.ws.WebServiceMessage;
import org.springframework.ws.context.MessageContext;
import org.springframework.ws.server.endpoint.MessageEndpoint;
import org.springframework.ws.server.endpoint.PayloadEndpoint;
import org.springframework.ws.server.endpoint.adapter.MessageEndpointAdapter;
import org.springframework.ws.server.endpoint.adapter.PayloadEndpointAdapter;
import org.springframework.ws.soap.server.SoapMessageDispatcher;
import org.springframework.ws.support.DefaultStrategiesHelper;
import org.springframework.ws.transport.WebServiceMessageReceiver;
/**
* Central dispatcher for use within Spring-WS, dispatching Web service messages to registered endpoints.
*
* <p>This dispatcher is quite similar to Spring MVCs {@link DispatcherServlet}. Just like its counterpart, this dispatcher
* is very flexible. This class is SOAP agnostic; in typical SOAP Web Services, the {@link SoapMessageDispatcher}
* subclass is used.
* <ul>
* <li>It can use any {@link EndpointMapping} implementation - whether standard, or provided as
* part of an application - to control the routing of request messages to endpoint objects. Endpoint mappings can be
* registered using the {@link #setEndpointMappings(List) endpointMappings} property.</li>
* <li>It can use any {@link EndpointAdapter}; this allows one to use any endpoint interface or form. Defaults to
* the {@link MessageEndpointAdapter} and {@link PayloadEndpointAdapter}, for {@link MessageEndpoint} and
* {@link PayloadEndpoint}, respectively, and the
* {@link org.springframework.ws.server.endpoint.adapter.MessageMethodEndpointAdapter MessageMethodEndpointAdapter} and
* {@link org.springframework.ws.server.endpoint.adapter.PayloadMethodEndpointAdapter PayloadMethodEndpointAdapter}.
* Additional endpoint adapters can be added through the {@link #setEndpointAdapters(List) endpointAdapters} property.</li>
* <li>Its exception resolution strategy can be specified via a
* {@link EndpointExceptionResolver}, for example mapping certain exceptions to SOAP Faults. Default is none. Additional
* exception resolvers can be added through the {@link #setEndpointExceptionResolvers(List) endpointExceptionResolvers}
* property.</li>
* </ul>
*
* @author Arjen Poutsma
* @see EndpointMapping
* @see EndpointAdapter
* @see EndpointExceptionResolver
* @see org.springframework.web.servlet.DispatcherServlet
* @since 1.0.0
*/
public class MessageDispatcher implements WebServiceMessageReceiver, BeanNameAware, ApplicationContextAware {
/** Logger available to subclasses. */
protected final Log logger = LogFactory.getLog(getClass());
/** Log category to use when no mapped endpoint is found for a request. */
public static final String ENDPOINT_NOT_FOUND_LOG_CATEGORY = "org.springframework.ws.server.EndpointNotFound";
/** Additional logger to use when no mapped endpoint is found for a request. */
protected static final Log endpointNotFoundLogger =
LogFactory.getLog(MessageDispatcher.ENDPOINT_NOT_FOUND_LOG_CATEGORY);
/** Log category to use for message tracing. */
public static final String MESSAGE_TRACING_LOG_CATEGORY = "org.springframework.ws.server.MessageTracing";
/** Additional logger to use for sent message tracing. */
protected static final Log sentMessageTracingLogger =
LogFactory.getLog(MessageDispatcher.MESSAGE_TRACING_LOG_CATEGORY + ".sent");
/** Additional logger to use for received message tracing. */
protected static final Log receivedMessageTracingLogger =
LogFactory.getLog(MessageDispatcher.MESSAGE_TRACING_LOG_CATEGORY + ".received");
private final DefaultStrategiesHelper defaultStrategiesHelper;
/** The registered bean name for this dispatcher. */
private String beanName;
/** List of EndpointAdapters used in this dispatcher. */
private List<EndpointAdapter> endpointAdapters;
/** List of EndpointExceptionResolvers used in this dispatcher. */
private List<EndpointExceptionResolver> endpointExceptionResolvers;
/** List of EndpointMappings used in this dispatcher. */
private List<EndpointMapping> endpointMappings;
/** Initializes a new instance of the {@code MessageDispatcher}. */
public MessageDispatcher() {
defaultStrategiesHelper = new DefaultStrategiesHelper(getClass());
}
/** Returns the {@code EndpointAdapter}s to use by this {@code MessageDispatcher}. */
public List<EndpointAdapter> getEndpointAdapters() {
return endpointAdapters;
}
/** Sets the {@code EndpointAdapter}s to use by this {@code MessageDispatcher}. */
public void setEndpointAdapters(List<EndpointAdapter> endpointAdapters) {
this.endpointAdapters = endpointAdapters;
}
/** Returns the {@code EndpointExceptionResolver}s to use by this {@code MessageDispatcher}. */
public List<EndpointExceptionResolver> getEndpointExceptionResolvers() {
return endpointExceptionResolvers;
}
/** Sets the {@code EndpointExceptionResolver}s to use by this {@code MessageDispatcher}. */
public void setEndpointExceptionResolvers(List<EndpointExceptionResolver> endpointExceptionResolvers) {
this.endpointExceptionResolvers = endpointExceptionResolvers;
}
/** Returns the {@code EndpointMapping}s to use by this {@code MessageDispatcher}. */
public List<EndpointMapping> getEndpointMappings() {
return endpointMappings;
}
/** Sets the {@code EndpointMapping}s to use by this {@code MessageDispatcher}. */
public void setEndpointMappings(List<EndpointMapping> endpointMappings) {
this.endpointMappings = endpointMappings;
}
@Override
public final void setBeanName(String beanName) {
this.beanName = beanName;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
initEndpointAdapters(applicationContext);
initEndpointExceptionResolvers(applicationContext);
initEndpointMappings(applicationContext);
}
@Override
public void receive(MessageContext messageContext) throws Exception {
// Let's keep a reference to the request content as it came in, it might be changed by interceptors in dispatch()
String requestContent = "";
if (receivedMessageTracingLogger.isTraceEnabled() || sentMessageTracingLogger.isTraceEnabled()) {
requestContent = getMessageContent(messageContext.getRequest());
}
if (receivedMessageTracingLogger.isTraceEnabled()) {
receivedMessageTracingLogger.trace("Received request [" + requestContent + "]");
}
else if (receivedMessageTracingLogger.isDebugEnabled()) {
receivedMessageTracingLogger.debug("Received request [" + messageContext.getRequest() + "]");
}
dispatch(messageContext);
if (messageContext.hasResponse()) {
WebServiceMessage response = messageContext.getResponse();
if (sentMessageTracingLogger.isTraceEnabled()) {
String responseContent = getMessageContent(response);
sentMessageTracingLogger.trace("Sent response [" + responseContent + "] for request [" +
requestContent + "]");
}
else if (sentMessageTracingLogger.isDebugEnabled()) {
sentMessageTracingLogger.debug("Sent response [" + response + "] for request [" +
messageContext.getRequest() + "]");
}
}
else if (sentMessageTracingLogger.isDebugEnabled()) {
sentMessageTracingLogger
.debug("MessageDispatcher with name '" + beanName + "' sends no response for request [" +
messageContext.getRequest() + "]");
}
}
private String getMessageContent(WebServiceMessage message) throws IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
message.writeTo(bos);
return bos.toString("UTF-8");
}
/**
* Dispatches the request in the given MessageContext according to the configuration.
*
* @param messageContext the message context
* @throws org.springframework.ws.NoEndpointFoundException
* thrown when an endpoint cannot be resolved for the incoming message
*/
protected final void dispatch(MessageContext messageContext) throws Exception {
EndpointInvocationChain mappedEndpoint = null;
int interceptorIndex = -1;
try {
try {
// Determine endpoint for the current context
mappedEndpoint = getEndpoint(messageContext);
if (mappedEndpoint == null || mappedEndpoint.getEndpoint() == null) {
throw new NoEndpointFoundException(messageContext.getRequest());
}
if (!handleRequest(mappedEndpoint, messageContext)) {
return;
}
// Apply handleRequest of registered interceptors
if (mappedEndpoint.getInterceptors() != null) {
for (int i = 0; i < mappedEndpoint.getInterceptors().length; i++) {
EndpointInterceptor interceptor = mappedEndpoint.getInterceptors()[i];
interceptorIndex = i;
if (!interceptor.handleRequest(messageContext, mappedEndpoint.getEndpoint())) {
triggerHandleResponse(mappedEndpoint, interceptorIndex, messageContext);
triggerAfterCompletion(mappedEndpoint, interceptorIndex, messageContext, null);
return;
}
}
}
// Actually invoke the endpoint
EndpointAdapter endpointAdapter = getEndpointAdapter(mappedEndpoint.getEndpoint());
endpointAdapter.invoke(messageContext, mappedEndpoint.getEndpoint());
// Apply handleResponse methods of registered interceptors
triggerHandleResponse(mappedEndpoint, interceptorIndex, messageContext);
}
catch (NoEndpointFoundException ex) {
// No triggering of interceptors if no endpoint is found
if (endpointNotFoundLogger.isWarnEnabled()) {
endpointNotFoundLogger.warn("No endpoint mapping found for [" + messageContext.getRequest() + "]");
}
throw ex;
}
catch (Exception ex) {
Object endpoint = mappedEndpoint != null ? mappedEndpoint.getEndpoint() : null;
processEndpointException(messageContext, endpoint, ex);
triggerHandleResponse(mappedEndpoint, interceptorIndex, messageContext);
}
triggerAfterCompletion(mappedEndpoint, interceptorIndex, messageContext, null);
}
catch (NoEndpointFoundException ex) {
throw ex;
}
catch (Exception ex) {
// Trigger after-completion for thrown exception.
triggerAfterCompletion(mappedEndpoint, interceptorIndex, messageContext, ex);
throw ex;
}
}
/**
* Returns the endpoint for this request. All endpoint mappings are tried, in order.
*
* @return the {@code EndpointInvocationChain}, or {@code null} if no endpoint could be found.
*/
protected EndpointInvocationChain getEndpoint(MessageContext messageContext) throws Exception {
for (EndpointMapping endpointMapping : getEndpointMappings()) {
EndpointInvocationChain endpoint = endpointMapping.getEndpoint(messageContext);
if (endpoint != null) {
if (logger.isDebugEnabled()) {
logger.debug("Endpoint mapping [" + endpointMapping + "] maps request to endpoint [" +
endpoint.getEndpoint() + "]");
}
return endpoint;
}
else if (logger.isDebugEnabled()) {
logger.debug("Endpoint mapping [" + endpointMapping + "] has no mapping for request");
}
}
return null;
}
/**
* Returns the {@code EndpointAdapter} for the given endpoint.
*
* @param endpoint the endpoint to find an adapter for
* @return the adapter
*/
protected EndpointAdapter getEndpointAdapter(Object endpoint) {
for (EndpointAdapter endpointAdapter : getEndpointAdapters()) {
if (logger.isDebugEnabled()) {
logger.debug("Testing endpoint adapter [" + endpointAdapter + "]");
}
if (endpointAdapter.supports(endpoint)) {
return endpointAdapter;
}
}
throw new IllegalStateException("No adapter for endpoint [" + endpoint + "]: Is your endpoint annotated with " +
"@Endpoint, or does it implement a supported interface like MessageHandler or PayloadEndpoint?");
}
/**
* Callback for pre-processing of given invocation chain and message context. Gets called before invocation of
* {@code handleRequest} on the interceptors.
*
* <p>Default implementation does nothing, and returns {@code true}.
*
* @param mappedEndpoint the mapped {@code EndpointInvocationChain}
* @param messageContext the message context
* @return {@code true} if processing should continue; {@code false} otherwise
*/
protected boolean handleRequest(EndpointInvocationChain mappedEndpoint, MessageContext messageContext) {
return true;
}
/**
* Determine an error {@code SOAPMessage} response via the registered {@code EndpointExceptionResolvers}.
* Most likely, the response contains a {@code SOAPFault}. If no suitable resolver was found, the exception is
* rethrown.
*
* @param messageContext current SOAPMessage request
* @param endpoint the executed endpoint, or null if none chosen at the time of the exception
* @param ex the exception that got thrown during handler execution
* @throws Exception if no suitable resolver is found
*/
protected void processEndpointException(MessageContext messageContext, Object endpoint, Exception ex)
throws Exception {
if (!CollectionUtils.isEmpty(getEndpointExceptionResolvers())) {
for (EndpointExceptionResolver resolver : getEndpointExceptionResolvers()) {
if (resolver.resolveException(messageContext, endpoint, ex)) {
if (logger.isDebugEnabled()) {
logger.debug("Endpoint invocation resulted in exception - responding with Fault", ex);
}
return;
}
}
}
// exception not resolved
throw ex;
}
/**
* Trigger handleResponse or handleFault on the mapped EndpointInterceptors. Will just invoke said method on all
* interceptors whose handleRequest invocation returned {@code true}, in addition to the last interceptor who
* returned {@code false}.
*
* @param mappedEndpoint the mapped EndpointInvocationChain
* @param interceptorIndex index of last interceptor that was called
* @param messageContext the message context, whose request and response are filled
* @see EndpointInterceptor#handleResponse(MessageContext,Object)
* @see EndpointInterceptor#handleFault(MessageContext, Object)
*/
private void triggerHandleResponse(EndpointInvocationChain mappedEndpoint,
int interceptorIndex,
MessageContext messageContext) throws Exception {
if (mappedEndpoint != null && messageContext.hasResponse() &&
!ObjectUtils.isEmpty(mappedEndpoint.getInterceptors())) {
boolean hasFault = false;
WebServiceMessage response = messageContext.getResponse();
if (response instanceof FaultAwareWebServiceMessage) {
hasFault = ((FaultAwareWebServiceMessage) response).hasFault();
}
boolean resume = true;
for (int i = interceptorIndex; resume && i >= 0; i--) {
EndpointInterceptor interceptor = mappedEndpoint.getInterceptors()[i];
if (!hasFault) {
resume = interceptor.handleResponse(messageContext, mappedEndpoint.getEndpoint());
}
else {
resume = interceptor.handleFault(messageContext, mappedEndpoint.getEndpoint());
}
}
}
}
/**
* Trigger afterCompletion callbacks on the mapped EndpointInterceptors.
* Will just invoke afterCompletion for all interceptors whose handleRequest invocation
* has successfully completed and returned true, in addition to the last interceptor who
* returned {@code false}.
*
* @param mappedEndpoint the mapped EndpointInvocationChain
* @param interceptorIndex index of last interceptor that successfully completed
* @param ex Exception thrown on handler execution, or {@code null} if none
* @see EndpointInterceptor#afterCompletion
*/
private void triggerAfterCompletion(EndpointInvocationChain mappedEndpoint,
int interceptorIndex,
MessageContext messageContext,
Exception ex) throws Exception {
// Apply afterCompletion methods of registered interceptors.
if (mappedEndpoint != null) {
EndpointInterceptor[] interceptors = mappedEndpoint.getInterceptors();
if (interceptors != null) {
for (int i = interceptorIndex; i >= 0; i--) {
EndpointInterceptor interceptor = interceptors[i];
try {
interceptor.afterCompletion(messageContext, mappedEndpoint.getEndpoint(), ex);
}
catch (Throwable ex2) {
logger.error("EndpointInterceptor.afterCompletion threw exception", ex2);
}
}
}
}
}
/**
* Initialize the {@code EndpointAdapters} used by this class. If no adapter beans are explicitly set by using
* the {@code endpointAdapters} property, we use the default strategies.
*
* @see #setEndpointAdapters(java.util.List)
*/
private void initEndpointAdapters(ApplicationContext applicationContext) throws BeansException {
if (endpointAdapters == null) {
Map<String, EndpointAdapter> matchingBeans = BeanFactoryUtils
.beansOfTypeIncludingAncestors(applicationContext, EndpointAdapter.class, true, false);
if (!matchingBeans.isEmpty()) {
endpointAdapters = new ArrayList<EndpointAdapter>(matchingBeans.values());
Collections.sort(endpointAdapters, new OrderComparator());
}
else {
endpointAdapters =
defaultStrategiesHelper.getDefaultStrategies(EndpointAdapter.class, applicationContext);
if (logger.isDebugEnabled()) {
logger.debug("No EndpointAdapters found, using defaults");
}
}
}
}
/**
* Initialize the {@code EndpointExceptionResolver} used by this class. If no resolver beans are explicitly set
* by using the {@code endpointExceptionResolvers} property, we use the default strategies.
*
* @see #setEndpointExceptionResolvers(java.util.List)
*/
private void initEndpointExceptionResolvers(ApplicationContext applicationContext) throws BeansException {
if (endpointExceptionResolvers == null) {
Map<String, EndpointExceptionResolver> matchingBeans = BeanFactoryUtils
.beansOfTypeIncludingAncestors(applicationContext, EndpointExceptionResolver.class, true, false);
if (!matchingBeans.isEmpty()) {
endpointExceptionResolvers = new ArrayList<EndpointExceptionResolver>(matchingBeans.values());
Collections.sort(endpointExceptionResolvers, new OrderComparator());
}
else {
endpointExceptionResolvers = defaultStrategiesHelper
.getDefaultStrategies(EndpointExceptionResolver.class, applicationContext);
if (logger.isDebugEnabled()) {
logger.debug("No EndpointExceptionResolvers found, using defaults");
}
}
}
}
/**
* Initialize the {@code EndpointMappings} used by this class. If no mapping beans are explictely set by using
* the {@code endpointMappings} property, we use the default strategies.
*
* @see #setEndpointMappings(java.util.List)
*/
private void initEndpointMappings(ApplicationContext applicationContext) throws BeansException {
if (endpointMappings == null) {
Map<String, EndpointMapping> matchingBeans = BeanFactoryUtils
.beansOfTypeIncludingAncestors(applicationContext, EndpointMapping.class, true, false);
if (!matchingBeans.isEmpty()) {
endpointMappings = new ArrayList<EndpointMapping>(matchingBeans.values());
Collections.sort(endpointMappings, new OrderComparator());
}
else {
endpointMappings =
defaultStrategiesHelper.getDefaultStrategies(EndpointMapping.class, applicationContext);
if (logger.isDebugEnabled()) {
logger.debug("No EndpointMappings found, using defaults");
}
}
}
}
}