/*
* (C) Copyright 2006-2011 Nuxeo SA (http://nuxeo.com/) and others.
*
* Licensed 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.
*
* Contributors:
* bstefanescu
*/
package org.nuxeo.ecm.automation.client.jaxrs.spi;
import java.io.IOException;
import java.io.StringWriter;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.Map;
import java.util.Stack;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.codehaus.jackson.JsonFactory;
import org.codehaus.jackson.JsonGenerator;
import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.JsonParser;
import org.codehaus.jackson.JsonProcessingException;
import org.codehaus.jackson.JsonToken;
import org.codehaus.jackson.Version;
import org.codehaus.jackson.map.DeserializationConfig;
import org.codehaus.jackson.map.DeserializationContext;
import org.codehaus.jackson.map.DeserializationProblemHandler;
import org.codehaus.jackson.map.JsonDeserializer;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.annotate.JsonCachable;
import org.codehaus.jackson.map.deser.BeanDeserializer;
import org.codehaus.jackson.map.deser.BeanDeserializerModifier;
import org.codehaus.jackson.map.introspect.BasicBeanDescription;
import org.codehaus.jackson.map.module.SimpleModule;
import org.codehaus.jackson.map.type.TypeBindings;
import org.codehaus.jackson.map.type.TypeFactory;
import org.codehaus.jackson.map.type.TypeModifier;
import org.codehaus.jackson.type.JavaType;
import org.nuxeo.ecm.automation.client.Constants;
import org.nuxeo.ecm.automation.client.OperationRequest;
import org.nuxeo.ecm.automation.client.RemoteThrowable;
import org.nuxeo.ecm.automation.client.jaxrs.impl.AutomationClientActivator;
import org.nuxeo.ecm.automation.client.jaxrs.spi.marshallers.BooleanMarshaller;
import org.nuxeo.ecm.automation.client.jaxrs.spi.marshallers.DateMarshaller;
import org.nuxeo.ecm.automation.client.jaxrs.spi.marshallers.DocumentMarshaller;
import org.nuxeo.ecm.automation.client.jaxrs.spi.marshallers.DocumentsMarshaller;
import org.nuxeo.ecm.automation.client.jaxrs.spi.marshallers.ExceptionMarshaller;
import org.nuxeo.ecm.automation.client.jaxrs.spi.marshallers.LoginMarshaller;
import org.nuxeo.ecm.automation.client.jaxrs.spi.marshallers.NumberMarshaller;
import org.nuxeo.ecm.automation.client.jaxrs.spi.marshallers.RecordSetMarshaller;
import org.nuxeo.ecm.automation.client.jaxrs.spi.marshallers.StringMarshaller;
import org.nuxeo.ecm.automation.client.jaxrs.util.JsonOperationMarshaller;
import org.nuxeo.ecm.automation.client.model.OperationDocumentation;
import org.nuxeo.ecm.automation.client.model.OperationInput;
import org.nuxeo.ecm.automation.client.model.OperationRegistry;
import org.nuxeo.ecm.automation.client.model.PropertyMap;
/**
* @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a>
*/
public class JsonMarshalling {
protected static final Log log = LogFactory.getLog(JsonMarshalling.class);
/**
* @author matic
* @since 5.5
*/
public static class ThowrableTypeModifier extends TypeModifier {
@Override
public JavaType modifyType(JavaType type, Type jdkType, TypeBindings context, TypeFactory typeFactory) {
Class<?> raw = type.getRawClass();
if (raw.equals(Throwable.class)) {
return typeFactory.constructType(RemoteThrowable.class);
}
return type;
}
}
@JsonCachable(false)
public static class ThrowableDeserializer extends org.codehaus.jackson.map.deser.ThrowableDeserializer {
protected Stack<Map<String, JsonNode>> unknownStack = new Stack<>();
public ThrowableDeserializer(BeanDeserializer src) {
super(src);
}
@Override
public Object deserializeFromObject(JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
unknownStack.push(new HashMap<String, JsonNode>());
try {
RemoteThrowable t = (RemoteThrowable) super.deserializeFromObject(jp, ctxt);
t.getOtherNodes().putAll(unknownStack.peek());
return t;
} finally {
unknownStack.pop();
}
}
}
private JsonMarshalling() {
}
protected static JsonFactory factory = newJsonFactory();
protected static final Map<String, JsonMarshaller<?>> marshallersByType = new ConcurrentHashMap<String, JsonMarshaller<?>>();
protected static final Map<Class<?>, JsonMarshaller<?>> marshallersByJavaType = new ConcurrentHashMap<Class<?>, JsonMarshaller<?>>();
public static JsonFactory getFactory() {
return factory;
}
public static JsonFactory newJsonFactory() {
JsonFactory jf = new JsonFactory();
ObjectMapper oc = new ObjectMapper(jf);
final TypeFactory typeFactoryWithModifier = oc.getTypeFactory().withModifier(new ThowrableTypeModifier());
oc.setTypeFactory(typeFactoryWithModifier);
oc.getDeserializationConfig().addHandler(new DeserializationProblemHandler() {
@Override
public boolean handleUnknownProperty(DeserializationContext ctxt, JsonDeserializer<?> deserializer,
Object beanOrClass, String propertyName) throws IOException, JsonProcessingException {
if (deserializer instanceof ThrowableDeserializer) {
JsonParser jp = ctxt.getParser();
JsonNode propertyNode = jp.readValueAsTree();
((ThrowableDeserializer) deserializer).unknownStack.peek().put(propertyName, propertyNode);
return true;
}
return false;
}
});
final SimpleModule module = new SimpleModule("automation", Version.unknownVersion()) {
@Override
public void setupModule(SetupContext context) {
super.setupModule(context);
context.addBeanDeserializerModifier(new BeanDeserializerModifier() {
@Override
public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config,
BasicBeanDescription beanDesc, JsonDeserializer<?> deserializer) {
if (!Throwable.class.isAssignableFrom(beanDesc.getBeanClass())) {
return super.modifyDeserializer(config, beanDesc, deserializer);
}
return new ThrowableDeserializer((BeanDeserializer) deserializer);
}
});
}
};
oc.registerModule(module);
jf.setCodec(oc);
return jf;
}
static {
addMarshaller(new DocumentMarshaller());
addMarshaller(new DocumentsMarshaller());
addMarshaller(new ExceptionMarshaller());
addMarshaller(new LoginMarshaller());
addMarshaller(new RecordSetMarshaller());
addMarshaller(new StringMarshaller());
addMarshaller(new BooleanMarshaller());
addMarshaller(new NumberMarshaller());
addMarshaller(new DateMarshaller());
}
public static void addMarshaller(JsonMarshaller<?> marshaller) {
marshallersByType.put(marshaller.getType(), marshaller);
marshallersByJavaType.put(marshaller.getJavaType(), marshaller);
}
@SuppressWarnings("unchecked")
public static <T> JsonMarshaller<T> getMarshaller(String type) {
return (JsonMarshaller<T>) marshallersByType.get(type);
}
@SuppressWarnings("unchecked")
public static <T> JsonMarshaller<T> getMarshaller(Class<T> clazz) {
return (JsonMarshaller<T>) marshallersByJavaType.get(clazz);
}
public static OperationRegistry readRegistry(String content) throws IOException {
HashMap<String, OperationDocumentation> ops = new HashMap<String, OperationDocumentation>();
HashMap<String, OperationDocumentation> chains = new HashMap<String, OperationDocumentation>();
HashMap<String, String> paths = new HashMap<String, String>();
JsonParser jp = factory.createJsonParser(content);
jp.nextToken(); // start_obj
JsonToken tok = jp.nextToken();
while (tok != null && tok != JsonToken.END_OBJECT) {
String key = jp.getCurrentName();
if ("operations".equals(key)) {
readOperations(jp, ops);
} else if ("chains".equals(key)) {
readChains(jp, chains);
} else if ("paths".equals(key)) {
readPaths(jp, paths);
}
tok = jp.nextToken();
}
if (tok == null) {
throw new IllegalArgumentException("Unexpected end of stream.");
}
return new OperationRegistry(paths, ops, chains);
}
private static void readOperations(JsonParser jp, Map<String, OperationDocumentation> ops) throws IOException {
jp.nextToken(); // skip [
JsonToken tok = jp.nextToken();
while (tok != null && tok != JsonToken.END_ARRAY) {
OperationDocumentation op = JsonOperationMarshaller.read(jp);
ops.put(op.id, op);
if (op.aliases != null) {
for (String alias : op.aliases) {
ops.put(alias, op);
}
}
tok = jp.nextToken();
}
}
private static void readChains(JsonParser jp, Map<String, OperationDocumentation> chains) throws IOException {
jp.nextToken(); // skip [
JsonToken tok = jp.nextToken();
while (tok != null && tok != JsonToken.END_ARRAY) {
OperationDocumentation op = JsonOperationMarshaller.read(jp);
chains.put(op.id, op);
tok = jp.nextToken();
}
}
private static void readPaths(JsonParser jp, Map<String, String> paths) throws IOException {
jp.nextToken(); // skip {
JsonToken tok = jp.nextToken();
while (tok != null && tok != JsonToken.END_OBJECT) {
jp.nextToken();
paths.put(jp.getCurrentName(), jp.getText());
tok = jp.nextToken();
}
if (tok == null) {
throw new IllegalArgumentException("Unexpected end of stream.");
}
}
public static Object readEntity(String content) throws IOException {
if (content.length() == 0) { // void response
return null;
}
JsonParser jp = factory.createJsonParser(content);
jp.nextToken(); // will return JsonToken.START_OBJECT (verify?)
jp.nextToken();
if (!Constants.KEY_ENTITY_TYPE.equals(jp.getText())) {
throw new RuntimeException("unuspported respone type. No entity-type key found at top of the object");
}
jp.nextToken();
String etype = jp.getText();
JsonMarshaller<?> jm = getMarshaller(etype);
if (jm == null) {
// fall-back on generic java class loading in case etype matches a
// valid class name
try {
// Introspect bundle context to load marshalling class
AutomationClientActivator automationClientActivator = AutomationClientActivator.getInstance();
Class<?> loadClass;
// Java mode or OSGi mode
if (automationClientActivator == null) {
loadClass = Thread.currentThread().getContextClassLoader().loadClass(etype);
} else {
loadClass = automationClientActivator.getContext().getBundle().loadClass(etype);
}
ObjectMapper mapper = new ObjectMapper();
jp.nextToken(); // move to next field
jp.nextToken(); // value field name
jp.nextToken(); // value field content
return mapper.readValue(jp, loadClass);
} catch (ClassNotFoundException e) {
log.warn("No marshaller for " + etype + " and not a valid Java class name either.");
jp = factory.createJsonParser(content);
return jp.readValueAsTree();
}
}
return jm.read(jp);
}
public static String writeRequest(OperationRequest req) throws IOException {
StringWriter writer = new StringWriter();
Object input = req.getInput();
JsonGenerator jg = factory.createJsonGenerator(writer);
jg.writeStartObject();
if (input instanceof OperationInput) {
// Custom String serialization
OperationInput operationInput = (OperationInput) input;
String ref = operationInput.getInputRef();
if (ref != null) {
jg.writeStringField("input", ref);
}
} else if (input != null) {
JsonMarshaller<?> marshaller = getMarshaller(input.getClass());
if (marshaller != null) {
// use the registered marshaller for this type
jg.writeFieldName("input");
marshaller.write(jg, input);
} else {
// fall-back to direct POJO to JSON mapping
jg.writeObjectField("input", input);
}
}
jg.writeObjectFieldStart("params");
writeMap(jg, req.getParameters());
jg.writeEndObject();
jg.writeObjectFieldStart("context");
writeMap(jg, req.getContextParameters());
jg.writeEndObject();
jg.writeEndObject();
jg.close();
return writer.toString();
}
public static void writeMap(JsonGenerator jg, Map<String, Object> map) throws IOException {
for (Map.Entry<String, Object> entry : map.entrySet()) {
Object param = entry.getValue();
jg.writeFieldName(entry.getKey());
write(jg, param);
}
}
public static void write(JsonGenerator jg, Object obj) throws IOException {
if (obj != null) {
JsonMarshaller<?> marshaller = getMarshaller(obj.getClass());
if (marshaller != null) {
try {
marshaller.write(jg, obj);
} catch (UnsupportedOperationException e) {
// Catch this exception to handle builtin marshaller exceptions
jg.writeObject(obj);
}
} else if (obj instanceof String) {
jg.writeString((String) obj);
} else if (obj instanceof PropertyMap || obj instanceof OperationInput) {
jg.writeString(obj.toString());
} else if (obj instanceof Iterable) {
jg.writeStartArray();
for (Object object : (Iterable) obj) {
write(jg, object);
}
jg.writeEndArray();
} else if (obj.getClass().isArray()) {
jg.writeStartArray();
for (Object object : (Object[]) obj) {
write(jg, object);
}
jg.writeEndArray();
} else {
jg.writeObject(obj);
}
} else {
jg.writeNull();
}
}
}