HeaderMapper refactoring

INT-2083
added outbound namespace support for XMPP header mapper

INT-2083
added inbound namespace support for XMPP header mapper

INT-2083
polished XMPP inbound/outbound header mappings, added namespace support for AMQP inbound adapter/gateway header mappings

INT-2083
added support and tests for AMQP outbound gateways and adapters

INT-2083
polishing and adding more tests for AMQP support for header mappings

INT-2083
polishing AMQP and XMPP header mappings, generalized headerMapper configuration in IntegrationNamespaceUtils.configureHeaderMapper(..) method

INT-2803
added headermapping support to WS outbound gateways

INT-2803 polishing

INT-2083
refactored SimpleWebServiceOutboundGateway, added full request/reply test

INT-2083
polished MarshallingWebServiceOutboundGateway to add marshalling callback handlers, added test for marshalling call

INT-2083
polishing based on PR comments

INT-2083 interim commit

INT-2083
added RequestReplyHeaderMapper startegy and migrated AMQP Header Mapper to use it

INT-2083
migrated WS and XMPP to use a new RequestReplyHeaderMapper strategy

INT-2083 polishing with PR comments

INT-2083 polishing
removed introspection method, simplified things

INT-2083 polishing based on recent PR comments

INT-2083 interim commit

INT-2083 polishing WS module

INT-2083
refactored to make sure that Soap action header is set within HeaderMapper

INT-2083 polishing
ensured the Soap header is set to default value if not provided

INT-2083 polishing, putting tests back
This commit is contained in:
Mark Fisher
2011-10-06 10:24:13 -04:00
parent ff7bbfa404
commit adf93e5560
50 changed files with 2330 additions and 500 deletions

View File

@@ -26,7 +26,6 @@ import org.springframework.core.Conventions;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.util.xml.DomUtils;
import org.w3c.dom.Element;
/**
@@ -220,4 +219,37 @@ public abstract class IntegrationNamespaceUtils {
return innerComponentDefinition;
}
/**
* Utility method to configure HeaderMapper for Inbound and Outbound channel adapters/gateway
*/
public static void configureHeaderMapper(Element element, BeanDefinitionBuilder rootBuilder, ParserContext parserContext, Class<?> headerMapperClass, String replyHeaderValue){
String defaultMappedReplyHeadersAttributeName = "mapped-reply-headers";
if (!StringUtils.hasText(replyHeaderValue)){
replyHeaderValue = defaultMappedReplyHeadersAttributeName;
}
boolean hasHeaderMapper = element.hasAttribute("header-mapper");
boolean hasMappedRequestHeaders = element.hasAttribute("mapped-request-headers");
boolean hasMappedReplyHeaders = element.hasAttribute(replyHeaderValue);
if (hasHeaderMapper && (hasMappedRequestHeaders || hasMappedReplyHeaders)){
parserContext.getReaderContext().error("The 'header-mapper' attribute is mutually exclusive with" +
" 'mapped-request-headers' or 'mapped-reply-headers'. " +
"You can only use one or the others", element);
}
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(rootBuilder, element, "header-mapper");
if (hasMappedRequestHeaders || hasMappedReplyHeaders){
BeanDefinitionBuilder headerMapperBuilder = BeanDefinitionBuilder.genericBeanDefinition(headerMapperClass);
if (hasMappedRequestHeaders) {
headerMapperBuilder.addPropertyValue("requestHeaderNames", element.getAttribute("mapped-request-headers"));
}
if (hasMappedReplyHeaders) {
headerMapperBuilder.addPropertyValue("replyHeaderNames", element.getAttribute(replyHeaderValue));
}
rootBuilder.addPropertyValue("headerMapper", headerMapperBuilder.getBeanDefinition());
}
}
}

View File

@@ -0,0 +1,302 @@
/*
* Copyright 2002-2011 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.integration.mapping;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.integration.MessageHeaders;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.PatternMatchUtils;
import org.springframework.util.StringUtils;
/**
* Abstract base class for HeaderMapper implementations.
*
* @author Mark Fisher
* @author Oleg Zhurakousky
* @since 2.1
*/
public abstract class AbstractHeaderMapper<T> implements RequestReplyHeaderMapper<T> {
public static final String STANDARD_REQUEST_HEADER_NAME_PATTERN = "STANDARD_REQUEST_HEADERS";
public static final String STANDARD_REPLY_HEADER_NAME_PATTERN = "STANDARD_REPLY_HEADERS";
private static final String[] TRANSIENT_HEADER_NAMES = new String[] {
MessageHeaders.ID,
MessageHeaders.ERROR_CHANNEL,
MessageHeaders.REPLY_CHANNEL,
MessageHeaders.TIMESTAMP
};
protected final Log logger = LogFactory.getLog(this.getClass());
private final String standardHeaderPrefix;
private volatile String userDefinedHeaderPrefix = "";
private volatile List<String> requestHeaderNames = new ArrayList<String>();
private volatile List<String> replyHeaderNames = new ArrayList<String>();
protected AbstractHeaderMapper() {
this.standardHeaderPrefix = this.getStandardHeaderPrefix();
this.requestHeaderNames.addAll(this.getStandardRequestHeaderNames());
this.replyHeaderNames.addAll(this.getStandardReplyHeaderNames());
}
/**
* Provide the header names that should be mapped from a request (for inbound/outbound adapters)
* TO a Spring Integration Message's headers.
* The values can also contain simple wildcard patterns (e.g. "foo*" or "*foo") to be matched.
* <p>
* This will match the header name directly or, for non-standard headers, it will match
* the header name prefixed with the value specified by {@link #setInboundPrefix(String)}.
*/
public void setRequestHeaderNames(String[] requestHeaderNames) {
Assert.notNull(requestHeaderNames, "'requestHeaderNames' must not be null");
this.requestHeaderNames = Arrays.asList(requestHeaderNames);
}
/**
* Provide the header names that should be mapped to a response (for inbound/outbound adapters)
* FROM a Spring Integration Message's headers.
* The values can also contain simple wildcard patterns (e.g. "foo*" or "*foo") to be matched.
* <p>
* Any non-standard headers will be prefixed with the value specified by {@link #setOutboundPrefix(String)}.
*/
public void setReplyHeaderNames(String[] replyHeaderNames) {
Assert.notNull(replyHeaderNames, "'replyHeaderNames' must not be null");
this.replyHeaderNames = Arrays.asList(replyHeaderNames);
}
/**
* Specify a prefix to be prepended to the header name for any integration
* message header that is being mapped to or from a user-defined value.
* <p/>
* This does not affect the standard properties for the particular protocol, such as
* contentType for AMQP, etc. The header names used for mapping such properties are
* defined in a corresponding Headers class as constants (e.g. AmqpHeaders).
*/
public void setUserDefinedHeaderPrefix(String userDefinedHeaderPrefix) {
this.userDefinedHeaderPrefix = (userDefinedHeaderPrefix != null) ? userDefinedHeaderPrefix : "";
}
/**
* Maps headers from a Spring Integration MessageHeaders instance to the target instance
* matching on the set of REQUEST headers (if different).
*/
public void fromHeadersToRequest(MessageHeaders headers, T target) {
this.fromHeaders(headers, target, this.requestHeaderNames);
}
/**
* Maps headers from a Spring Integration MessageHeaders instance to the target instance
* matching on the set of REPLY headers (if different).
*/
public void fromHeadersToReply(MessageHeaders headers, T target) {
this.fromHeaders(headers, target, this.replyHeaderNames);
}
/**
* Maps headers/properties of the target object to Map of MessageHeaders
* matching on the set of REQUEST headers
*/
public Map<String, Object> toHeadersFromRequest(T source) {
return this.toHeaders(source, this.requestHeaderNames);
}
/**
* Maps headers/properties of the target object to Map of MessageHeaders
* matching on the set of REPLY headers
*/
public Map<String, Object> toHeadersFromReply(T source) {
return this.toHeaders(source, this.replyHeaderNames);
}
private void fromHeaders(MessageHeaders headers, T target, List<String> headerPatterns){
try {
Map<String, Object> subset = new HashMap<String, Object>();
for (String headerName : headers.keySet()) {
if (this.shouldMapHeader(headerName, headerPatterns)){
subset.put(headerName, headers.get(headerName));
}
}
this.populateStandardHeaders(subset, target);
this.populateUserDefinedHeaders(subset, target);
}
catch (Exception e) {
if (logger.isWarnEnabled()) {
logger.warn("error occurred while mapping from MessageHeaders", e);
}
}
}
private void populateUserDefinedHeaders(Map<String, Object> headers, T target) {
for (String headerName : headers.keySet()) {
Object value = headers.get(headerName);
if (value != null) {
try {
String key = this.addPrefixIfNecessary(this.userDefinedHeaderPrefix, headerName);
this.populateUserDefinedHeader(key, value, target);
}
catch (Exception e) {
if (logger.isWarnEnabled()) {
logger.warn("failed to map from Message header '" + headerName + "' to target", e);
}
}
}
}
}
/**
* Maps headers from a source instance to the MessageHeaders of a
* Spring Integration Message.
*/
private Map<String, Object> toHeaders(T source, List<String> headerPatterns) {
Map<String, Object> headers = new HashMap<String, Object>();
Map<String, Object> standardHeaders = this.extractStandardHeaders(source);
this.copyHeaders(this.standardHeaderPrefix, standardHeaders, headers, headerPatterns);
Map<String, Object> userDefinedHeaders = this.extractUserDefinedHeaders(source);
this.copyHeaders(this.userDefinedHeaderPrefix, userDefinedHeaders, headers, headerPatterns);
return headers;
}
private <V> void copyHeaders(String prefix, Map<String, Object> source, Map<String, Object> target, List<String> headerPatterns) {
if (!CollectionUtils.isEmpty(source)) {
for (Map.Entry<String, Object> entry : source.entrySet()) {
try {
String headerName = this.addPrefixIfNecessary(prefix, entry.getKey());
if (this.shouldMapHeader(headerName, headerPatterns)){
target.put(headerName, entry.getValue());
}
}
catch (Exception e) {
if (logger.isWarnEnabled()) {
logger.warn("error occurred while mapping header '"
+ entry.getKey() + "' to Message header", e);
}
}
}
}
}
private boolean shouldMapHeader(String headerName, List<String> patterns) {
if (!StringUtils.hasText(headerName)
|| ObjectUtils.containsElement(TRANSIENT_HEADER_NAMES, headerName)) {
return false;
}
if (patterns != null && patterns.size() > 0) {
for (String pattern : patterns) {
if (PatternMatchUtils.simpleMatch(pattern.toLowerCase(), headerName.toLowerCase())) {
if (logger.isDebugEnabled()) {
logger.debug(MessageFormat.format("headerName=[{0}] WILL be mapped, matched pattern={1}", headerName, pattern));
}
return true;
}
else if (STANDARD_REQUEST_HEADER_NAME_PATTERN.equals(pattern)
&& this.containsElementIgnoreCase(this.getStandardRequestHeaderNames(), headerName)) {
if (logger.isDebugEnabled()) {
logger.debug(MessageFormat.format("headerName=[{0}] WILL be mapped, matched pattern={1}", headerName, pattern));
}
return true;
}
else if (STANDARD_REPLY_HEADER_NAME_PATTERN.equals(pattern)
&& this.containsElementIgnoreCase(this.getStandardReplyHeaderNames(), headerName)) {
if (logger.isDebugEnabled()) {
logger.debug(MessageFormat.format("headerName=[{0}] WILL be mapped, matched pattern={1}", headerName, pattern));
}
return true;
}
}
}
if (logger.isDebugEnabled()) {
logger.debug(MessageFormat.format("headerName=[{0}] WILL NOT be mapped", headerName));
}
return false;
}
@SuppressWarnings("unchecked")
protected <V> V getHeaderIfAvailable(Map<String, Object> headers, String name, Class<V> type) {
Object value = headers.get(name);
if (value == null) {
return null;
}
if (!type.isAssignableFrom(value.getClass())) {
if (logger.isWarnEnabled()) {
logger.warn("skipping header '" + name + "' since it is not of expected type [" + type + "]");
}
}
return (V) value;
}
private boolean containsElementIgnoreCase(List<String> headerNames, String name) {
for (String headerName : headerNames) {
if (headerName.equalsIgnoreCase(name)){
return true;
}
}
return false;
}
/**
* Adds the prefix to the header name
*/
private String addPrefixIfNecessary(String prefix, String propertyName) {
String headerName = propertyName;
if (StringUtils.hasText(prefix) && !headerName.startsWith(prefix)) {
headerName = prefix + propertyName;
}
return headerName;
}
/**
* Returns the list of standard REQUEST headers. Implementation provided by a subclass
*/
protected List<String> getStandardReplyHeaderNames(){
return Collections.emptyList();
}
/**
* Returns the PREFIX used by standard headers (if any)
*/
protected List<String> getStandardRequestHeaderNames(){
return Collections.emptyList();
}
/**
* Returns the list of standard REPLY headers. Implementation provided by a subclass
*/
protected abstract String getStandardHeaderPrefix();
protected abstract Map<String, Object> extractStandardHeaders(T source);
protected abstract Map<String, Object> extractUserDefinedHeaders(T source);
protected abstract void populateStandardHeaders(Map<String, Object> headers, T target);
protected abstract void populateUserDefinedHeader(String headerName, Object headerValue, T target);
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2002-2011 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.integration.mapping;
import java.util.Map;
import org.springframework.integration.MessageHeaders;
/**
* Request/Reply strategy interface for mapping {@link MessageHeaders} to and from other
* types of objects. This would typically be used by adapters where the "other type"
* has a concept of headers or properties (HTTP, JMS, AMQP, etc).
*
* @author Oleg Zhurakousky
* @since 2.1
*
*/
public interface RequestReplyHeaderMapper<T> {
void fromHeadersToRequest(MessageHeaders headers, T target);
void fromHeadersToReply(MessageHeaders headers, T target);
Map<String, Object> toHeadersFromRequest(T source);
Map<String, Object> toHeadersFromReply(T source);
}