/**
* 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.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;
import java.util.regex.Pattern;
import com.jayway.jsonpath.Configuration;
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.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);
}
public JsonPathEngine(String expression, boolean suppressExceptions, boolean allowSimple, Option[] options) {
this.expression = expression;
Defaults defaults = DefaultsImpl.INSTANCE;
if (options != null) {
Configuration.ConfigurationBuilder builder = Configuration.builder().jsonProvider(defaults.jsonProvider()).options(options);
if (suppressExceptions) {
builder.options(SUPPRESS_EXCEPTIONS);
}
this.configuration = builder.build();
} else {
Configuration.ConfigurationBuilder builder = Configuration.builder().jsonProvider(defaults.jsonProvider());
if (suppressExceptions) {
builder.options(SUPPRESS_EXCEPTIONS);
}
this.configuration = builder.build();
}
boolean hasSimple = false;
if (allowSimple) {
// is simple language embedded
Matcher matcher = SIMPLE_PATTERN.matcher(expression);
if (matcher.find()) {
hasSimple = true;
}
}
if (hasSimple) {
this.path = null;
} else {
this.path = JsonPath.compile(expression);
LOG.debug("Compiled static JsonPath: {}", expression);
}
}
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);
JsonPath path = JsonPath.compile(text);
LOG.debug("Compiled dynamic JsonPath: {}", expression);
return doRead(path, exchange);
} else {
return doRead(path, exchange);
}
}
private Object doRead(JsonPath path, Exchange exchange) throws IOException, CamelExchangeException {
Object json = exchange.getIn().getBody();
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
InputStream inputStream = new FileInputStream((File) genericFile.getFile());
return path.read(inputStream, genericFile.getCharset(), configuration);
}
}
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 {
// 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
return path.read(is, jsonEncoding, configuration);
} else {
// No json encoding specified --> assume json encoding is unicode and determine the specific unicode encoding according to RFC-4627.
// This is a temporary solution, it can be removed as soon as jsonpath offers the encoding detection
JsonStream jsonStream = new JsonStream(is);
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;
adapter.init(exchange.getContext());
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;
}
}