This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch 8958 in repository https://gitbox.apache.org/repos/asf/camel.git
commit c135f44eeaa2ba234ddc1c8294cb1077868d9ac7 Author: Claus Ibsen <claus.ib...@gmail.com> AuthorDate: Tue Feb 6 14:21:22 2018 +0100 CAMEL-8958: Claim Check EIP with push/pop. Work in progress. --- .../apache/camel/model/ClaimCheckDefinition.java | 105 ++++---- .../apache/camel/model/ProcessorDefinition.java | 25 +- .../processor/ClaimCheckAggregationStrategy.java | 265 +++++++++++++++------ .../camel/processor/ClaimCheckProcessor.java | 22 +- .../ClaimCheckEipPushPopExcludeBodyTest.java | 6 +- ...a => ClaimCheckEipPushPopRemoveHeaderTest.java} | 12 +- 6 files changed, 274 insertions(+), 161 deletions(-) diff --git a/camel-core/src/main/java/org/apache/camel/model/ClaimCheckDefinition.java b/camel-core/src/main/java/org/apache/camel/model/ClaimCheckDefinition.java index 4dc1609..2b5dbce 100644 --- a/camel-core/src/main/java/org/apache/camel/model/ClaimCheckDefinition.java +++ b/camel-core/src/main/java/org/apache/camel/model/ClaimCheckDefinition.java @@ -46,9 +46,7 @@ public class ClaimCheckDefinition extends NoOutputDefinition<ClaimCheckDefinitio @XmlAttribute private String key; @XmlAttribute - private String include; - @XmlAttribute - private String exclude; + private String filter; @XmlAttribute(name = "strategyRef") @Metadata(label = "advanced") private String aggregationStrategyRef; @XmlAttribute(name = "strategyMethodName") @Metadata(label = "advanced") @@ -80,17 +78,60 @@ public class ClaimCheckDefinition extends NoOutputDefinition<ClaimCheckDefinitio ClaimCheckProcessor claim = new ClaimCheckProcessor(); claim.setOperation(operation.name()); claim.setKey(getKey()); - claim.setInclude(getInclude()); - claim.setExclude(getExclude()); + claim.setFilter(getFilter()); AggregationStrategy strategy = createAggregationStrategy(routeContext); if (strategy != null) { claim.setAggregationStrategy(strategy); } - // only data or aggregation strategy can be configured not both - if ((getInclude() != null || getExclude() != null) && strategy != null) { - throw new IllegalArgumentException("Cannot use both include/exclude and custom aggregation strategy on ClaimCheck EIP"); + // only filter or aggregation strategy can be configured not both + if (getFilter() != null && strategy != null) { + throw new IllegalArgumentException("Cannot use both filter and custom aggregation strategy on ClaimCheck EIP"); + } + + // validate filter, we cannot have both +/- at the same time + if (getFilter() != null) { + Iterable it = ObjectHelper.createIterable(filter, ","); + boolean includeBody = false; + boolean excludeBody = false; + for (Object o : it) { + String pattern = o.toString(); + if ("body".equals(pattern) || "+body".equals(pattern)) { + includeBody = true; + } else if ("-body".equals(pattern)) { + excludeBody = true; + } + } + if (includeBody && excludeBody) { + throw new IllegalArgumentException("Cannot have both include and exclude body at the same time in the filter: " + filter); + } + boolean includeHeaders = false; + boolean excludeHeaders = false; + for (Object o : it) { + String pattern = o.toString(); + if ("headers".equals(pattern) || "+headers".equals(pattern)) { + includeHeaders = true; + } else if ("-headers".equals(pattern)) { + excludeHeaders = true; + } + } + if (includeHeaders && excludeHeaders) { + throw new IllegalArgumentException("Cannot have both include and exclude headers at the same time in the filter: " + filter); + } + boolean includeHeader = false; + boolean excludeHeader = false; + for (Object o : it) { + String pattern = o.toString(); + if (pattern.startsWith("header:") || pattern.startsWith("+header:")) { + includeHeader = true; + } else if (pattern.startsWith("-header:")) { + excludeHeader = true; + } + } + if (includeHeader && excludeHeader) { + throw new IllegalArgumentException("Cannot have both include and exclude header at the same time in the filter: " + filter); + } } return claim; @@ -144,7 +185,7 @@ public class ClaimCheckDefinition extends NoOutputDefinition<ClaimCheckDefinitio } /** - * What data to include when merging data back from claim check repository. + * Specified a filter to control what data gets merging data back from the claim check repository. * * The following syntax is supported: * <ul> @@ -155,30 +196,18 @@ public class ClaimCheckDefinition extends NoOutputDefinition<ClaimCheckDefinitio * </ul> * You can specify multiple rules separated by comma. For example to include the message body and all headers starting with foo * <tt>body,header:foo*</tt>. - * If the include rule is specified as empty or as wildcard then everything is included. - * If you have configured both include and exclude then exclude take precedence over include. - */ - public ClaimCheckDefinition include(String include) { - setInclude(include); - return this; - } - - /** - * What data to exclude when merging data back from claim check repository. - * - * The following syntax is supported: + * The syntax supports the following prefixes which can be used to specify include,exclude, or remove * <ul> - * <li>body</li> - to aggregate the message body - * <li>headers</li> - to aggregate all the message headers - * <li>header:pattern</li> - to aggregate all the message headers that matches the pattern. - * The pattern syntax is documented by: {@link EndpointHelper#matchPattern(String, String)}. + * <li>+</li> - to include (which is the default mode) + * <li>-</li> - to exclude (exclude takes precedence over include) + * <li>--</li> - to remove (remove takes precedence) * </ul> - * You can specify multiple rules separated by comma. For example to exclude the message body and all headers starting with bar - * <tt>body,header:bar*</tt>. - * If you have configured both include and exclude then exclude take precedence over include. + * For example to exclude a header name foo, and remove all headers starting with bar + * <tt>-header:foo,--headers:bar*</tt> + * Note you cannot have both include and exclude <tt>header:pattern</tt> at the same time. */ - public ClaimCheckDefinition exclude(String exclude) { - setExclude(exclude); + public ClaimCheckDefinition filter(String filter) { + setFilter(filter); return this; } @@ -227,20 +256,12 @@ public class ClaimCheckDefinition extends NoOutputDefinition<ClaimCheckDefinitio this.operation = operation; } - public String getInclude() { - return include; - } - - public void setInclude(String include) { - this.include = include; - } - - public String getExclude() { - return exclude; + public String getFilter() { + return filter; } - public void setExclude(String exclude) { - this.exclude = exclude; + public void setFilter(String filter) { + this.filter = filter; } public String getAggregationStrategyRef() { diff --git a/camel-core/src/main/java/org/apache/camel/model/ProcessorDefinition.java b/camel-core/src/main/java/org/apache/camel/model/ProcessorDefinition.java index 39a0c6d..9544d93 100644 --- a/camel-core/src/main/java/org/apache/camel/model/ProcessorDefinition.java +++ b/camel-core/src/main/java/org/apache/camel/model/ProcessorDefinition.java @@ -3478,7 +3478,7 @@ public abstract class ProcessorDefinition<Type extends ProcessorDefinition<Type> * @param key the unique key to use for the get and set operations, can be <tt>null</tt> for push/pop operations */ public Type claimCheck(ClaimCheckOperation operation, String key) { - return claimCheck(operation, key, null, null); + return claimCheck(operation, key, null); } /** @@ -3488,30 +3488,13 @@ public abstract class ProcessorDefinition<Type extends ProcessorDefinition<Type> * * @param operation the claim check operation to use. * @param key the unique key to use for the get and set operations, can be <tt>null</tt> for push/pop operations - * @param include describes what data to include when merging data back when using get or pop operations. + * @param filter describes what data to include/exclude when merging data back when using get or pop operations. */ - public Type claimCheck(ClaimCheckOperation operation, String key, String include) { - return claimCheck(operation, key, include, null); - } - - /** - * The <a href="http://camel.apache.org/claim-check.html">Claim Check EIP</a> - * allows you to replace message content with a claim check (a unique key), - * which can be used to retrieve the message content at a later time. - * - * @param operation the claim check operation to use. - * @param key the unique key to use for the get and set operations, can be <tt>null</tt> for push/pop operations - * @param include describes what data to include when merging data back when using get or pop operations. - * If you have configured both include and exclude then exclude take precedence over include. - * @param exclude describes what data to exclude when merging data back when using get or pop operations. - * If you have configured both include and exclude then exclude take precedence over include. - */ - public Type claimCheck(ClaimCheckOperation operation, String key, String include, String exclude) { + public Type claimCheck(ClaimCheckOperation operation, String key, String filter) { ClaimCheckDefinition answer = new ClaimCheckDefinition(); answer.setOperation(operation); answer.setKey(key); - answer.setInclude(include); - answer.setExclude(exclude); + answer.setFilter(filter); addOutput(answer); return (Type) this; } diff --git a/camel-core/src/main/java/org/apache/camel/processor/ClaimCheckAggregationStrategy.java b/camel-core/src/main/java/org/apache/camel/processor/ClaimCheckAggregationStrategy.java index fb58346..b608ad2 100644 --- a/camel-core/src/main/java/org/apache/camel/processor/ClaimCheckAggregationStrategy.java +++ b/camel-core/src/main/java/org/apache/camel/processor/ClaimCheckAggregationStrategy.java @@ -16,7 +16,9 @@ */ package org.apache.camel.processor; +import java.util.HashSet; import java.util.Map; +import java.util.Set; import org.apache.camel.Exchange; import org.apache.camel.processor.aggregate.AggregationStrategy; @@ -44,26 +46,17 @@ import org.slf4j.LoggerFactory; public class ClaimCheckAggregationStrategy implements AggregationStrategy { private static final Logger LOG = LoggerFactory.getLogger(ClaimCheckAggregationStrategy.class); - private String include; - private String exclude; + private String filter; public ClaimCheckAggregationStrategy() { } - public String getInclude() { - return include; + public String getFilter() { + return filter; } - public void setInclude(String include) { - this.include = include; - } - - public String getExclude() { - return exclude; - } - - public void setExclude(String exclude) { - this.exclude = exclude; + public void setFilter(String filter) { + this.filter = filter; } @Override @@ -72,91 +65,217 @@ public class ClaimCheckAggregationStrategy implements AggregationStrategy { return oldExchange; } - if (ObjectHelper.isEmpty(exclude) && (ObjectHelper.isEmpty(include) || "*".equals(include))) { - // grab everything if include is empty or wildcard (and exclude is not in use) - return newExchange; + if (ObjectHelper.isEmpty(filter) || "*".equals(filter)) { + // grab everything + oldExchange.getMessage().setBody(newExchange.getMessage().getBody()); + LOG.trace("Including: body"); + oldExchange.getMessage().getHeaders().putAll(newExchange.getMessage().getHeaders()); + LOG.trace("Including: headers"); + return oldExchange; + } + + // body is by default often included + if (isBodyEnabled()) { + oldExchange.getMessage().setBody(newExchange.getMessage().getBody()); + LOG.trace("Including: body"); + } + + // headers is by default often included + if (isHeadersEnabled()) { + oldExchange.getMessage().getHeaders().putAll(newExchange.getMessage().getHeaders()); + LOG.trace("Including: headers"); + } + + // filter specific header if they are somehow enabled by the filter + if (hasHeaderPatterns()) { + boolean excludeOnly = isExcludeOnlyHeaderPatterns(); + for (Map.Entry<String, Object> header : newExchange.getMessage().getHeaders().entrySet()) { + String key = header.getKey(); + if (hasHeaderPattern(key)) { + boolean include = isIncludedHeader(key); + boolean exclude = isExcludedHeader(key); + if (include) { + LOG.trace("Including: header:{}", key); + oldExchange.getMessage().getHeaders().put(key, header.getValue()); + } else if (exclude) { + LOG.trace("Excluding: header:{}", key); + } else { + LOG.trace("Skipping: header:{}", key); + } + } else if (excludeOnly) { + LOG.trace("Including: header:{}", key); + oldExchange.getMessage().getHeaders().put(key, header.getValue()); + } + } } - // if we have include - if (ObjectHelper.isNotEmpty(include)) { - Iterable it = ObjectHelper.createIterable(include, ","); + // filter body and all headers + if (ObjectHelper.isNotEmpty(filter)) { + Iterable it = ObjectHelper.createIterable(filter, ","); for (Object k : it) { String part = k.toString(); - if ("body".equals(part) && !isExcluded("body")) { + if (("body".equals(part) || "+body".equals(part)) && !"-body".equals(part)) { oldExchange.getMessage().setBody(newExchange.getMessage().getBody()); LOG.trace("Including: body"); - } else if ("headers".equals(part) && !isExcluded("headers")) { + } else if (("headers".equals(part) || "+headers".equals(part)) && !"-headers".equals(part)) { oldExchange.getMessage().getHeaders().putAll(newExchange.getMessage().getHeaders()); LOG.trace("Including: headers"); - } else if (part.startsWith("header:")) { - // pattern matching for headers, eg header:foo, header:foo*, header:(foo|bar) - String after = StringHelper.after(part, "header:"); - Iterable i = ObjectHelper.createIterable(after, ","); - for (Object o : i) { - String pattern = o.toString(); - for (Map.Entry<String, Object> header : newExchange.getMessage().getHeaders().entrySet()) { - String key = header.getKey(); - boolean matched = EndpointHelper.matchPattern(key, pattern); - if (matched && !isExcluded(key)) { - LOG.trace("Including: header:{}", key); - oldExchange.getMessage().getHeaders().put(key, header.getValue()); - } - } - } } } - } else if (ObjectHelper.isNotEmpty(exclude)) { - // grab body unless its excluded - if (!isExcluded("body")) { - oldExchange.getMessage().setBody(newExchange.getMessage().getBody()); - LOG.trace("Including: body"); - } - - // if not all headers is excluded, then check each header one-by-one - if (!isExcluded("headers")) { - // check if we exclude a specific headers - Iterable it = ObjectHelper.createIterable(exclude, ","); - for (Object k : it) { - String part = k.toString(); - if (part.startsWith("header:")) { - // pattern matching for headers, eg header:foo, header:foo*, header:(foo|bar) - String after = StringHelper.after(part, "header:"); - Iterable i = ObjectHelper.createIterable(after, ","); - for (Object o : i) { - String pattern = o.toString(); - for (Map.Entry<String, Object> header : newExchange.getMessage().getHeaders().entrySet()) { - String key = header.getKey(); - boolean excluded = EndpointHelper.matchPattern(key, pattern); - if (!excluded) { - LOG.trace("Including: header:{}", key); - oldExchange.getMessage().getHeaders().put(key, header.getValue()); - } else { - LOG.trace("Excluding: header:{}", key); - } - } + } + + // filter with remove (--) take precedence at the end + Iterable it = ObjectHelper.createIterable(filter, ","); + for (Object k : it) { + String part = k.toString(); + if ("--body".equals(part)) { + oldExchange.getMessage().setBody(null); + } else if ("--headers".equals(part)) { + oldExchange.getMessage().getHeaders().clear(); + } else if (part.startsWith("--header:")) { + // pattern matching for headers, eg header:foo, header:foo*, header:(foo|bar) + String after = StringHelper.after(part, "--header:"); + Iterable i = ObjectHelper.createIterable(after, ","); + Set<String> toRemoveKeys = new HashSet<>(); + for (Object o : i) { + String pattern = o.toString(); + for (Map.Entry<String, Object> header : oldExchange.getMessage().getHeaders().entrySet()) { + String key = header.getKey(); + boolean matched = EndpointHelper.matchPattern(key, pattern); + if (matched) { + toRemoveKeys.add(key); } } } + for (String key : toRemoveKeys) { + LOG.trace("Removing: header:{}", key); + oldExchange.getMessage().removeHeader(key); + } } } return oldExchange; } - private boolean isExcluded(String key) { - if (ObjectHelper.isEmpty(exclude)) { - return false; + private boolean hasHeaderPatterns() { + String[] parts = filter.split(","); + for (String pattern : parts) { + if (pattern.startsWith("--")) { + continue; + } + if (pattern.startsWith("header:") || pattern.startsWith("+header:") || pattern.startsWith("-header:")) { + return true; + } + } + return false; + } + + private boolean isExcludeOnlyHeaderPatterns() { + String[] parts = filter.split(","); + for (String pattern : parts) { + if (pattern.startsWith("--")) { + continue; + } + if (pattern.startsWith("header:") || pattern.startsWith("+header:")) { + return false; + } + } + return true; + } + + private boolean hasHeaderPattern(String key) { + String[] parts = filter.split(","); + for (String pattern : parts) { + if (pattern.startsWith("--")) { + continue; + } + String header = null; + if (pattern.startsWith("header:") || pattern.startsWith("+header:")) { + header = StringHelper.after(pattern, "header:"); + } else if (pattern.startsWith("-header:")) { + header = StringHelper.after(pattern, "-header:"); + } + if (header != null && EndpointHelper.matchPattern(key, header)) { + return true; + } } - String[] excludes = exclude.split(","); - for (String pattern : excludes) { - if (pattern.startsWith("header:")) { + return false; + } + + private boolean isIncludedHeader(String key) { + String[] parts = filter.split(","); + for (String pattern : parts) { + if (pattern.startsWith("--")) { + continue; + } + if (pattern.startsWith("header:") || pattern.startsWith("+header:")) { pattern = StringHelper.after(pattern, "header:"); } if (EndpointHelper.matchPattern(key, pattern)) { - LOG.trace("Excluding: {}", key); return true; } } return false; } + + private boolean isExcludedHeader(String key) { + String[] parts = filter.split(","); + for (String pattern : parts) { + if (pattern.startsWith("--")) { + continue; + } + if (pattern.startsWith("-header:")) { + pattern = StringHelper.after(pattern, "-header:"); + } + if (EndpointHelper.matchPattern(key, pattern)) { + return true; + } + } + return false; + } + + private boolean isBodyEnabled() { + // body is always enabled unless excluded + String[] parts = filter.split(","); + + boolean onlyExclude = true; + for (String pattern : parts) { + if (pattern.startsWith("--")) { + continue; + } + if ("body".equals(pattern) || "+body".equals(pattern)) { + return true; + } else if ("-body".equals(pattern)) { + return false; + } + onlyExclude &= pattern.startsWith("-"); + } + // body is enabled if we only have exclude patterns + return onlyExclude; + } + + private boolean isHeadersEnabled() { + // headers may be enabled unless excluded + String[] parts = filter.split(","); + + boolean onlyExclude = true; + for (String pattern : parts) { + if (pattern.startsWith("--")) { + continue; + } + // if there is individual header filters then we cannot rely on this + if (pattern.startsWith("header:") || pattern.startsWith("+header:") || pattern.startsWith("-header:")) { + return false; + } + if ("headers".equals(pattern) || "+headers".equals(pattern)) { + return true; + } else if ("-headers".equals(pattern)) { + return false; + } + onlyExclude &= pattern.startsWith("-"); + } + // headers is enabled if we only have exclude patterns + return onlyExclude; + } + } diff --git a/camel-core/src/main/java/org/apache/camel/processor/ClaimCheckProcessor.java b/camel-core/src/main/java/org/apache/camel/processor/ClaimCheckProcessor.java index 2421878..cedf95f 100644 --- a/camel-core/src/main/java/org/apache/camel/processor/ClaimCheckProcessor.java +++ b/camel-core/src/main/java/org/apache/camel/processor/ClaimCheckProcessor.java @@ -49,8 +49,7 @@ public class ClaimCheckProcessor extends ServiceSupport implements AsyncProcesso private String operation; private AggregationStrategy aggregationStrategy; private String key; - private String include; - private String exclude; + private String filter; @Override public CamelContext getCamelContext() { @@ -96,20 +95,12 @@ public class ClaimCheckProcessor extends ServiceSupport implements AsyncProcesso this.key = key; } - public String getInclude() { - return include; + public String getFilter() { + return filter; } - public void setInclude(String include) { - this.include = include; - } - - public String getExclude() { - return exclude; - } - - public void setExclude(String exclude) { - this.exclude = exclude; + public void setFilter(String filter) { + this.filter = filter; } public void process(Exchange exchange) throws Exception { @@ -206,8 +197,7 @@ public class ClaimCheckProcessor extends ServiceSupport implements AsyncProcesso protected AggregationStrategy createAggregationStrategy() { ClaimCheckAggregationStrategy answer = new ClaimCheckAggregationStrategy(); - answer.setInclude(include); - answer.setExclude(exclude); + answer.setFilter(filter); return answer; } } diff --git a/camel-core/src/test/java/org/apache/camel/processor/ClaimCheckEipPushPopExcludeBodyTest.java b/camel-core/src/test/java/org/apache/camel/processor/ClaimCheckEipPushPopExcludeBodyTest.java index 6ddbb29..4e0e0fb 100644 --- a/camel-core/src/test/java/org/apache/camel/processor/ClaimCheckEipPushPopExcludeBodyTest.java +++ b/camel-core/src/test/java/org/apache/camel/processor/ClaimCheckEipPushPopExcludeBodyTest.java @@ -29,7 +29,7 @@ public class ClaimCheckEipPushPopExcludeBodyTest extends ContextTestSupport { getMockEndpoint("mock:b").expectedBodiesReceived("Bye World"); getMockEndpoint("mock:b").expectedHeaderReceived("foo", 456); getMockEndpoint("mock:b").expectedHeaderReceived("bar", "Jacks"); - getMockEndpoint("mock:c").expectedBodiesReceived("Hello World"); + getMockEndpoint("mock:c").expectedBodiesReceived("Bye World"); getMockEndpoint("mock:c").expectedHeaderReceived("foo", 123); getMockEndpoint("mock:c").expectedHeaderReceived("bar", "Jacks"); @@ -51,8 +51,8 @@ public class ClaimCheckEipPushPopExcludeBodyTest extends ContextTestSupport { .setHeader("foo", constant(456)) .setHeader("bar", constant("Jacks")) .to("mock:b") - // skip the foo header - .claimCheck(ClaimCheckOperation.Pop, null, null, "header:bar") + // skip the body and bar header + .claimCheck(ClaimCheckOperation.Pop, null, "-body,-header:bar") .to("mock:c"); } }; diff --git a/camel-core/src/test/java/org/apache/camel/processor/ClaimCheckEipPushPopExcludeBodyTest.java b/camel-core/src/test/java/org/apache/camel/processor/ClaimCheckEipPushPopRemoveHeaderTest.java similarity index 83% copy from camel-core/src/test/java/org/apache/camel/processor/ClaimCheckEipPushPopExcludeBodyTest.java copy to camel-core/src/test/java/org/apache/camel/processor/ClaimCheckEipPushPopRemoveHeaderTest.java index 6ddbb29..c164d64 100644 --- a/camel-core/src/test/java/org/apache/camel/processor/ClaimCheckEipPushPopExcludeBodyTest.java +++ b/camel-core/src/test/java/org/apache/camel/processor/ClaimCheckEipPushPopRemoveHeaderTest.java @@ -20,18 +20,18 @@ import org.apache.camel.ContextTestSupport; import org.apache.camel.builder.RouteBuilder; import org.apache.camel.model.ClaimCheckOperation; -public class ClaimCheckEipPushPopExcludeBodyTest extends ContextTestSupport { +public class ClaimCheckEipPushPopRemoveHeaderTest extends ContextTestSupport { - public void testPushPopBodyExclude() throws Exception { + public void testPushPopBodyRemoveHeader() throws Exception { getMockEndpoint("mock:a").expectedBodiesReceived("Hello World"); getMockEndpoint("mock:a").expectedHeaderReceived("foo", 123); getMockEndpoint("mock:a").expectedHeaderReceived("bar", "Moes"); getMockEndpoint("mock:b").expectedBodiesReceived("Bye World"); getMockEndpoint("mock:b").expectedHeaderReceived("foo", 456); getMockEndpoint("mock:b").expectedHeaderReceived("bar", "Jacks"); - getMockEndpoint("mock:c").expectedBodiesReceived("Hello World"); + getMockEndpoint("mock:c").expectedBodiesReceived("Bye World"); getMockEndpoint("mock:c").expectedHeaderReceived("foo", 123); - getMockEndpoint("mock:c").expectedHeaderReceived("bar", "Jacks"); + getMockEndpoint("mock:c").message(0).header("bar").isNull(); template.sendBodyAndHeader("direct:start", "Hello World", "foo", 123); @@ -51,8 +51,8 @@ public class ClaimCheckEipPushPopExcludeBodyTest extends ContextTestSupport { .setHeader("foo", constant(456)) .setHeader("bar", constant("Jacks")) .to("mock:b") - // skip the foo header - .claimCheck(ClaimCheckOperation.Pop, null, null, "header:bar") + // skip the body and remove the bar header + .claimCheck(ClaimCheckOperation.Pop, null, "-body,--header:bar") .to("mock:c"); } }; -- To stop receiving notification emails like this one, please contact davscl...@apache.org.