CAMEL-10646: jsonpath now supports POJOs if jackson is on the classpath
Project: http://git-wip-us.apache.org/repos/asf/camel/repo Commit: http://git-wip-us.apache.org/repos/asf/camel/commit/0a3efe21 Tree: http://git-wip-us.apache.org/repos/asf/camel/tree/0a3efe21 Diff: http://git-wip-us.apache.org/repos/asf/camel/diff/0a3efe21 Branch: refs/heads/master Commit: 0a3efe214aa6bb6b6e6c304444595f0ae30c72b3 Parents: 2baea25 Author: Claus Ibsen <davscl...@apache.org> Authored: Fri Dec 23 12:54:24 2016 +0100 Committer: Claus Ibsen <davscl...@apache.org> Committed: Fri Dec 23 12:54:24 2016 +0100 ---------------------------------------------------------------------- components/camel-jsonpath/pom.xml | 20 +++- .../apache/camel/jsonpath/JsonPathAdapter.java | 33 +++++++ .../apache/camel/jsonpath/JsonPathEngine.java | 97 ++++++++++++++++++-- .../jsonpath/jackson/JacksonJsonAdapter.java | 64 +++++++++++++ .../jsonpath/JsonPathPojoTransformTest.java | 51 ++++++++++ .../org/apache/camel/jsonpath/MyPojoType.java | 39 ++++++++ 6 files changed, 295 insertions(+), 9 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/camel/blob/0a3efe21/components/camel-jsonpath/pom.xml ---------------------------------------------------------------------- diff --git a/components/camel-jsonpath/pom.xml b/components/camel-jsonpath/pom.xml index 499e99c..b20deb3 100644 --- a/components/camel-jsonpath/pom.xml +++ b/components/camel-jsonpath/pom.xml @@ -31,8 +31,16 @@ <description>Camel JSON Path Language</description> <properties> - <camel.osgi.export.pkg>org.apache.camel.jsonpath.*</camel.osgi.export.pkg> + <camel.osgi.export.pkg> + org.apache.camel.jsonpath, + !org.apache.camel.jsonpath.jackson + </camel.osgi.export.pkg> <camel.osgi.export.service>org.apache.camel.spi.LanguageResolver;language=jsonpath</camel.osgi.export.service> + <camel.osgi.import> + com.fasterxml.jackson.databind;resolution:=optional, + com.fasterxml.jackson.module.jaxb;resolution:=optional, + * + </camel.osgi.import> </properties> <dependencies> @@ -50,6 +58,16 @@ <artifactId>json-smart</artifactId> <version>${json-smart-version}</version> </dependency> + <dependency> + <groupId>com.fasterxml.jackson.core</groupId> + <artifactId>jackson-databind</artifactId> + <optional>true</optional> + </dependency> + <dependency> + <groupId>com.fasterxml.jackson.module</groupId> + <artifactId>jackson-module-jaxb-annotations</artifactId> + <optional>true</optional> + </dependency> <!-- testing --> <dependency> http://git-wip-us.apache.org/repos/asf/camel/blob/0a3efe21/components/camel-jsonpath/src/main/java/org/apache/camel/jsonpath/JsonPathAdapter.java ---------------------------------------------------------------------- diff --git a/components/camel-jsonpath/src/main/java/org/apache/camel/jsonpath/JsonPathAdapter.java b/components/camel-jsonpath/src/main/java/org/apache/camel/jsonpath/JsonPathAdapter.java new file mode 100644 index 0000000..41e0903 --- /dev/null +++ b/components/camel-jsonpath/src/main/java/org/apache/camel/jsonpath/JsonPathAdapter.java @@ -0,0 +1,33 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.camel.jsonpath; + +import java.util.Map; + +import org.apache.camel.Exchange; + +public interface JsonPathAdapter { + + /** + * Attempt to read/convert the message body into a {@link Map} type + * + * @param body the message body + * @param exchange the Camel exchange + * @return converted as {@link Map} or <tt>null</tt> if not possible + */ + Map readValue(Object body, Exchange exchange); +} http://git-wip-us.apache.org/repos/asf/camel/blob/0a3efe21/components/camel-jsonpath/src/main/java/org/apache/camel/jsonpath/JsonPathEngine.java ---------------------------------------------------------------------- diff --git a/components/camel-jsonpath/src/main/java/org/apache/camel/jsonpath/JsonPathEngine.java b/components/camel-jsonpath/src/main/java/org/apache/camel/jsonpath/JsonPathEngine.java index 14071d7..0816e30 100644 --- a/components/camel-jsonpath/src/main/java/org/apache/camel/jsonpath/JsonPathEngine.java +++ b/components/camel-jsonpath/src/main/java/org/apache/camel/jsonpath/JsonPathEngine.java @@ -20,6 +20,7 @@ import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.regex.Matcher; @@ -30,22 +31,29 @@ import com.jayway.jsonpath.Configuration.Defaults; import com.jayway.jsonpath.JsonPath; import com.jayway.jsonpath.Option; import com.jayway.jsonpath.internal.DefaultsImpl; - +import org.apache.camel.CamelExchangeException; import org.apache.camel.Exchange; import org.apache.camel.Expression; -import org.apache.camel.InvalidPayloadException; import org.apache.camel.component.file.GenericFile; +import org.apache.camel.util.ObjectHelper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import static com.jayway.jsonpath.Option.ALWAYS_RETURN_LIST; +import static com.jayway.jsonpath.Option.SUPPRESS_EXCEPTIONS; + public class JsonPathEngine { private static final Logger LOG = LoggerFactory.getLogger(JsonPathEngine.class); + private static final String JACKSON_JSON_ADAPTER = "org.apache.camel.jsonpath.jackson.JacksonJsonAdapter"; + private static final Pattern SIMPLE_PATTERN = Pattern.compile("\\$\\{[^\\}]+\\}", Pattern.MULTILINE); private final String expression; private final JsonPath path; private final Configuration configuration; + private JsonPathAdapter adapter; + private volatile boolean initJsonAdapter; public JsonPathEngine(String expression) { this(expression, false, true, null); @@ -58,13 +66,13 @@ public class JsonPathEngine { if (options != null) { Configuration.ConfigurationBuilder builder = Configuration.builder().jsonProvider(defaults.jsonProvider()).options(options); if (suppressExceptions) { - builder.options(Option.SUPPRESS_EXCEPTIONS); + builder.options(SUPPRESS_EXCEPTIONS); } this.configuration = builder.build(); } else { Configuration.ConfigurationBuilder builder = Configuration.builder().jsonProvider(defaults.jsonProvider()); if (suppressExceptions) { - builder.options(Option.SUPPRESS_EXCEPTIONS); + builder.options(SUPPRESS_EXCEPTIONS); } this.configuration = builder.build(); } @@ -85,7 +93,7 @@ public class JsonPathEngine { } } - public Object read(Exchange exchange) throws IOException, InvalidPayloadException { + public Object read(Exchange exchange) throws Exception { if (path == null) { Expression exp = exchange.getContext().resolveLanguage("simple").createExpression(expression); String text = exp.evaluate(exchange, String.class); @@ -97,10 +105,13 @@ public class JsonPathEngine { } } - private Object doRead(JsonPath path, Exchange exchange) throws IOException, InvalidPayloadException { + private Object doRead(JsonPath path, Exchange exchange) throws IOException, CamelExchangeException { Object json = exchange.getIn().getBody(); - if (json instanceof GenericFile) { + if (json instanceof InputStream) { + return readWithInputStream(path, exchange); + } else if (json instanceof GenericFile) { + LOG.trace("JSonPath: {} is read as generic file from message body: {}", path, json); GenericFile<?> genericFile = (GenericFile<?>) json; if (genericFile.getCharset() != null) { // special treatment for generic file with charset @@ -110,16 +121,48 @@ public class JsonPathEngine { } if (json instanceof String) { + LOG.trace("JSonPath: {} is read as String from message body: {}", path, json); String str = (String) json; return path.read(str, configuration); } else if (json instanceof Map) { + LOG.trace("JSonPath: {} is read as Map from message body: {}", path, json); Map map = (Map) json; return path.read(map, configuration); } else if (json instanceof List) { + LOG.trace("JSonPath: {} is read as List from message body: {}", path, json); List list = (List) json; return path.read(list, configuration); } else { - InputStream is = exchange.getIn().getMandatoryBody(InputStream.class); + // can we find an adapter which can read the message body + Object answer = readWithAdapter(path, exchange); + if (answer == null) { + // fallback and attempt input stream for any other types + answer = readWithInputStream(path, exchange); + } + if (answer != null) { + return answer; + } + } + + // is json path configured to suppress exceptions + if (configuration.getOptions().contains(SUPPRESS_EXCEPTIONS)) { + if (configuration.getOptions().contains(ALWAYS_RETURN_LIST)) { + return Collections.emptyList(); + } else { + return null; + } + } + + // okay it was not then lets throw a failure + throw new CamelExchangeException("Cannot read message body as supported JSon value", exchange); + } + + private Object readWithInputStream(JsonPath path, Exchange exchange) throws IOException { + Object json = exchange.getIn().getBody(); + LOG.trace("JSonPath: {} is read as InputStream from message body: {}", path, json); + + InputStream is = exchange.getContext().getTypeConverter().tryConvertTo(InputStream.class, exchange, json); + if (is != null) { String jsonEncoding = exchange.getIn().getHeader(JsonPathConstants.HEADER_JSON_ENCODING, String.class); if (jsonEncoding != null) { // json encoding specified in header @@ -131,5 +174,43 @@ public class JsonPathEngine { return path.read(jsonStream, jsonStream.getEncoding().name(), configuration); } } + + return null; + } + + private Object readWithAdapter(JsonPath path, Exchange exchange) { + Object json = exchange.getIn().getBody(); + LOG.trace("JSonPath: {} is read with adapter from message body: {}", path, json); + + if (!initJsonAdapter) { + try { + // need to load this adapter dynamically as its optional + LOG.debug("Attempting to enable JacksonJsonAdapter by resolving: {} from classpath", JACKSON_JSON_ADAPTER); + Class<?> clazz = exchange.getContext().getClassResolver().resolveClass(JACKSON_JSON_ADAPTER); + if (clazz != null) { + Object obj = exchange.getContext().getInjector().newInstance(clazz); + if (obj instanceof JsonPathAdapter) { + adapter = (JsonPathAdapter) obj; + LOG.debug("JacksonJsonAdapter found on classpath and enabled for camel-jsonpath: {}", adapter); + } + } + } catch (Throwable e) { + // ignore + } + initJsonAdapter = true; + } + + if (adapter != null) { + LOG.trace("Attempting to use JacksonJsonAdapter: {}", adapter); + Map map = adapter.readValue(json, exchange); + if (map != null) { + if (LOG.isDebugEnabled()) { + LOG.debug("JacksonJsonAdapter converted message body from: {} to: java.util.Map", ObjectHelper.classCanonicalName(json)); + } + return path.read(map, configuration); + } + } + + return null; } } http://git-wip-us.apache.org/repos/asf/camel/blob/0a3efe21/components/camel-jsonpath/src/main/java/org/apache/camel/jsonpath/jackson/JacksonJsonAdapter.java ---------------------------------------------------------------------- diff --git a/components/camel-jsonpath/src/main/java/org/apache/camel/jsonpath/jackson/JacksonJsonAdapter.java b/components/camel-jsonpath/src/main/java/org/apache/camel/jsonpath/jackson/JacksonJsonAdapter.java new file mode 100644 index 0000000..f109628 --- /dev/null +++ b/components/camel-jsonpath/src/main/java/org/apache/camel/jsonpath/jackson/JacksonJsonAdapter.java @@ -0,0 +1,64 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.camel.jsonpath.jackson; + +import java.util.Map; +import java.util.Set; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.module.jaxb.JaxbAnnotationModule; +import org.apache.camel.Exchange; +import org.apache.camel.jsonpath.JsonPathAdapter; +import org.apache.camel.spi.Registry; + +/** + * A Jackson {@link JsonPathAdapter} which is using Jackson to convert the message + * body to {@link Map}. This allows us to support POJO classes with camel-jsonpath. + */ +public class JacksonJsonAdapter implements JsonPathAdapter { + + private final ObjectMapper defaultMapper; + + public JacksonJsonAdapter() { + defaultMapper = new ObjectMapper(); + // Enables JAXB processing so we can easily convert JAXB annotated pojos also + JaxbAnnotationModule module = new JaxbAnnotationModule(); + defaultMapper.registerModule(module); + } + + @Override + public Map readValue(Object body, Exchange exchange) { + ObjectMapper mapper = resolveObjectMapper(exchange.getContext().getRegistry()); + try { + return mapper.convertValue(body, Map.class); + } catch (Throwable e) { + // ignore because we are attempting to convert + } + + return null; + } + + private ObjectMapper resolveObjectMapper(Registry registry) { + Set<ObjectMapper> mappers = registry.findByType(ObjectMapper.class); + if (mappers.size() == 1) { + return mappers.iterator().next(); + } + return defaultMapper; + } + +} + http://git-wip-us.apache.org/repos/asf/camel/blob/0a3efe21/components/camel-jsonpath/src/test/java/org/apache/camel/jsonpath/JsonPathPojoTransformTest.java ---------------------------------------------------------------------- diff --git a/components/camel-jsonpath/src/test/java/org/apache/camel/jsonpath/JsonPathPojoTransformTest.java b/components/camel-jsonpath/src/test/java/org/apache/camel/jsonpath/JsonPathPojoTransformTest.java new file mode 100644 index 0000000..209bff2 --- /dev/null +++ b/components/camel-jsonpath/src/test/java/org/apache/camel/jsonpath/JsonPathPojoTransformTest.java @@ -0,0 +1,51 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.camel.jsonpath; + +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.test.junit4.CamelTestSupport; +import org.junit.Test; + +public class JsonPathPojoTransformTest extends CamelTestSupport { + + @Override + protected RouteBuilder createRouteBuilder() throws Exception { + return new RouteBuilder() { + @Override + public void configure() throws Exception { + from("direct:start") + .transform().jsonpath("$.type") + .to("mock:type"); + } + }; + } + + @Test + public void testPojo() throws Exception { + getMockEndpoint("mock:type").expectedBodiesReceived("customer"); + + // this will be read using jackson if its on the classpath + MyPojoType pojo = new MyPojoType(); + pojo.setKind("full"); + pojo.setType("customer"); + + template.sendBody("direct:start", pojo); + + assertMockEndpointsSatisfied(); + } + +} http://git-wip-us.apache.org/repos/asf/camel/blob/0a3efe21/components/camel-jsonpath/src/test/java/org/apache/camel/jsonpath/MyPojoType.java ---------------------------------------------------------------------- diff --git a/components/camel-jsonpath/src/test/java/org/apache/camel/jsonpath/MyPojoType.java b/components/camel-jsonpath/src/test/java/org/apache/camel/jsonpath/MyPojoType.java new file mode 100644 index 0000000..243e648 --- /dev/null +++ b/components/camel-jsonpath/src/test/java/org/apache/camel/jsonpath/MyPojoType.java @@ -0,0 +1,39 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.camel.jsonpath; + +public class MyPojoType { + + private String kind; + private String type; + + public String getKind() { + return kind; + } + + public void setKind(String kind) { + this.kind = kind; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } +}