/* * (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(); } } }