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 515d417dd9..d967f563f9 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 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. @@ -20,8 +20,8 @@ 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 java.util.Map.Entry; @@ -139,7 +139,7 @@ public abstract class AbstractHeaderMapper implements RequestReplyHeaderMappe * @return a header mapper that match if any of the specified patters match */ protected HeaderMatcher createHeaderMatcher(Collection patterns) { - Collection matchers = new ArrayList(); + List matchers = new ArrayList(); for (String pattern : patterns) { if (STANDARD_REQUEST_HEADER_NAME_PATTERN.equals(pattern)) { matchers.add(new ContentBasedHeaderMatcher(true, this.requestHeaderNames)); @@ -151,7 +151,22 @@ public abstract class AbstractHeaderMapper implements RequestReplyHeaderMappe matchers.add(new PrefixBasedMatcher(false, this.standardHeaderPrefix)); } else { - matchers.add(new PatternBasedHeaderMatcher(Collections.singleton(pattern))); + String thePattern = pattern; + boolean negate = false; + if (pattern.startsWith("!")) { + thePattern = pattern.substring(1); + negate = true; + } + else if (pattern.startsWith("\\!")) { + thePattern = pattern.substring(1); + } + if (negate) { + // negative matchers get priority + matchers.add(0, new SinglePatternBasedHeaderMatcher(thePattern, negate)); + } + else { + matchers.add(new SinglePatternBasedHeaderMatcher(thePattern, negate)); + } } } return new CompositeHeaderMatcher(matchers); @@ -343,6 +358,12 @@ public abstract class AbstractHeaderMapper implements RequestReplyHeaderMappe */ boolean matchHeader(String headerName); + /** + * Return true if this match should be explicitly excluded from the mapping. + * @return true if negated. + */ + boolean isNegated(); + } /** @@ -388,11 +409,16 @@ public abstract class AbstractHeaderMapper implements RequestReplyHeaderMappe return false; } + @Override + public boolean isNegated() { + return false; + } + } /** * A pattern-based {@link HeaderMatcher} that matches if the specified - * header match one of the specified simple patterns. + * header matches one of the specified simple patterns. * @see org.springframework.util.PatternMatchUtils#simpleMatch(String, String) * @since 4.1 */ @@ -400,19 +426,21 @@ public abstract class AbstractHeaderMapper implements RequestReplyHeaderMappe private static final Log logger = LogFactory.getLog(HeaderMatcher.class); - private final Collection patterns; + private final Collection patterns = new ArrayList(); public PatternBasedHeaderMatcher(Collection patterns) { - Assert.notNull(patterns, "Patters must no be null"); + Assert.notNull(patterns, "Patterns must no be null"); Assert.notEmpty(patterns, "At least one pattern must be specified"); - this.patterns = patterns; + for (String pattern : patterns) { + this.patterns.add(pattern.toLowerCase()); + } } @Override public boolean matchHeader(String headerName) { String header = headerName.toLowerCase(); for (String pattern : this.patterns) { - if (PatternMatchUtils.simpleMatch(pattern.toLowerCase(), header)) { + if (PatternMatchUtils.simpleMatch(pattern, header)) { if (logger.isDebugEnabled()) { logger.debug(MessageFormat.format( "headerName=[{0}] WILL be mapped, matched pattern={1}", headerName, pattern)); @@ -423,6 +451,56 @@ public abstract class AbstractHeaderMapper implements RequestReplyHeaderMappe return false; } + @Override + public boolean isNegated() { + return false; + } + + } + + /** + * A pattern-based {@link HeaderMatcher} that matches if the specified + * header matches the specified simple pattern. + *

The {@code negate == true} state indicates if the matching should be treated as "not matched". + * @see org.springframework.util.PatternMatchUtils#simpleMatch(String, String) + * @since 4.3 + */ + protected static class SinglePatternBasedHeaderMatcher implements HeaderMatcher { + + private static final Log logger = LogFactory.getLog(HeaderMatcher.class); + + private final String pattern; + + private final boolean negate; + + public SinglePatternBasedHeaderMatcher(String pattern) { + this(pattern, false); + } + + public SinglePatternBasedHeaderMatcher(String pattern, boolean negate) { + Assert.notNull(pattern, "Pattern must no be null"); + this.pattern = pattern.toLowerCase(); + this.negate = negate; + } + + @Override + public boolean matchHeader(String headerName) { + String header = headerName.toLowerCase(); + if (PatternMatchUtils.simpleMatch(this.pattern, header)) { + if (logger.isDebugEnabled()) { + logger.debug(MessageFormat.format( + "headerName=[{0}] WILL be mapped, matched pattern={1}", headerName, pattern)); + } + return true; + } + return false; + } + + @Override + public boolean isNegated() { + return this.negate; + } + } /** @@ -457,6 +535,11 @@ public abstract class AbstractHeaderMapper implements RequestReplyHeaderMappe return result; } + @Override + public boolean isNegated() { + return false; + } + } /** @@ -464,14 +547,14 @@ public abstract class AbstractHeaderMapper implements RequestReplyHeaderMappe * {@link HeaderMatcher}s matches to the {@code headerName}. * @since 4.1 */ - protected static class CompositeHeaderMatcher implements HeaderMatcher { + protected static class CompositeHeaderMatcher implements HeaderMatcher{ private static final Log logger = LogFactory.getLog(HeaderMatcher.class); - private final Collection strategies; + private final Collection matchers; CompositeHeaderMatcher(Collection strategies) { - this.strategies = strategies; + this.matchers = strategies; } CompositeHeaderMatcher(HeaderMatcher... strategies) { @@ -480,8 +563,11 @@ public abstract class AbstractHeaderMapper implements RequestReplyHeaderMappe @Override public boolean matchHeader(String headerName) { - for (HeaderMatcher strategy : this.strategies) { + for (HeaderMatcher strategy : this.matchers) { if (strategy.matchHeader(headerName)) { + if (strategy.isNegated()) { + break; + } return true; } } @@ -491,6 +577,11 @@ public abstract class AbstractHeaderMapper implements RequestReplyHeaderMappe return false; } + @Override + public boolean isNegated() { + return false; + } + } } 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 index 117e5b5897..9c7a676aff 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2016 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,11 +19,6 @@ 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; @@ -33,6 +28,12 @@ import java.util.Map; import org.junit.Test; +import org.springframework.integration.mapping.AbstractHeaderMapper.CompositeHeaderMatcher; +import org.springframework.integration.mapping.AbstractHeaderMapper.ContentBasedHeaderMatcher; +import org.springframework.integration.mapping.AbstractHeaderMapper.HeaderMatcher; +import org.springframework.integration.mapping.AbstractHeaderMapper.PatternBasedHeaderMatcher; +import org.springframework.integration.mapping.AbstractHeaderMapper.PrefixBasedMatcher; +import org.springframework.integration.mapping.AbstractHeaderMapper.SinglePatternBasedHeaderMatcher; import org.springframework.messaging.MessageHeaders; import org.springframework.util.StringUtils; @@ -237,6 +238,33 @@ public class HeaderMapperTests { assertEquals(1, properties.getUserDefinedHeaders().size()); } + @Test + public void fromHeadersToRequestWithStandardRequestPatternAndNegatives() { + this.mapper.setRequestHeaderNames("foo", "!foo", "bar", "!baz", "\\!qux", "!fiz*", + GenericTestHeaderMapper.STANDARD_REQUEST_HEADER_NAME_PATTERN); + 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", "foo"); + headers.put("bar", "bar"); + headers.put("baz", "baz"); + headers.put("!qux", "qux"); + headers.put("fizbuz", "fizbuz"); + MessageHeaders messageHeaders = new MessageHeaders(headers); + 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("bar")); + assertEquals("qux", properties.getUserDefinedHeaders().get("!qux")); + assertEquals(2, properties.getUserDefinedHeaders().size()); + } + @Test public void fromHeadersToReply() { MessageHeaders messageHeaders = createSimpleMessageHeaders(); @@ -296,8 +324,9 @@ public class HeaderMapperTests { @Test public void prefixHeaderPatternMatching() { + @SuppressWarnings("deprecation") PatternBasedHeaderMatcher strategy = - new PatternBasedHeaderMatcher(Collections.singleton("foo*")); + new PatternBasedHeaderMatcher(Collections.singleton("fOo*")); assertMapping(strategy, "foo", true); assertMapping(strategy, "foo123", true); @@ -309,8 +338,35 @@ public class HeaderMapperTests { @Test public void suffixHeaderPatternMatching() { + @SuppressWarnings("deprecation") PatternBasedHeaderMatcher strategy = - new PatternBasedHeaderMatcher(Collections.singleton("*foo")); + 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 prefixSingleHeaderPatternMatching() { + SinglePatternBasedHeaderMatcher strategy = + new SinglePatternBasedHeaderMatcher("Foo*"); + + assertMapping(strategy, "foo", true); + assertMapping(strategy, "foo123", true); + assertMapping(strategy, "FoO", true); + + assertMapping(strategy, "123foo", false); + assertMapping(strategy, "_foo", false); + } + + @Test + public void suffixSingleHeaderPatternMatching() { + SinglePatternBasedHeaderMatcher strategy = + new SinglePatternBasedHeaderMatcher("*fOo"); assertMapping(strategy, "foo", true); assertMapping(strategy, "123foo", true); @@ -322,7 +378,7 @@ public class HeaderMapperTests { @Test public void contentHeaderMatching() { - AbstractHeaderMapper.ContentBasedHeaderMatcher strategy = + ContentBasedHeaderMatcher strategy = new ContentBasedHeaderMatcher(true, Arrays.asList("foo", "bar")); assertMapping(strategy, "foo", true); 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 0534755f15..12e251e841 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-2015 the original author or authors. + * Copyright 2002-2016 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. @@ -36,12 +36,12 @@ import org.springframework.xml.namespace.QNameUtils; /** * A {@link HeaderMapper} implementation for mapping to and from a SoapHeader. * The {@link #setRequestHeaderNames(String[])} and {@link #setReplyHeaderNames(String[])} - * accept exact name Strings or simple patterns (e.g. "start*", "*end", or "*"). + * accept exact name Strings or simple patterns (e.g. "start*", "*end", or "*"). * By default all inbound headers will be accepted, but any outbound header that should * be mapped must be configured explicitly. Note that the outbound mapping only writes * String header values into attributes on the SoapHeader. For anything more advanced, * one should implement the HeaderMapper interface directly. - * + * * @author Mark Fisher * @author Oleg Zhurakousky * @author Stephane Nicoll @@ -49,7 +49,7 @@ import org.springframework.xml.namespace.QNameUtils; * @since 2.0 */ public class DefaultSoapHeaderMapper extends AbstractHeaderMapper implements SoapHeaderMapper { - + private static final List STANDARD_HEADER_NAMES = new ArrayList(); static { @@ -67,8 +67,10 @@ public class DefaultSoapHeaderMapper extends AbstractHeaderMapper i Map headers = new HashMap(1); headers.put(WebServiceHeaders.SOAP_ACTION, soapAction); return headers; - } else + } + else { return Collections.emptyMap(); + } } @Override diff --git a/src/reference/asciidoc/amqp.adoc b/src/reference/asciidoc/amqp.adoc index d862f2f786..2f2af7cacf 100644 --- a/src/reference/asciidoc/amqp.adoc +++ b/src/reference/asciidoc/amqp.adoc @@ -1059,11 +1059,14 @@ public IntegrationFlow pubSubInFlow(ConnectionFactory connectionFactory) { === AMQP Message Headers The Spring Integration AMQP Adapters will map standard AMQP properties automatically. -These properties will be copied by default to and from Spring Integration `MessageHeaders` using the http://static.springsource.org/spring-integration/api/org/springframework/integration/amqp/support/DefaultAmqpHeaderMapper.html[DefaultAmqpHeaderMapper]. +These properties will be copied by default to and from Spring Integration `MessageHeaders` using the +http://docs.spring.io/spring-integration/api/org/springframework/integration/amqp/support/DefaultAmqpHeaderMapper.html[DefaultAmqpHeaderMapper]. Of course, you can pass in your own implementation of AMQP specific header mappers, as the adapters have respective properties to support that. -Any user-defined headers within the AMQP http://static.springsource.org/spring-amqp/api/org/springframework/amqp/core/MessageProperties.html[MessageProperties] will NOT be copied to or from an AMQP Message, unless explicitly specified by the _requestHeaderNames_ and/or _replyHeaderNames_ properties of the `DefaultAmqpHeaderMapper`. +Any user-defined headers within the AMQP http://docs.spring.io/spring-amqp/api/org/springframework/amqp/core/MessageProperties.html[MessageProperties] will NOT +be copied to or from an AMQP Message, unless explicitly specified by the _requestHeaderNames_ and/or +_replyHeaderNames_ properties of the `DefaultAmqpHeaderMapper`. TIP: When mapping user-defined headers, the values can also contain simple wildcard patterns (e.g. "foo*" or "*foo") to be matched. For example, if you need to copy all user-defined headers simply use the wildcard character `*`, but see the CAUTION below. @@ -1142,6 +1145,14 @@ before sending the reply to the AMQP Inbound Gateway. Alternatively, you could explicitly list those properties that you actually want mapped instead of using wildcards. +Starting with _version 4.3_, patterns in the header mappings can be negated by preceding the pattern with `!`. +Negated patterns get priority, so a list such as +`STANDARD_REQUEST_HEADERS,foo,ba*,!bar,!baz,qux,!foo` will *NOT* map `foo` +(nor `bar` nor `baz`); the standard headers plus `bad`, `qux` will be mapped. + +IMPORTANT: If you have a user defined header that begins with `!` that you *do* wish to map, you need to escape it with +`\` thus: `STANDARD_REQUEST_HEADERS,\!myBangHeader` and it *WILL* be mapped. + === AMQP Samples To experiment with the AMQP adapters, check out the samples available in the Spring Integration Samples Git repository at: diff --git a/src/reference/asciidoc/whats-new.adoc b/src/reference/asciidoc/whats-new.adoc index 9aca185639..934f139a08 100644 --- a/src/reference/asciidoc/whats-new.adoc +++ b/src/reference/asciidoc/whats-new.adoc @@ -114,7 +114,7 @@ See <> for more information. A new factory bean is provided to simplify the configuration of Jsch proxies for SFTP. See <> for more information. -==== Routers Changes +==== Router Changes The `ErrorMessageExceptionTypeRouter` supports now the `Exception` superclass mappings to avoid duplication for the same channel in case of several inheritors. @@ -122,3 +122,9 @@ For this purpose the `ErrorMessageExceptionTypeRouter` loads mapping classes dur for a `ClassNotFoundException`. See <> for more information. + +==== Header Mapping + +AMQP, WS and XMPP header mappings (e.g. `request-header-mapping`, `reply-header-mapping`) now support negated +patterns. +See <>, <>, and <> for more information. diff --git a/src/reference/asciidoc/ws.adoc b/src/reference/asciidoc/ws.adoc index 79c0c9de22..f81f7a0074 100644 --- a/src/reference/asciidoc/ws.adoc +++ b/src/reference/asciidoc/ws.adoc @@ -165,3 +165,34 @@ If you wish to partially encode some of the URL, this can be achieved using an ` ---- Note, `encode-uri` is ignored, if `DestinationProvider` is supplied. + +[[ws-message-headers]] +=== WS Message Headers + +The Spring Integration WebService Gateways will map the SOAP Action header automatically. +It will be copied by default to and from Spring Integration `MessageHeaders` using the +http://docs.spring.io/spring-integration/api/org/springframework/integration/ws/DefaultSoapHeaderMapper.html[DefaultSoapHeaderMapper]. + +Of course, you can pass in your own implementation of SOAP specific header mappers, as the gateways have respective properties to support that. + +Any user-defined headers will NOT +be copied to or from an SOAP Message, unless explicitly specified by the _requestHeaderNames_ and/or +_replyHeaderNames_ properties of the `DefaultSoapHeaderMapper`. + +TIP: When mapping user-defined headers, the values can also contain simple wildcard patterns (e.g. "foo*" or "*foo") to be matched. +For example, if you need to copy all user-defined headers simply use the wildcard character `*`. + +Starting with _version 4.1_, the `AbstractHeaderMapper` (a `DefaultSoapHeaderMapper` 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 a +`*`, to avoid mapping of _request_ headers to the reply. + +Starting with _version 4.3_, patterns in the header mappings can be negated by preceding the pattern with `!`. +Negated patterns get priority, so a list such as +`STANDARD_REQUEST_HEADERS,foo,ba*,!bar,!baz,qux,!foo` will *NOT* map `foo` +(nor `bar` nor `baz`); the standard headers plus `bad`, `qux` will be mapped. + +IMPORTANT: If you have a user defined header that begins with `!` that you *do* wish to map, you need to escape it with +`\` thus: `STANDARD_REQUEST_HEADERS,\!myBangHeader` and it *WILL* be mapped. diff --git a/src/reference/asciidoc/xmpp.adoc b/src/reference/asciidoc/xmpp.adoc index 8e19af45db..7b2550c89a 100644 --- a/src/reference/asciidoc/xmpp.adoc +++ b/src/reference/asciidoc/xmpp.adoc @@ -219,3 +219,40 @@ public class CustomConnectionConfiguration { ---- For more information on the JavaConfig style of Application Context configuration, refer to the following section in the Spring Reference Manual: http://static.springsource.org/spring/docs/3.0.x/spring-framework-reference/html/beans.html#beans-java + +[[xmpp-message-headers]] +=== XMPP Message Headers + +The Spring Integration XMPP Adapters will map standard XMPP properties automatically. +These properties will be copied by default to and from Spring Integration `MessageHeaders` using the +http://docs.spring.io/spring-integration/api/org/springframework/integration/xmpp/support/DefaultXmppHeaderMapper.html[DefaultXmppHeaderMapper]. + +Any user-defined headers will NOT be copied to or from an XMPP Message, unless explicitly specified by the +_requestHeaderNames_ and/or _replyHeaderNames_ properties of the `DefaultXmppHeaderMapper`. + +TIP: When mapping user-defined headers, the values can also contain simple wildcard patterns (e.g. "foo*" or "*foo") to +be matched. + +Starting with _version 4.1_, the `AbstractHeaderMapper` (a `DefaultXmppHeaderMapper` superclass) allows the +`NON_STANDARD_HEADERS` token to be configured for the _requestHeaderNames_ property (in addition to existing +`STANDARD_REQUEST_HEADERS`) to map all user-defined headers. + +Class `org.springframework.xmpp.XmppHeaders` identifies the default headers that will be used by the `DefaultXmppHeaderMapper`: + +* xmpp_from + +* xmpp_subject + +* xmpp_thread + +* xmpp_to + +* xmpp_type + +Starting with _version 4.3_, patterns in the header mappings can be negated by preceding the pattern with `!`. +Negated patterns get priority, so a list such as +`STANDARD_REQUEST_HEADERS,foo,ba*,!bar,!baz,qux,!foo` will *NOT* map `foo` +(nor `bar` nor `baz`); the standard headers plus `bad`, `qux` will be mapped. + +IMPORTANT: If you have a user defined header that begins with `!` that you *do* wish to map, you need to escape it with +`\` thus: `STANDARD_REQUEST_HEADERS,\!myBangHeader` and it *WILL* be mapped.