diff --git a/spring-integration-amqp/src/main/java/org/springframework/integration/amqp/support/DefaultAmqpHeaderMapper.java b/spring-integration-amqp/src/main/java/org/springframework/integration/amqp/support/DefaultAmqpHeaderMapper.java index f8119f59dd..8f36d38e55 100644 --- a/spring-integration-amqp/src/main/java/org/springframework/integration/amqp/support/DefaultAmqpHeaderMapper.java +++ b/spring-integration-amqp/src/main/java/org/springframework/integration/amqp/support/DefaultAmqpHeaderMapper.java @@ -48,6 +48,7 @@ import org.springframework.util.StringUtils; * @author Oleg Zhurakousky * @author Gary Russell * @author Artem Bilan + * @author Stephane Nicoll * @since 2.1 */ public class DefaultAmqpHeaderMapper extends AbstractHeaderMapper implements AmqpHeaderMapper { @@ -80,6 +81,10 @@ public class DefaultAmqpHeaderMapper extends AbstractHeaderMapper getStandardRequestHeaderNames() { - return STANDARD_HEADER_NAMES; - } - - - @Override - protected List getStandardReplyHeaderNames() { - return STANDARD_HEADER_NAMES; - } - - - @Override - protected String getStandardHeaderPrefix() { - return AmqpHeaders.PREFIX; - } - /** * Will extract Content-Type from MessageHeaders and convert it to String if possible * Required since Content-Type can be represented as org.springframework.http.MediaType diff --git a/spring-integration-amqp/src/main/resources/org/springframework/integration/amqp/config/spring-integration-amqp-4.1.xsd b/spring-integration-amqp/src/main/resources/org/springframework/integration/amqp/config/spring-integration-amqp-4.1.xsd index d1205b3823..d259a4a55a 100644 --- a/spring-integration-amqp/src/main/resources/org/springframework/integration/amqp/config/spring-integration-amqp-4.1.xsd +++ b/spring-integration-amqp/src/main/resources/org/springframework/integration/amqp/config/spring-integration-amqp-4.1.xsd @@ -195,7 +195,7 @@ this list can also be simple patterns to be matched against the header names (e.g. "foo*" or "*foo"). A special token 'STANDARD_REPLY_HEADERS' represents all the standard AMQP headers (replyTo, correlationId etc); it is included by default. If you wish to add your own headers, you must also include this token if you wish the - standard headers to also be mapped. + standard headers to also be mapped. To map all non-standard headers the 'NON_STANDARD_HEADERS' token can be used. ]]> @@ -257,7 +257,7 @@ this list can also be simple patterns to be matched against the header names (e.g. "foo*" or "*foo"). A special token 'STANDARD_REPLY_HEADERS' represents all the standard AMQP headers (replyTo, correlationId etc); it is included by default. If you wish to add your own headers, you must also include this token if you wish the - standard headers to also be mapped. + standard headers to also be mapped. To map all non-standard headers the 'NON_STANDARD_HEADERS' token can be used. ]]> @@ -457,7 +457,7 @@ This can only be provided if the 'header-mapper' reference is not being set dire this list can also be simple patterns to be matched against the header names (e.g. "foo*" or "*foo"). A special token 'STANDARD_REQUEST_HEADERS' represents all the standard AMQP headers (replyTo, correlationId etc); it is included by default. If you wish to add your own headers, you must also include this token if you wish the -standard headers to also be mapped. +standard headers to also be mapped. To map all non-standard headers the 'NON_STANDARD_HEADERS' token can be used. ]]> @@ -559,7 +559,7 @@ This can only be provided if the 'header-mapper' reference is not being set dire this list can also be simple patterns to be matched against the header names (e.g. "foo*" or "*foo"). A special token 'STANDARD_REQUEST_HEADERS' represents all the standard AMQP headers (replyTo, correlationId etc); it is included by default. If you wish to add your own headers, you must also include this token if you wish the -standard headers to also be mapped. +standard headers to also be mapped. To map all non-standard headers the 'NON_STANDARD_HEADERS' token can be used. ]]> diff --git a/spring-integration-amqp/src/test/java/org/springframework/integration/amqp/support/DefaultAmqpHeaderMapperTests.java b/spring-integration-amqp/src/test/java/org/springframework/integration/amqp/support/DefaultAmqpHeaderMapperTests.java index de29700bb7..f283744d5d 100644 --- a/spring-integration-amqp/src/test/java/org/springframework/integration/amqp/support/DefaultAmqpHeaderMapperTests.java +++ b/spring-integration-amqp/src/test/java/org/springframework/integration/amqp/support/DefaultAmqpHeaderMapperTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-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. @@ -17,7 +17,9 @@ package org.springframework.integration.amqp.support; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; import java.util.Date; import java.util.HashMap; @@ -30,6 +32,7 @@ import org.springframework.amqp.core.MessageDeliveryMode; import org.springframework.amqp.core.MessageProperties; import org.springframework.amqp.support.converter.JsonMessageConverter; import org.springframework.http.MediaType; +import org.springframework.messaging.MessageChannel; import org.springframework.messaging.MessageHeaders; import org.springframework.integration.amqp.AmqpHeaders; @@ -37,6 +40,7 @@ import org.springframework.integration.amqp.AmqpHeaders; * @author Mark Fisher * @author Gary Russell * @author Oleg Zhurakousky + * @author Stephane Nicoll * @since 2.1 */ public class DefaultAmqpHeaderMapperTests { @@ -67,6 +71,10 @@ public class DefaultAmqpHeaderMapperTests { headerMap.put(AmqpHeaders.USER_ID, "test.userId"); headerMap.put(AmqpHeaders.SPRING_REPLY_CORRELATION, "test.correlation"); headerMap.put(AmqpHeaders.SPRING_REPLY_TO_STACK, "test.replyTo2"); + + headerMap.put(MessageHeaders.ERROR_CHANNEL, mock(MessageChannel.class)); + headerMap.put(MessageHeaders.REPLY_CHANNEL, mock(MessageChannel.class)); + MessageHeaders integrationHeaders = new MessageHeaders(headerMap); MessageProperties amqpProperties = new MessageProperties(); headerMapper.fromHeadersToRequest(integrationHeaders, amqpProperties); @@ -95,6 +103,9 @@ public class DefaultAmqpHeaderMapperTests { assertEquals("test.userId", amqpProperties.getUserId()); assertEquals("test.correlation", amqpProperties.getHeaders().get(AmqpHeaders.STACKED_CORRELATION_HEADER)); assertEquals("test.replyTo2", amqpProperties.getHeaders().get(AmqpHeaders.STACKED_REPLY_TO_HEADER)); + + assertNull(amqpProperties.getHeaders().get(MessageHeaders.ERROR_CHANNEL)); + assertNull(amqpProperties.getHeaders().get(MessageHeaders.REPLY_CHANNEL)); } @Test @@ -163,25 +174,25 @@ public class DefaultAmqpHeaderMapperTests { } @Test - public void replyChannelNotMappedToAmqpProperties() { + public void messageIdNotMappedToAmqpProperties() { DefaultAmqpHeaderMapper headerMapper = new DefaultAmqpHeaderMapper(); Map headerMap = new HashMap(); - headerMap.put(MessageHeaders.REPLY_CHANNEL, "foo"); + headerMap.put(MessageHeaders.ID, "msg-id"); MessageHeaders integrationHeaders = new MessageHeaders(headerMap); MessageProperties amqpProperties = new MessageProperties(); headerMapper.fromHeadersToRequest(integrationHeaders, amqpProperties); - assertEquals(null, amqpProperties.getHeaders().get(MessageHeaders.REPLY_CHANNEL)); + assertNull(amqpProperties.getHeaders().get(MessageHeaders.ID)); } @Test - public void errorChannelNotMappedToAmqpProperties() { + public void messageTimestampNotMappedToAmqpProperties() { DefaultAmqpHeaderMapper headerMapper = new DefaultAmqpHeaderMapper(); Map headerMap = new HashMap(); - headerMap.put(MessageHeaders.ERROR_CHANNEL, "foo"); + headerMap.put(MessageHeaders.TIMESTAMP, 1234L); MessageHeaders integrationHeaders = new MessageHeaders(headerMap); MessageProperties amqpProperties = new MessageProperties(); headerMapper.fromHeadersToRequest(integrationHeaders, amqpProperties); - assertEquals(null, amqpProperties.getHeaders().get(MessageHeaders.ERROR_CHANNEL)); + assertNull(amqpProperties.getHeaders().get(MessageHeaders.TIMESTAMP)); } @Test // INT-2090 diff --git a/spring-integration-core/src/main/java/org/springframework/integration/mapping/AbstractHeaderMapper.java b/spring-integration-core/src/main/java/org/springframework/integration/mapping/AbstractHeaderMapper.java index b97b4f03d3..1578603f25 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/mapping/AbstractHeaderMapper.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/mapping/AbstractHeaderMapper.java @@ -19,152 +19,168 @@ package org.springframework.integration.mapping; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; 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.mapping.support.JsonHeaders; import org.springframework.messaging.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. + * Abstract base class for {@link RequestReplyHeaderMapper} implementations. * * @author Mark Fisher * @author Oleg Zhurakousky + * @author Stephane Nicoll * @since 2.1 */ public abstract class AbstractHeaderMapper implements RequestReplyHeaderMapper { + /** + * A special pattern that only matches standard request headers. + */ public static final String STANDARD_REQUEST_HEADER_NAME_PATTERN = "STANDARD_REQUEST_HEADERS"; + /** + * A special pattern that only matches standard reply 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 - }; + /** + * A special pattern that matches any header that is not a standard header (i.e. any + * header that does not start with the configured standard header prefix) + */ + public static final String NON_STANDARD_HEADER_NAME_PATTERN = "NON_STANDARD_HEADERS"; - protected final Log logger = LogFactory.getLog(this.getClass()); + private static final Collection TRANSIENT_HEADER_NAMES = Arrays.asList( + MessageHeaders.ID, MessageHeaders.TIMESTAMP); + + protected final Log logger = LogFactory.getLog(getClass()); private final String standardHeaderPrefix; - private volatile String userDefinedHeaderPrefix = ""; + private final Collection requestHeaderNames; - private volatile List requestHeaderNames = new ArrayList(); + private final Collection replyHeaderNames; - private volatile List replyHeaderNames = new ArrayList(); + private volatile HeaderMatcher requestHeaderMatcher; - protected AbstractHeaderMapper() { - this.standardHeaderPrefix = this.getStandardHeaderPrefix(); - this.requestHeaderNames.addAll(this.getStandardRequestHeaderNames()); - this.replyHeaderNames.addAll(this.getStandardReplyHeaderNames()); + private volatile HeaderMatcher replyHeaderMatcher; + + /** + * Create a new instance. + * @param standardHeaderPrefix the header prefix that identifies standard header. Such prefix helps to + * differentiate user-defined headers from standard headers. If set, user-defined headers are also + * mapped by default + * @param requestHeaderNames the header names that should be mapped from a request to {@link MessageHeaders} + * @param replyHeaderNames the header names that should be mapped to a response from {@link MessageHeaders} + */ + protected AbstractHeaderMapper(String standardHeaderPrefix, + Collection requestHeaderNames, Collection replyHeaderNames) { + + this.standardHeaderPrefix = standardHeaderPrefix; + this.requestHeaderNames = requestHeaderNames; + this.replyHeaderNames = replyHeaderNames; + this.requestHeaderMatcher = createDefaultHeaderMatcher(this.standardHeaderPrefix, this.requestHeaderNames); + this.replyHeaderMatcher = createDefaultHeaderMatcher(this.standardHeaderPrefix, this.replyHeaderNames); } /** - * 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. - *

- * This will match the header name directly or, for non-standard headers, it will match - * the header name prefixed with the value, if specified, by {@link #setUserDefinedHeaderPrefix(String)}. - * + * Provide the header names that should be mapped from a request + * to a {@link MessageHeaders}. + *

The values can also contain simple wildcard patterns (e.g. "foo*" or "*foo") to be matched. * @param requestHeaderNames The request header names. */ - public void setRequestHeaderNames(String[] requestHeaderNames) { + public void setRequestHeaderNames(String... requestHeaderNames) { Assert.notNull(requestHeaderNames, "'requestHeaderNames' must not be null"); - this.requestHeaderNames = Arrays.asList(requestHeaderNames); + this.requestHeaderMatcher = createHeaderMatcher(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. - *

- * Any non-standard headers will be prefixed with the value specified by {@link #setUserDefinedHeaderPrefix(String)}. + * Provide the header names that should be mapped to a response + * from a {@link MessageHeaders}. + *

The values can also contain simple wildcard patterns (e.g. "foo*" or "*foo") to be matched. * * @param replyHeaderNames The reply header names. */ - public void setReplyHeaderNames(String[] replyHeaderNames) { + public void setReplyHeaderNames(String... replyHeaderNames) { Assert.notNull(replyHeaderNames, "'replyHeaderNames' must not be null"); - this.replyHeaderNames = Arrays.asList(replyHeaderNames); + this.replyHeaderMatcher = createHeaderMatcher(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. - *

- * 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). - * - * @param userDefinedHeaderPrefix The user defined header prefix. + * Create the initial {@link HeaderMatcher} based on the specified headers and + * standard header prefix. + * @param standardHeaderPrefix the prefix for standard headers. + * @param headerNames the collection of header names to map. + * @return the deafault {@link HeaderMatcher} instance. */ - public void setUserDefinedHeaderPrefix(String userDefinedHeaderPrefix) { - this.userDefinedHeaderPrefix = (userDefinedHeaderPrefix != null) ? userDefinedHeaderPrefix : ""; + protected HeaderMatcher createDefaultHeaderMatcher(String standardHeaderPrefix, Collection headerNames) { + return new ContentBasedHeaderMatcher(true, headerNames); } /** - * Maps headers from a Spring Integration MessageHeaders instance to the target instance - * matching on the set of REQUEST headers (if different). - * - * @param headers The headers. - * @param target The target. + * Create a {@link HeaderMatcher} that match if any of the specified {@code patterns} + * match. The pattern can be a header name, a wildcard pattern such as + * {@code foo*}, {@code *foo}, or {@code within*foo}. + *

Special patterns are also recognized: {@link #STANDARD_REQUEST_HEADER_NAME_PATTERN}, + * {@link #STANDARD_REQUEST_HEADER_NAME_PATTERN} and {@link #NON_STANDARD_HEADER_NAME_PATTERN}. + * @param patterns the patterns to apply + * @return a header mapper that match if any of the specified patters match */ + protected HeaderMatcher createHeaderMatcher(Collection patterns) { + Collection matchers = new ArrayList(); + for (String pattern : patterns) { + if (STANDARD_REQUEST_HEADER_NAME_PATTERN.equals(pattern)) { + matchers.add(new ContentBasedHeaderMatcher(true, this.requestHeaderNames)); + } + else if (STANDARD_REPLY_HEADER_NAME_PATTERN.equals(pattern)) { + matchers.add(new ContentBasedHeaderMatcher(true, this.replyHeaderNames)); + } + else if (NON_STANDARD_HEADER_NAME_PATTERN.equals(pattern)) { + matchers.add(new PrefixBasedMatcher(false, this.standardHeaderPrefix)); + } + else { + matchers.add(new PatternBasedHeaderMatcher(Collections.singleton(pattern))); + } + } + return new CompositeHeaderMatcher(matchers); + } + @Override 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). - * - * @param headers The headers. - * @param target The target. - */ - @Override - 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 - * - * @param source The source. - * @return The headers. - */ - @Override - public Map 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 - * - * @param source The source. - * @return The headers. - */ - @Override - public Map toHeadersFromReply(T source) { - return this.toHeaders(source, this.replyHeaderNames); + this.fromHeaders(headers, target, this.requestHeaderMatcher); } - private void fromHeaders(MessageHeaders headers, T target, List headerPatterns){ + @Override + public void fromHeadersToReply(MessageHeaders headers, T target) { + this.fromHeaders(headers, target, this.replyHeaderMatcher); + } + + @Override + public Map toHeadersFromRequest(T source) { + return this.toHeaders(source, this.requestHeaderMatcher); + } + + @Override + public Map toHeadersFromReply(T source) { + return this.toHeaders(source, this.replyHeaderMatcher); + } + + private void fromHeaders(MessageHeaders headers, T target, HeaderMatcher headerMatcher) { try { Map subset = new HashMap(); - for (String headerName : headers.keySet()) { - if (this.shouldMapHeader(headerName, headerPatterns)){ - subset.put(headerName, headers.get(headerName)); + for (Map.Entry entry : headers.entrySet()) { + String headerName = entry.getKey(); + if (this.shouldMapHeader(headerName, headerMatcher)) { + subset.put(headerName, entry.getValue()); } } this.populateStandardHeaders(subset, target); @@ -182,8 +198,8 @@ public abstract class AbstractHeaderMapper implements RequestReplyHeaderMappe Object value = headers.get(headerName); if (value != null) { try { - if (!headerName.startsWith(this.standardHeaderPrefix)){ - String key = this.addPrefixIfNecessary(this.userDefinedHeaderPrefix, headerName); + if (!headerName.startsWith(this.standardHeaderPrefix)) { + String key = this.createTargetPropertyName(headerName, true); this.populateUserDefinedHeader(key, value, target); } } @@ -197,24 +213,24 @@ public abstract class AbstractHeaderMapper implements RequestReplyHeaderMappe } /** - * Maps headers from a source instance to the MessageHeaders of a - * Spring Integration Message. + * Map headers from a source instance to the {@link MessageHeaders} of + * a {@link org.springframework.messaging.Message}. */ - private Map toHeaders(T source, List headerPatterns) { + private Map toHeaders(T source, HeaderMatcher headerMatcher) { Map headers = new HashMap(); - Map standardHeaders = this.extractStandardHeaders(source); - this.copyHeaders(this.standardHeaderPrefix, standardHeaders, headers, headerPatterns); - Map userDefinedHeaders = this.extractUserDefinedHeaders(source); - this.copyHeaders(this.userDefinedHeaderPrefix, userDefinedHeaders, headers, headerPatterns); + Map standardHeaders = extractStandardHeaders(source); + this.copyHeaders(standardHeaders, headers, headerMatcher); + Map userDefinedHeaders = extractUserDefinedHeaders(source); + this.copyHeaders(userDefinedHeaders, headers, headerMatcher); return headers; } - private void copyHeaders(String prefix, Map source, Map target, List headerPatterns) { + private void copyHeaders(Map source, Map target, HeaderMatcher headerMatcher) { if (!CollectionUtils.isEmpty(source)) { for (Map.Entry entry : source.entrySet()) { try { - String headerName = this.addPrefixIfNecessary(prefix, entry.getKey()); - if (this.shouldMapHeader(headerName, headerPatterns)){ + String headerName = this.createTargetPropertyName(entry.getKey(), false); + if (this.shouldMapHeader(headerName, headerMatcher)) { target.put(headerName, entry.getValue()); } } @@ -228,39 +244,12 @@ public abstract class AbstractHeaderMapper implements RequestReplyHeaderMappe } } - private boolean shouldMapHeader(String headerName, List patterns) { + private boolean shouldMapHeader(String headerName, HeaderMatcher headerMatcher) { if (!StringUtils.hasText(headerName) - || ObjectUtils.containsElement(TRANSIENT_HEADER_NAMES, headerName)) { + || getTransientHeaderNames().contains(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; + return headerMatcher.matchHeader(headerName); } @SuppressWarnings("unchecked") @@ -281,56 +270,216 @@ public abstract class AbstractHeaderMapper implements RequestReplyHeaderMappe } } - private boolean containsElementIgnoreCase(List headerNames, String name) { - for (String headerName : headerNames) { - if (headerName.equalsIgnoreCase(name)){ - return true; - } - } - return false; + /** + * Alter the specified {@code propertyName} if necessary. By default, the original + * {@code propertyName} is returned. + * @param propertyName the original name of the property. + * @param fromMessageHeaders specify if the property originates from a {@link MessageHeaders} + * instance (true) or from the type managed by this mapper (false). + * @return the property name for mapping. + */ + protected String createTargetPropertyName(String propertyName, boolean fromMessageHeaders) { + return propertyName; } /** - * Adds the prefix to the header name + * Return the transient header names. Transient headers are never mapped. + * @return the names of headers to be skipped from mapping. */ - private String addPrefixIfNecessary(String prefix, String propertyName) { - String headerName = propertyName; - if (StringUtils.hasText(prefix) && !headerName.startsWith(prefix) && - !headerName.equals(MessageHeaders.CONTENT_TYPE) && - (!JsonHeaders.HEADERS.contains(headerName) || !JsonHeaders.HEADERS.contains(JsonHeaders.PREFIX + headerName))) { - headerName = prefix + propertyName; - } - if (JsonHeaders.HEADERS.contains(JsonHeaders.PREFIX + headerName)) { - headerName = JsonHeaders.PREFIX + headerName; - } - return headerName; + protected Collection getTransientHeaderNames() { + return TRANSIENT_HEADER_NAMES; } /** - * @return The list of standard REQUEST headers. Implementation provided by a subclass + * Extract the standard headers from the specified source. + * @param source the source object to extract standard headers. + * @return the map of headers to be mapped. */ - protected List getStandardReplyHeaderNames(){ - return Collections.emptyList(); - } - - /** - * @return The PREFIX used by standard headers (if any) - */ - protected List getStandardRequestHeaderNames(){ - return Collections.emptyList(); - } - - /** - * @return The list of standard REPLY headers. Implementation provided by a subclass - */ - protected abstract String getStandardHeaderPrefix(); - protected abstract Map extractStandardHeaders(T source); + /** + * Extract the user-defined headers from the specified source. + * @param source the source object to extract user defined headers. + * @return the map of headers to be mapped. + */ protected abstract Map extractUserDefinedHeaders(T source); + /** + * Populate the specified standard headers to the specified source. + * @param headers the map of standard headers to be populated. + * @param target the target object to populate headers. + */ protected abstract void populateStandardHeaders(Map headers, T target); + /** + * Populate the specified user-defined headers to the specified source. + * @param headerName the user defined header name to be populated. + * @param headerValue the user defined header value to be populated. + * @param target the target object to populate headers. + */ protected abstract void populateUserDefinedHeader(String headerName, Object headerValue, T target); + /** + * Strategy interface to determine if a given header name matches. + * @since 4.1 + */ + public interface HeaderMatcher { + + /** + * Specify if the given {@code headerName} matches. + * @param headerName the header name to be matched. + * @return {@code true} if {@code headerName} matches to this {@link HeaderMatcher}. + */ + boolean matchHeader(String headerName); + + } + + /** + * A content-based {@link HeaderMatcher} that matches if the specified + * header is contained within a list of candidates. The case of the + * header does not matter. + * @since 4.1 + */ + protected static class ContentBasedHeaderMatcher implements HeaderMatcher { + + private static final Log logger = LogFactory.getLog(HeaderMatcher.class); + + private final boolean match; + + private final Collection content; + + public ContentBasedHeaderMatcher(boolean match, Collection content) { + this.match = match; + Assert.notNull(content, "Content must not be null"); + this.content = content; + } + + @Override + public boolean matchHeader(String headerName) { + boolean result = (this.match == containsIgnoreCase(headerName)); + if (result && logger.isDebugEnabled()) { + StringBuilder message = new StringBuilder("headerName=[{0}] WILL be mapped, "); + if (!this.match) { + message.append("not "); + } + message.append("found in {1}"); + logger.debug(MessageFormat.format(message.toString(), headerName, this.content)); + } + return result; + } + + private boolean containsIgnoreCase(String name) { + for (String headerName : this.content) { + if (headerName.equalsIgnoreCase(name)) { + return true; + } + } + return false; + } + + } + + /** + * A pattern-based {@link HeaderMatcher} that matches if the specified + * header match one of the specified simple patterns. + * @see org.springframework.util.PatternMatchUtils#simpleMatch(String, String) + * @since 4.1 + */ + protected static class PatternBasedHeaderMatcher implements HeaderMatcher { + + private static final Log logger = LogFactory.getLog(HeaderMatcher.class); + + private final Collection patterns; + + public PatternBasedHeaderMatcher(Collection patterns) { + Assert.notNull(patterns, "Patters must no be null"); + Assert.notEmpty(patterns, "At least one pattern must be specified"); + this.patterns = patterns; + } + + @Override + public boolean matchHeader(String headerName) { + String header = headerName.toLowerCase(); + for (String pattern : this.patterns) { + if (PatternMatchUtils.simpleMatch(pattern.toLowerCase(), header)) { + if (logger.isDebugEnabled()) { + logger.debug(MessageFormat.format( + "headerName=[{0}] WILL be mapped, matched pattern={1}", headerName, pattern)); + } + return true; + } + } + return false; + } + + } + + /** + * A prefix-based {@link HeaderMatcher} that matches if the specified + * header starts with a configurable prefix. + * @since 4.1 + */ + protected static class PrefixBasedMatcher implements HeaderMatcher { + + private static final Log logger = LogFactory.getLog(HeaderMatcher.class); + + private final boolean match; + + private final String prefix; + + public PrefixBasedMatcher(boolean match, String prefix) { + this.match = match; + this.prefix = prefix; + } + + @Override + public boolean matchHeader(String headerName) { + boolean result = (this.match == headerName.startsWith(this.prefix)); + if (result && logger.isDebugEnabled()) { + StringBuilder message = new StringBuilder("headerName=[{0}] WILL be mapped, "); + if (!this.match) { + message.append("does not "); + } + message.append("start with [{1}]"); + logger.debug(MessageFormat.format(message.toString(), headerName, this.prefix)); + } + return result; + } + + } + + /** + * A composite {@link HeaderMatcher} that matches if one of provided + * {@link HeaderMatcher}s matches to the {@code headerName}. + * @since 4.1 + */ + protected static class CompositeHeaderMatcher implements HeaderMatcher { + + private static final Log logger = LogFactory.getLog(HeaderMatcher.class); + + private final Collection strategies; + + CompositeHeaderMatcher(Collection strategies) { + this.strategies = strategies; + } + + CompositeHeaderMatcher(HeaderMatcher... strategies) { + this(Arrays.asList(strategies)); + } + + @Override + public boolean matchHeader(String headerName) { + for (HeaderMatcher strategy : this.strategies) { + if (strategy.matchHeader(headerName)) { + return true; + } + } + if (logger.isDebugEnabled()) { + logger.debug(MessageFormat.format("headerName=[{0}] WILL NOT be mapped", headerName)); + } + return false; + } + + } + } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/mapping/RequestReplyHeaderMapper.java b/spring-integration-core/src/main/java/org/springframework/integration/mapping/RequestReplyHeaderMapper.java index 0da2c0e60e..0f34c8bdf4 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/mapping/RequestReplyHeaderMapper.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/mapping/RequestReplyHeaderMapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 the original author or authors. + * Copyright 2002-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. @@ -23,19 +23,40 @@ import org.springframework.messaging.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 * + * @param the type of the target object holding the headers + * @author Oleg Zhurakousky + * @author Stephane Nicoll + * @since 2.1 */ public interface RequestReplyHeaderMapper { + /** + * Map from the given {@link MessageHeaders} to the specified request target. + * @param headers the abstracted MessageHeaders + * @param target the native target request + */ void fromHeadersToRequest(MessageHeaders headers, T target); + /** + * Map from the given {@link MessageHeaders} to the specified reply target. + * @param headers the abstracted MessageHeaders + * @param target the native target reply + */ void fromHeadersToReply(MessageHeaders headers, T target); + /** + * Map from the given request object to abstracted {@link MessageHeaders}. + * @param source the native target request + * @return the abstracted MessageHeaders + */ Map toHeadersFromRequest(T source); + /** + * Map from the given reply object to abstracted {@link MessageHeaders}. + * @param source the native target reply + * @return the abstracted MessageHeaders + */ Map toHeadersFromReply(T source); } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/mapping/HeaderMapperTests.java b/spring-integration-core/src/test/java/org/springframework/integration/mapping/HeaderMapperTests.java new file mode 100644 index 0000000000..117e5b5897 --- /dev/null +++ b/spring-integration-core/src/test/java/org/springframework/integration/mapping/HeaderMapperTests.java @@ -0,0 +1,535 @@ +/* + * Copyright 2002-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.integration.mapping; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.springframework.integration.mapping.AbstractHeaderMapper.CompositeHeaderMatcher; +import static org.springframework.integration.mapping.AbstractHeaderMapper.ContentBasedHeaderMatcher; +import static org.springframework.integration.mapping.AbstractHeaderMapper.HeaderMatcher; +import static org.springframework.integration.mapping.AbstractHeaderMapper.PatternBasedHeaderMatcher; +import static org.springframework.integration.mapping.AbstractHeaderMapper.PrefixBasedMatcher; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Test; + +import org.springframework.messaging.MessageHeaders; +import org.springframework.util.StringUtils; + +/** + * @author Stephane Nicoll + * @since 4.1 + */ +public class HeaderMapperTests { + + private final GenericTestHeaderMapper mapper = new GenericTestHeaderMapper(); + + @Test + public void toHeadersFromRequest() { + GenericTestProperties properties = createSimpleGenericTestProperties(); + + Map attributes = this.mapper.toHeadersFromRequest(properties); + assertEquals("appId", attributes.get(GenericTestHeaders.APP_ID)); + assertEquals("request-123", attributes.get(GenericTestHeaders.REQUEST_ONLY)); + assertFalse(attributes.containsKey(GenericTestHeaders.REPLY_ONLY)); + assertEquals("Wrong number of mapped header(s)", 2, attributes.size()); + } + + @Test + public void toHeadersFromRequestWithStar() { + this.mapper.setRequestHeaderNames("*"); + GenericTestProperties properties = createSimpleGenericTestProperties(); + + Map attributes = this.mapper.toHeadersFromRequest(properties); + assertEquals("appId", attributes.get(GenericTestHeaders.APP_ID)); + assertEquals("request-123", attributes.get(GenericTestHeaders.REQUEST_ONLY)); + assertEquals("reply-123", attributes.get(GenericTestHeaders.REPLY_ONLY)); + assertEquals("bar", attributes.get("foo")); + assertEquals("Wrong number of mapped header(s)", 4, attributes.size()); + } + + @Test + public void toHeadersFromRequestWithCustomPatterns() { + this.mapper.setRequestHeaderNames("foo*", "generic_reply*"); + GenericTestProperties properties = createSimpleGenericTestProperties(); + + Map attributes = this.mapper.toHeadersFromRequest(properties); + assertEquals(null, attributes.get(GenericTestHeaders.APP_ID)); + assertEquals(null, attributes.get(GenericTestHeaders.REQUEST_ONLY)); + assertEquals("reply-123", attributes.get(GenericTestHeaders.REPLY_ONLY)); + assertEquals("bar", attributes.get("foo")); + assertEquals("Wrong number of mapped header(s)", 2, attributes.size()); + } + + @Test + public void toHeadersFromRequestWithStandardRequestPattern() { + this.mapper.setRequestHeaderNames("foo*", GenericTestHeaderMapper.STANDARD_REQUEST_HEADER_NAME_PATTERN); + GenericTestProperties properties = createSimpleGenericTestProperties(); + properties.setUserDefinedHeader("foo2", "bar"); + properties.setUserDefinedHeader("something-else", "bar"); + + Map attributes = this.mapper.toHeadersFromRequest(properties); + assertEquals("appId", attributes.get(GenericTestHeaders.APP_ID)); + assertEquals("request-123", attributes.get(GenericTestHeaders.REQUEST_ONLY)); + assertFalse(attributes.containsKey(GenericTestHeaders.REPLY_ONLY)); + assertEquals("bar", attributes.get("foo")); + assertEquals("bar", attributes.get("foo2")); + assertEquals("Wrong number of mapped header(s)", 4, attributes.size()); + } + + @Test + public void toHeadersFromRequestWithOnlyStandardHeaders() { + this.mapper.setRequestHeaderNames(GenericTestHeaderMapper.STANDARD_REQUEST_HEADER_NAME_PATTERN); + GenericTestProperties properties = createSimpleGenericTestProperties(); + properties.setUserDefinedHeader("foo2", "bar"); + properties.setUserDefinedHeader("something-else", "bar"); + + Map attributes = this.mapper.toHeadersFromRequest(properties); + assertEquals("appId", attributes.get(GenericTestHeaders.APP_ID)); + assertEquals("request-123", attributes.get(GenericTestHeaders.REQUEST_ONLY)); + assertFalse(attributes.containsKey(GenericTestHeaders.REPLY_ONLY)); + assertEquals("Wrong number of mapped header(s)", 2, attributes.size()); + } + + @Test + public void toHeadersFromReply() { + GenericTestProperties properties = createSimpleGenericTestProperties(); + + Map attributes = this.mapper.toHeadersFromReply(properties); + assertEquals("appId", attributes.get(GenericTestHeaders.APP_ID)); + assertFalse(attributes.containsKey(GenericTestHeaders.REQUEST_ONLY)); + assertEquals("reply-123", attributes.get(GenericTestHeaders.REPLY_ONLY)); + assertEquals("Wrong number of mapped header(s)", 2, attributes.size()); + } + + @Test + public void toHeadersFromReplyWithStar() { + this.mapper.setReplyHeaderNames("*"); + GenericTestProperties properties = createSimpleGenericTestProperties(); + + Map attributes = this.mapper.toHeadersFromReply(properties); + assertEquals("appId", attributes.get(GenericTestHeaders.APP_ID)); + assertEquals("request-123", attributes.get(GenericTestHeaders.REQUEST_ONLY)); + assertEquals("reply-123", attributes.get(GenericTestHeaders.REPLY_ONLY)); + assertEquals("bar", attributes.get("foo")); + assertEquals("Wrong number of mapped header(s)", 4, attributes.size()); + } + + @Test + public void toHeadersFromReplyWithStandardReplyPattern() { + this.mapper.setReplyHeaderNames("foo*", GenericTestHeaderMapper.STANDARD_REPLY_HEADER_NAME_PATTERN); + GenericTestProperties properties = createSimpleGenericTestProperties(); + + properties.setUserDefinedHeader("foo2", "bar"); + properties.setUserDefinedHeader("something-else", "bar"); + + Map attributes = this.mapper.toHeadersFromReply(properties); + assertEquals("appId", attributes.get(GenericTestHeaders.APP_ID)); + assertFalse(attributes.containsKey(GenericTestHeaders.REQUEST_ONLY)); + assertEquals("reply-123", attributes.get(GenericTestHeaders.REPLY_ONLY)); + assertEquals("bar", attributes.get("foo")); + assertEquals("bar", attributes.get("foo2")); + assertEquals("Wrong number of mapped header(s)", 4, attributes.size()); + } + + @Test + public void toHeadersFromReplyWithOnlyStandardReplyHeaders() { + this.mapper.setReplyHeaderNames(GenericTestHeaderMapper.STANDARD_REPLY_HEADER_NAME_PATTERN); + GenericTestProperties properties = createSimpleGenericTestProperties(); + + properties.setUserDefinedHeader("foo2", "bar"); + properties.setUserDefinedHeader("something-else", "bar"); + + Map attributes = this.mapper.toHeadersFromReply(properties); + assertEquals("appId", attributes.get(GenericTestHeaders.APP_ID)); + assertFalse(attributes.containsKey(GenericTestHeaders.REQUEST_ONLY)); + assertEquals("reply-123", attributes.get(GenericTestHeaders.REPLY_ONLY)); + assertEquals("Wrong number of mapped header(s)", 2, attributes.size()); + } + + @Test + public void customTransientHeaderNames() { + GenericTestHeaderMapper customMapper = new GenericTestHeaderMapper() { + @Override + protected Collection getTransientHeaderNames() { + return Arrays.asList("foo", GenericTestHeaders.APP_ID); + } + }; + GenericTestProperties properties = createSimpleGenericTestProperties(); + + Map attributes = customMapper.toHeadersFromReply(properties); + // foo custom header and app Id not mapped + assertFalse(attributes.containsKey(GenericTestHeaders.APP_ID)); + assertFalse(attributes.containsKey("foo")); + assertEquals("Wrong number of mapped header(s)", 1, attributes.size()); + } + + private GenericTestProperties createSimpleGenericTestProperties() { + GenericTestProperties properties = new GenericTestProperties(); + properties.setAppId("appId"); + properties.setRequestOnly("request-123"); + properties.setReplyOnly("reply-123"); + + properties.setUserDefinedHeader("foo", "bar"); + return properties; + } + + @Test + public void fromHeadersToRequest() { + MessageHeaders messageHeaders = createSimpleMessageHeaders(); + GenericTestProperties properties = new GenericTestProperties(); + this.mapper.fromHeadersToRequest(messageHeaders, properties); + assertEquals("myAppId", properties.getAppId()); + assertNull(properties.getTransactionSize()); + assertEquals(true, properties.getRedelivered()); + assertEquals("request-456", properties.getRequestOnly()); + assertNull(properties.getReplyOnly()); + assertEquals(0, properties.getUserDefinedHeaders().size()); + } + + @Test + public void fromHeadersToRequestWithStar() { + this.mapper.setRequestHeaderNames("*"); + MessageHeaders messageHeaders = createSimpleMessageHeaders(); + GenericTestProperties properties = new GenericTestProperties(); + this.mapper.fromHeadersToRequest(messageHeaders, properties); + assertEquals("myAppId", properties.getAppId()); + assertNull(properties.getTransactionSize()); + assertEquals(true, properties.getRedelivered()); + assertEquals("request-456", properties.getRequestOnly()); + assertEquals("reply-456", properties.getReplyOnly()); + assertEquals("bar", properties.getUserDefinedHeaders().get("foo")); + assertEquals(1, properties.getUserDefinedHeaders().size()); + } + + @Test + public void fromHeadersToRequestWithStandardRequestPattern() { + this.mapper.setRequestHeaderNames("foo", GenericTestHeaderMapper.STANDARD_REQUEST_HEADER_NAME_PATTERN); + MessageHeaders messageHeaders = createSimpleMessageHeaders(); + GenericTestProperties properties = new GenericTestProperties(); + this.mapper.fromHeadersToRequest(messageHeaders, properties); + assertEquals("myAppId", properties.getAppId()); + assertNull(properties.getTransactionSize()); + assertEquals(true, properties.getRedelivered()); + assertEquals("request-456", properties.getRequestOnly()); + assertNull(properties.getReplyOnly()); + assertEquals("bar", properties.getUserDefinedHeaders().get("foo")); + assertEquals(1, properties.getUserDefinedHeaders().size()); + } + + @Test + public void fromHeadersToReply() { + MessageHeaders messageHeaders = createSimpleMessageHeaders(); + GenericTestProperties properties = new GenericTestProperties(); + this.mapper.fromHeadersToReply(messageHeaders, properties); + assertEquals("myAppId", properties.getAppId()); + assertNull(properties.getTransactionSize()); + assertEquals(true, properties.getRedelivered()); + assertNull(properties.getRequestOnly()); + assertEquals("reply-456", properties.getReplyOnly()); + assertEquals(0, properties.getUserDefinedHeaders().size()); + } + + @Test + public void fromHeadersToReplyWithStar() { + this.mapper.setReplyHeaderNames("*"); + MessageHeaders messageHeaders = createSimpleMessageHeaders(); + GenericTestProperties properties = new GenericTestProperties(); + this.mapper.fromHeadersToReply(messageHeaders, properties); + assertEquals("myAppId", properties.getAppId()); + assertNull(properties.getTransactionSize()); + assertEquals(true, properties.getRedelivered()); + assertEquals("request-456", properties.getRequestOnly()); + assertEquals("reply-456", properties.getReplyOnly()); + assertEquals("bar", properties.getUserDefinedHeaders().get("foo")); + assertEquals(1, properties.getUserDefinedHeaders().size()); + } + + + @Test + public void fromHeadersToReplyWithStandardReplyPattern() { + this.mapper.setReplyHeaderNames("foo", GenericTestHeaderMapper.STANDARD_REPLY_HEADER_NAME_PATTERN); + MessageHeaders messageHeaders = createSimpleMessageHeaders(); + GenericTestProperties properties = new GenericTestProperties(); + this.mapper.fromHeadersToReply(messageHeaders, properties); + assertEquals("myAppId", properties.getAppId()); + assertNull(properties.getTransactionSize()); + assertEquals(true, properties.getRedelivered()); + assertNull(properties.getRequestOnly()); + assertEquals("reply-456", properties.getReplyOnly()); + assertEquals("bar", properties.getUserDefinedHeaders().get("foo")); + assertEquals(1, properties.getUserDefinedHeaders().size()); + } + + public MessageHeaders createSimpleMessageHeaders() { + Map headers = new HashMap(); + headers.put(GenericTestHeaders.APP_ID, "myAppId"); + headers.put(GenericTestHeaders.REDELIVERED, true); + headers.put(GenericTestHeaders.REQUEST_ONLY, "request-456"); + headers.put(GenericTestHeaders.REPLY_ONLY, "reply-456"); + + headers.put("foo", "bar"); + + return new MessageHeaders(headers); + } + + + @Test + public void prefixHeaderPatternMatching() { + PatternBasedHeaderMatcher strategy = + new PatternBasedHeaderMatcher(Collections.singleton("foo*")); + + assertMapping(strategy, "foo", true); + assertMapping(strategy, "foo123", true); + assertMapping(strategy, "FoO", true); + + assertMapping(strategy, "123foo", false); + assertMapping(strategy, "_foo", false); + } + + @Test + public void suffixHeaderPatternMatching() { + PatternBasedHeaderMatcher strategy = + new PatternBasedHeaderMatcher(Collections.singleton("*foo")); + + assertMapping(strategy, "foo", true); + assertMapping(strategy, "123foo", true); + assertMapping(strategy, "FoO", true); + + assertMapping(strategy, "foo123", false); + assertMapping(strategy, "foo_", false); + } + + @Test + public void contentHeaderMatching() { + AbstractHeaderMapper.ContentBasedHeaderMatcher strategy = + new ContentBasedHeaderMatcher(true, Arrays.asList("foo", "bar")); + + assertMapping(strategy, "foo", true); + assertMapping(strategy, "bar", true); + assertMapping(strategy, "FOO", true); + assertMapping(strategy, "somethingElse", false); + } + + @Test + public void contentHeaderReverseMatching() { + ContentBasedHeaderMatcher strategy = + new ContentBasedHeaderMatcher(false, Arrays.asList("foo", "bar")); + + assertMapping(strategy, "foo", false); + assertMapping(strategy, "bar", false); + assertMapping(strategy, "somethingElse", true); + assertMapping(strategy, "anything", true); + } + + @Test + public void prefixHeaderMatching() { + PrefixBasedMatcher strategy = new PrefixBasedMatcher(true, "foo_"); + + assertMapping(strategy, "foo_", true); + assertMapping(strategy, "foo_ANYTHING", true); + assertMapping(strategy, "something_foo_", false); + assertMapping(strategy, "somethingElse", false); + } + + @Test + public void prefixHeaderReverseMatching() { + PrefixBasedMatcher strategy = new PrefixBasedMatcher(false, "foo_"); + + assertMapping(strategy, "foo_", false); + assertMapping(strategy, "foo_ANYTHING", false); + assertMapping(strategy, "something_foo_", true); + assertMapping(strategy, "somethingElse", true); + } + + @Test + public void compositeOneMatch() { + HeaderMatcher strategy = new CompositeHeaderMatcher( + new PrefixBasedMatcher(true, "foo_"), + new PrefixBasedMatcher(true, "bar_")); + + assertMapping(strategy, "foo_ANYTHING", true); + assertMapping(strategy, "bar_ANYTHING", true); + assertMapping(strategy, "somethingElse", false); + } + + + protected void assertMapping(HeaderMatcher strategy, String candidate, boolean match) { + assertEquals("Wrong mapping result for " + candidate + "", match, strategy.matchHeader(candidate)); + } + + + private static abstract class GenericTestHeaders { + + public static final String PREFIX = "generic_"; + + public static final String APP_ID = PREFIX + "appId"; + + public static final String TRANSACTION_SIZE = PREFIX + "transactionSize"; + + public static final String REDELIVERED = PREFIX + "redelivered"; + + public static final String REQUEST_ONLY = PREFIX + "requestOnly"; + + public static final String REPLY_ONLY = PREFIX + "replyOnly"; + + } + + private static class GenericTestHeaderMapper extends AbstractHeaderMapper { + + + private GenericTestHeaderMapper() { + super(GenericTestHeaders.PREFIX, + Arrays.asList(GenericTestHeaders.APP_ID, GenericTestHeaders.TRANSACTION_SIZE, + GenericTestHeaders.REDELIVERED, GenericTestHeaders.REQUEST_ONLY), + Arrays.asList(GenericTestHeaders.APP_ID, GenericTestHeaders.TRANSACTION_SIZE, + GenericTestHeaders.REDELIVERED, GenericTestHeaders.REPLY_ONLY)); + } + + @Override + protected Map extractStandardHeaders(GenericTestProperties source) { + Map result = new HashMap(); + if (StringUtils.hasText(source.getAppId())) { + result.put(GenericTestHeaders.APP_ID, source.getAppId()); + } + if (source.getTransactionSize() != null) { + result.put(GenericTestHeaders.TRANSACTION_SIZE, source.getTransactionSize()); + } + if (source.getRedelivered() != null) { + result.put(GenericTestHeaders.REDELIVERED, source.getRedelivered()); + } + if (StringUtils.hasText(source.getRequestOnly())) { + result.put(GenericTestHeaders.REQUEST_ONLY, source.getRequestOnly()); + } + if (StringUtils.hasText(source.getReplyOnly())) { + result.put(GenericTestHeaders.REPLY_ONLY, source.getReplyOnly()); + } + return result; + + } + + @Override + protected Map extractUserDefinedHeaders(GenericTestProperties source) { + return source.getUserDefinedHeaders(); + } + + @Override + protected void populateStandardHeaders(Map headers, GenericTestProperties target) { + String appId = getHeaderIfAvailable(headers, GenericTestHeaders.APP_ID, String.class); + if (StringUtils.hasText(appId)) { + target.setAppId(appId); + } + Integer transactionSize = getHeaderIfAvailable(headers, GenericTestHeaders.TRANSACTION_SIZE, Integer.class); + if (transactionSize != null) { + target.setTransactionSize(transactionSize); + } + Boolean redelivered = getHeaderIfAvailable(headers, GenericTestHeaders.REDELIVERED, Boolean.class); + if (redelivered != null) { + target.setRedelivered(redelivered); + } + String requestOnly = getHeaderIfAvailable(headers, GenericTestHeaders.REQUEST_ONLY, String.class); + if (StringUtils.hasText(requestOnly)) { + target.setRequestOnly(requestOnly); + } + String replyOnly = getHeaderIfAvailable(headers, GenericTestHeaders.REPLY_ONLY, String.class); + if (StringUtils.hasText(replyOnly)) { + target.setReplyOnly(replyOnly); + } + + } + + @Override + protected void populateUserDefinedHeader(String headerName, Object headerValue, GenericTestProperties target) { + target.setUserDefinedHeader(headerName, headerValue); + } + + } + + private static class GenericTestProperties { + + private String appId; + + private Integer transactionSize; + + private Boolean redelivered; + + private String requestOnly; + + private String replyOnly; + + private final Map userDefinedHeaders = new HashMap(); + + private GenericTestProperties() { + } + + public String getAppId() { + return appId; + } + + public void setAppId(String appId) { + this.appId = appId; + } + + public Integer getTransactionSize() { + return transactionSize; + } + + public void setTransactionSize(Integer transactionSize) { + this.transactionSize = transactionSize; + } + + public Boolean getRedelivered() { + return redelivered; + } + + public void setRedelivered(boolean redelivered) { + this.redelivered = redelivered; + } + + public String getRequestOnly() { + return requestOnly; + } + + public void setRequestOnly(String requestOnly) { + this.requestOnly = requestOnly; + } + + public String getReplyOnly() { + return replyOnly; + } + + public void setReplyOnly(String replyOnly) { + this.replyOnly = replyOnly; + } + + public Map getUserDefinedHeaders() { + return userDefinedHeaders; + } + + public void setUserDefinedHeader(String name, Object value) { + this.userDefinedHeaders.put(name, value); + } + + } + +} diff --git a/spring-integration-file/src/main/java/org/springframework/integration/file/remote/RemoteFileOperations.java b/spring-integration-file/src/main/java/org/springframework/integration/file/remote/RemoteFileOperations.java index fa03331c9f..3ab8c07141 100644 --- a/spring-integration-file/src/main/java/org/springframework/integration/file/remote/RemoteFileOperations.java +++ b/spring-integration-file/src/main/java/org/springframework/integration/file/remote/RemoteFileOperations.java @@ -134,6 +134,7 @@ public interface RemoteFileOperations { * * @param callback the ClientCallback. * @param The type returned by {@link ClientCallback#doWithClient(Object)}. + * @param The type of the underlying client object. * @return The result of the callback method. * @since 4.1 */ diff --git a/spring-integration-ws/src/main/java/org/springframework/integration/ws/DefaultSoapHeaderMapper.java b/spring-integration-ws/src/main/java/org/springframework/integration/ws/DefaultSoapHeaderMapper.java index 934935ece1..c7e3af944b 100644 --- a/spring-integration-ws/src/main/java/org/springframework/integration/ws/DefaultSoapHeaderMapper.java +++ b/spring-integration-ws/src/main/java/org/springframework/integration/ws/DefaultSoapHeaderMapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 the original author or authors. + * Copyright 2002-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. @@ -44,6 +44,7 @@ import org.springframework.xml.namespace.QNameUtils; * * @author Mark Fisher * @author Oleg Zhurakousky + * @author Stephane Nicoll * @since 2.0 */ public class DefaultSoapHeaderMapper extends AbstractHeaderMapper implements SoapHeaderMapper { @@ -52,9 +53,12 @@ public class DefaultSoapHeaderMapper extends AbstractHeaderMapper i static { STANDARD_HEADER_NAMES.add(WebServiceHeaders.SOAP_ACTION); - } - + + public DefaultSoapHeaderMapper() { + super(WebServiceHeaders.PREFIX, STANDARD_HEADER_NAMES, Collections.emptyList()); + } + @Override protected Map extractStandardHeaders(SoapMessage source) { return Collections.emptyMap(); @@ -106,14 +110,5 @@ public class DefaultSoapHeaderMapper extends AbstractHeaderMapper i soapHeader.addAttribute(qname, (String) headerValue); } } - - @Override - protected List getStandardRequestHeaderNames() { - return STANDARD_HEADER_NAMES; - } - @Override - protected String getStandardHeaderPrefix() { - return WebServiceHeaders.PREFIX; - } } diff --git a/spring-integration-ws/src/test/java/org/springframework/integration/ws/config/WebServiceInboundGatewayParserTests.java b/spring-integration-ws/src/test/java/org/springframework/integration/ws/config/WebServiceInboundGatewayParserTests.java index 53562fe826..52f75f598b 100644 --- a/spring-integration-ws/src/test/java/org/springframework/integration/ws/config/WebServiceInboundGatewayParserTests.java +++ b/spring-integration-ws/src/test/java/org/springframework/integration/ws/config/WebServiceInboundGatewayParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-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. @@ -17,7 +17,6 @@ package org.springframework.integration.ws.config; import java.util.Collections; -import java.util.List; import java.util.Map; import java.util.Properties; @@ -30,6 +29,7 @@ import org.mockito.Mockito; import org.springframework.beans.DirectFieldAccessor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.integration.mapping.AbstractHeaderMapper; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.MessageHeaders; @@ -52,10 +52,9 @@ import org.springframework.ws.soap.SoapMessage; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; -import static org.hamcrest.CoreMatchers.is; - +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThat; - +import static org.hamcrest.CoreMatchers.is; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -64,6 +63,7 @@ import static org.mockito.Mockito.when; * @author Oleg Zhurakousky * @author Mark Fisher * @author Gunnar Hillert + * @author Stephane Nicoll */ @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration @@ -144,14 +144,16 @@ public class WebServiceInboundGatewayParserTests { assertThat( (MessageChannel) accessor.getPropertyValue("errorChannel"), is(customErrorChannel)); - @SuppressWarnings("unchecked") - List requestHeaders = TestUtils.getPropertyValue(marshallingGateway, "headerMapper.requestHeaderNames", List.class); - @SuppressWarnings("unchecked") - List replyHeaders = TestUtils.getPropertyValue(marshallingGateway, "headerMapper.replyHeaderNames", List.class); - assertEquals(1, requestHeaders.size()); - assertEquals(1, replyHeaders.size()); - assertTrue(requestHeaders.contains("testRequest")); - assertTrue(replyHeaders.contains("testReply")); + + AbstractHeaderMapper.HeaderMatcher requestHeaderMatcher = TestUtils.getPropertyValue(marshallingGateway, + "headerMapper.requestHeaderMatcher", AbstractHeaderMapper.HeaderMatcher.class); + assertTrue(requestHeaderMatcher.matchHeader("testRequest")); + assertFalse(requestHeaderMatcher.matchHeader("testReply")); + + AbstractHeaderMapper.HeaderMatcher replyHeaderMatcher = TestUtils.getPropertyValue(marshallingGateway, + "headerMapper.replyHeaderMatcher", AbstractHeaderMapper.HeaderMatcher.class); + assertFalse(replyHeaderMatcher.matchHeader("testRequest")); + assertTrue(replyHeaderMatcher.matchHeader("testReply")); } @Test diff --git a/spring-integration-ws/src/test/java/org/springframework/integration/ws/config/WebServiceOutboundGatewayParserTests.java b/spring-integration-ws/src/test/java/org/springframework/integration/ws/config/WebServiceOutboundGatewayParserTests.java index 883e1e9476..5edf01d9d9 100644 --- a/spring-integration-ws/src/test/java/org/springframework/integration/ws/config/WebServiceOutboundGatewayParserTests.java +++ b/spring-integration-ws/src/test/java/org/springframework/integration/ws/config/WebServiceOutboundGatewayParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-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. @@ -19,7 +19,6 @@ package org.springframework.integration.ws.config; import static org.junit.Assert.*; import static org.mockito.Mockito.*; -import java.util.List; import org.junit.Assert; import org.junit.Test; @@ -32,6 +31,7 @@ import org.springframework.integration.endpoint.AbstractEndpoint; import org.springframework.integration.endpoint.EventDrivenConsumer; import org.springframework.integration.endpoint.PollingConsumer; import org.springframework.integration.handler.advice.AbstractRequestHandlerAdvice; +import org.springframework.integration.mapping.AbstractHeaderMapper; import org.springframework.integration.test.util.TestUtils; import org.springframework.integration.ws.MarshallingWebServiceOutboundGateway; import org.springframework.integration.ws.SimpleWebServiceOutboundGateway; @@ -75,14 +75,15 @@ public class WebServiceOutboundGatewayParserTests { assertEquals(expected, accessor.getPropertyValue("outputChannel")); Assert.assertEquals(Boolean.FALSE, accessor.getPropertyValue("requiresReply")); - @SuppressWarnings("unchecked") - List requestHeaders = TestUtils.getPropertyValue(endpoint, "handler.headerMapper.requestHeaderNames", List.class); - @SuppressWarnings("unchecked") - List replyHeaders = TestUtils.getPropertyValue(endpoint, "handler.headerMapper.replyHeaderNames", List.class); - assertEquals(1, requestHeaders.size()); - assertEquals(1, replyHeaders.size()); - assertTrue(requestHeaders.contains("testRequest")); - assertTrue(replyHeaders.contains("testReply")); + AbstractHeaderMapper.HeaderMatcher requestHeaderMatcher = TestUtils.getPropertyValue(endpoint, + "handler.headerMapper.requestHeaderMatcher", AbstractHeaderMapper.HeaderMatcher.class); + assertTrue(requestHeaderMatcher.matchHeader("testRequest")); + assertFalse(requestHeaderMatcher.matchHeader("testReply")); + + AbstractHeaderMapper.HeaderMatcher replyHeaderMatcher = TestUtils.getPropertyValue(endpoint, + "handler.headerMapper.replyHeaderMatcher", AbstractHeaderMapper.HeaderMatcher.class); + assertFalse(replyHeaderMatcher.matchHeader("testRequest")); + assertTrue(replyHeaderMatcher.matchHeader("testReply")); Long sendTimeout = TestUtils.getPropertyValue(gateway, "messagingTemplate.sendTimeout", Long.class); assertEquals(Long.valueOf(777), sendTimeout); diff --git a/spring-integration-ws/src/test/java/org/springframework/integration/ws/config/WebServiceOutboundGatewayWithHeaderMapperTests.java b/spring-integration-ws/src/test/java/org/springframework/integration/ws/config/WebServiceOutboundGatewayWithHeaderMapperTests.java index 4f34594f71..2cfd5e5eb1 100644 --- a/spring-integration-ws/src/test/java/org/springframework/integration/ws/config/WebServiceOutboundGatewayWithHeaderMapperTests.java +++ b/spring-integration-ws/src/test/java/org/springframework/integration/ws/config/WebServiceOutboundGatewayWithHeaderMapperTests.java @@ -17,6 +17,7 @@ package org.springframework.integration.ws.config; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; @@ -24,7 +25,6 @@ import static org.junit.Assert.assertTrue; import java.io.ByteArrayInputStream; import java.io.IOException; import java.net.URI; -import java.util.List; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; @@ -39,6 +39,7 @@ import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.integration.mapping.AbstractHeaderMapper; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.integration.channel.QueueChannel; @@ -90,14 +91,23 @@ public class WebServiceOutboundGatewayWithHeaderMapperTests { DefaultSoapHeaderMapper headerMapper = TestUtils.getPropertyValue(gateway, "headerMapper", DefaultSoapHeaderMapper.class); assertNotNull(headerMapper); - List requestHeaderNames = TestUtils.getPropertyValue(headerMapper, "requestHeaderNames", List.class); - assertEquals(2, requestHeaderNames.size()); - assertEquals("foo*", requestHeaderNames.get(0)); - assertEquals("*baz*", requestHeaderNames.get(1)); + AbstractHeaderMapper.HeaderMatcher requestHeaderMatcher = TestUtils.getPropertyValue(headerMapper, + "requestHeaderMatcher", AbstractHeaderMapper.HeaderMatcher.class); + assertTrue(requestHeaderMatcher.matchHeader("foo")); + assertTrue(requestHeaderMatcher.matchHeader("foo123")); + assertTrue(requestHeaderMatcher.matchHeader("baz")); + assertTrue(requestHeaderMatcher.matchHeader("123baz123")); + assertFalse(requestHeaderMatcher.matchHeader("bar")); + assertFalse(requestHeaderMatcher.matchHeader("bar123")); - List responseHeaderNames = TestUtils.getPropertyValue(headerMapper, "replyHeaderNames", List.class); - assertEquals(1, responseHeaderNames.size()); - assertEquals("bar*", responseHeaderNames.get(0)); + AbstractHeaderMapper.HeaderMatcher replyHeaderMatcher = TestUtils.getPropertyValue(headerMapper, + "replyHeaderMatcher", AbstractHeaderMapper.HeaderMatcher.class); + assertFalse(replyHeaderMatcher.matchHeader("foo")); + assertFalse(replyHeaderMatcher.matchHeader("foo123")); + assertFalse(replyHeaderMatcher.matchHeader("baz")); + assertFalse(replyHeaderMatcher.matchHeader("123baz123")); + assertTrue(replyHeaderMatcher.matchHeader("bar")); + assertTrue(replyHeaderMatcher.matchHeader("bar123")); } @Test diff --git a/spring-integration-xmpp/src/main/java/org/springframework/integration/xmpp/support/DefaultXmppHeaderMapper.java b/spring-integration-xmpp/src/main/java/org/springframework/integration/xmpp/support/DefaultXmppHeaderMapper.java index 19e460ac58..fa34d6de19 100644 --- a/spring-integration-xmpp/src/main/java/org/springframework/integration/xmpp/support/DefaultXmppHeaderMapper.java +++ b/spring-integration-xmpp/src/main/java/org/springframework/integration/xmpp/support/DefaultXmppHeaderMapper.java @@ -34,6 +34,7 @@ import org.springframework.util.StringUtils; * @author Mark Fisher * @author Oleg Zhurakousky * @author Florian Schmaus + * @author Stephane Nicoll * * @since 2.1 */ @@ -49,6 +50,10 @@ public class DefaultXmppHeaderMapper extends AbstractHeaderMapper imple STANDARD_HEADER_NAMES.add(XmppHeaders.TYPE); } + public DefaultXmppHeaderMapper() { + super(XmppHeaders.PREFIX, STANDARD_HEADER_NAMES, STANDARD_HEADER_NAMES); + } + @Override protected Map extractStandardHeaders(Message source) { Map headers = new HashMap(); @@ -128,18 +133,4 @@ public class DefaultXmppHeaderMapper extends AbstractHeaderMapper imple JivePropertiesManager.addProperty(target, headerName, headerValue); } - @Override - protected List getStandardReplyHeaderNames() { - return STANDARD_HEADER_NAMES; - } - - @Override - protected List getStandardRequestHeaderNames() { - return STANDARD_HEADER_NAMES; - } - @Override - protected String getStandardHeaderPrefix() { - return XmppHeaders.PREFIX; - } - } diff --git a/spring-integration-xmpp/src/main/resources/org/springframework/integration/xmpp/config/spring-integration-xmpp-4.1.xsd b/spring-integration-xmpp/src/main/resources/org/springframework/integration/xmpp/config/spring-integration-xmpp-4.1.xsd index bb20ec02c4..e51e634ee8 100644 --- a/spring-integration-xmpp/src/main/resources/org/springframework/integration/xmpp/config/spring-integration-xmpp-4.1.xsd +++ b/spring-integration-xmpp/src/main/resources/org/springframework/integration/xmpp/config/spring-integration-xmpp-4.1.xsd @@ -259,7 +259,7 @@ - The conversation thread id used to corelate XMPP packets as + The conversation thread id used to correlate XMPP packets as belonging to a particular conversation diff --git a/spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/config/ChatMessageOutboundChannelAdapterParserTests.java b/spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/config/ChatMessageOutboundChannelAdapterParserTests.java index 50ae3d53a4..8f9798a533 100644 --- a/spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/config/ChatMessageOutboundChannelAdapterParserTests.java +++ b/spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/config/ChatMessageOutboundChannelAdapterParserTests.java @@ -18,11 +18,10 @@ package org.springframework.integration.xmpp.config; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertFalse; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import java.util.List; - import org.jivesoftware.smack.XMPPConnection; import org.jivesoftware.smackx.jiveproperties.JivePropertiesManager; import org.junit.Test; @@ -32,6 +31,7 @@ import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; +import org.springframework.integration.mapping.AbstractHeaderMapper; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.integration.channel.QueueChannel; @@ -96,10 +96,15 @@ public class ChatMessageOutboundChannelAdapterParserTests { Object eventConsumer = context.getBean("outboundEventAdapter"); DefaultXmppHeaderMapper headerMapper = TestUtils.getPropertyValue(eventConsumer, "handler.headerMapper", DefaultXmppHeaderMapper.class); - List requestHeaderNames = TestUtils.getPropertyValue(headerMapper, "requestHeaderNames", List.class); - assertEquals(2, requestHeaderNames.size()); - assertEquals("foo*", requestHeaderNames.get(0)); - assertEquals("bar*", requestHeaderNames.get(1)); + + AbstractHeaderMapper.HeaderMatcher requestHeaderMatcher = TestUtils.getPropertyValue(headerMapper, + "requestHeaderMatcher", AbstractHeaderMapper.HeaderMatcher.class); + assertTrue(requestHeaderMatcher.matchHeader("foo")); + assertTrue(requestHeaderMatcher.matchHeader("foo123")); + assertTrue(requestHeaderMatcher.matchHeader("bar")); + assertTrue(requestHeaderMatcher.matchHeader("bar123")); + assertFalse(requestHeaderMatcher.matchHeader("biz")); + assertFalse(requestHeaderMatcher.matchHeader("else")); assertTrue(eventConsumer instanceof EventDrivenConsumer); } diff --git a/src/reference/docbook/amqp.xml b/src/reference/docbook/amqp.xml index fcac39da59..f63c71d7b0 100644 --- a/src/reference/docbook/amqp.xml +++ b/src/reference/docbook/amqp.xml @@ -698,7 +698,7 @@ public Object handle(@Payload String payload, @Header(AmqpHeaders.CHANNEL) Chann "pollable" option for a publish-subscribe-channel; it must be message-driven. -

+
AMQP Message Headers The Spring Integration AMPQ Adapters will map standard AMQP properties @@ -724,6 +724,15 @@ public Object handle(@Payload String payload, @Header(AmqpHeaders.CHANNEL) Chann if you need to copy all user-defined headers simply use the wild-card character '*'. + + Starting with version 4.1, the AbstractHeaderMapper + (a DefaultAmqpHeaderMapper superclass) allows the NON_STANDARD_HEADERS + token to be configured for the requestHeaderNames and/or replyHeaderNames + properties (in addition to existing STANDARD_REQUEST_HEADERS and + STANDARD_REPLY_HEADERS) to map all user-defined headers. Note, it is recommended to use the + combination like this STANDARD_REPLY_HEADERS, NON_STANDARD_HEADERS instead of + generic *, to avoid mapping of request headers to the reply. + Class AmqpHeaders identifies the default headers that will be used by the diff --git a/src/reference/docbook/whats-new.xml b/src/reference/docbook/whats-new.xml index f1483bb1ef..e15ec49615 100644 --- a/src/reference/docbook/whats-new.xml +++ b/src/reference/docbook/whats-new.xml @@ -146,5 +146,13 @@ See for more information.
+
+ AbstractHeaderMapper: NON_STANDARD_HEADERS token + + The AbstractHeaderMapper implementations now provides the additional + NON_STANDARD_HEADERS token to map any user-defined headers, which aren't mapped by default. + See for more information. + +