INT-3811: Support Negative Matches on Header Map

JIRA: https://jira.spring.io/browse/INT-3811

Polishing - PR Comments
This commit is contained in:
Gary Russell
2016-02-11 12:47:59 -05:00
committed by Artem Bilan
parent 583371007a
commit edb30efcd5
7 changed files with 264 additions and 30 deletions

View File

@@ -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<T> implements RequestReplyHeaderMappe
* @return a header mapper that match if any of the specified patters match
*/
protected HeaderMatcher createHeaderMatcher(Collection<String> patterns) {
Collection<HeaderMatcher> matchers = new ArrayList<HeaderMatcher>();
List<HeaderMatcher> matchers = new ArrayList<HeaderMatcher>();
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<T> 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<T> 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<T> 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<T> implements RequestReplyHeaderMappe
private static final Log logger = LogFactory.getLog(HeaderMatcher.class);
private final Collection<String> patterns;
private final Collection<String> patterns = new ArrayList<String>();
public PatternBasedHeaderMatcher(Collection<String> 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<T> 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.
* <p> 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<T> implements RequestReplyHeaderMappe
return result;
}
@Override
public boolean isNegated() {
return false;
}
}
/**
@@ -464,14 +547,14 @@ public abstract class AbstractHeaderMapper<T> 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<HeaderMatcher> strategies;
private final Collection<HeaderMatcher> matchers;
CompositeHeaderMatcher(Collection<HeaderMatcher> strategies) {
this.strategies = strategies;
this.matchers = strategies;
}
CompositeHeaderMatcher(HeaderMatcher... strategies) {
@@ -480,8 +563,11 @@ public abstract class AbstractHeaderMapper<T> 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<T> implements RequestReplyHeaderMappe
return false;
}
@Override
public boolean isNegated() {
return false;
}
}
}

View File

@@ -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<String, Object> headers = new HashMap<String, Object>();
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);

View File

@@ -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<SoapMessage> implements SoapHeaderMapper {
private static final List<String> STANDARD_HEADER_NAMES = new ArrayList<String>();
static {
@@ -67,8 +67,10 @@ public class DefaultSoapHeaderMapper extends AbstractHeaderMapper<SoapMessage> i
Map<String, Object> headers = new HashMap<String, Object>(1);
headers.put(WebServiceHeaders.SOAP_ACTION, soapAction);
return headers;
} else
}
else {
return Collections.emptyMap();
}
}
@Override

View File

@@ -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:

View File

@@ -114,7 +114,7 @@ See <<http-inbound>> for more information.
A new factory bean is provided to simplify the configuration of Jsch proxies for SFTP.
See <<sftp-proxy-factory-bean>> 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 <<router>> for more information.
==== Header Mapping
AMQP, WS and XMPP header mappings (e.g. `request-header-mapping`, `reply-header-mapping`) now support negated
patterns.
See <<amqp-message-headers>>, <<ws-message-headers>>, and <<xmpp-message-headers>> for more information.

View File

@@ -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.

View File

@@ -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.