001/*
002 * Copyright (c) 2006-2011 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the Eclipse Public License v1.0
006 * which accompanies this distribution, and is available at
007 * http://www.eclipse.org/legal/epl-v10.html
008 *
009 * Contributors:
010 *     bstefanescu
011 */
012package org.nuxeo.ecm.automation.client.jaxrs.spi;
013
014import java.io.IOException;
015import java.io.StringWriter;
016import java.lang.reflect.Type;
017import java.util.HashMap;
018import java.util.Map;
019import java.util.Stack;
020import java.util.concurrent.ConcurrentHashMap;
021
022import org.apache.commons.logging.Log;
023import org.apache.commons.logging.LogFactory;
024import org.codehaus.jackson.JsonFactory;
025import org.codehaus.jackson.JsonGenerator;
026import org.codehaus.jackson.JsonNode;
027import org.codehaus.jackson.JsonParser;
028import org.codehaus.jackson.JsonProcessingException;
029import org.codehaus.jackson.JsonToken;
030import org.codehaus.jackson.Version;
031import org.codehaus.jackson.map.DeserializationConfig;
032import org.codehaus.jackson.map.DeserializationContext;
033import org.codehaus.jackson.map.DeserializationProblemHandler;
034import org.codehaus.jackson.map.JsonDeserializer;
035import org.codehaus.jackson.map.ObjectMapper;
036import org.codehaus.jackson.map.annotate.JsonCachable;
037import org.codehaus.jackson.map.deser.BeanDeserializer;
038import org.codehaus.jackson.map.deser.BeanDeserializerModifier;
039import org.codehaus.jackson.map.introspect.BasicBeanDescription;
040import org.codehaus.jackson.map.module.SimpleModule;
041import org.codehaus.jackson.map.type.TypeBindings;
042import org.codehaus.jackson.map.type.TypeFactory;
043import org.codehaus.jackson.map.type.TypeModifier;
044import org.codehaus.jackson.type.JavaType;
045import org.nuxeo.ecm.automation.client.Constants;
046import org.nuxeo.ecm.automation.client.OperationRequest;
047import org.nuxeo.ecm.automation.client.RemoteThrowable;
048import org.nuxeo.ecm.automation.client.jaxrs.impl.AutomationClientActivator;
049import org.nuxeo.ecm.automation.client.jaxrs.spi.marshallers.BooleanMarshaller;
050import org.nuxeo.ecm.automation.client.jaxrs.spi.marshallers.DateMarshaller;
051import org.nuxeo.ecm.automation.client.jaxrs.spi.marshallers.DocumentMarshaller;
052import org.nuxeo.ecm.automation.client.jaxrs.spi.marshallers.DocumentsMarshaller;
053import org.nuxeo.ecm.automation.client.jaxrs.spi.marshallers.ExceptionMarshaller;
054import org.nuxeo.ecm.automation.client.jaxrs.spi.marshallers.LoginMarshaller;
055import org.nuxeo.ecm.automation.client.jaxrs.spi.marshallers.NumberMarshaller;
056import org.nuxeo.ecm.automation.client.jaxrs.spi.marshallers.RecordSetMarshaller;
057import org.nuxeo.ecm.automation.client.jaxrs.spi.marshallers.StringMarshaller;
058import org.nuxeo.ecm.automation.client.jaxrs.util.JsonOperationMarshaller;
059import org.nuxeo.ecm.automation.client.model.OperationDocumentation;
060import org.nuxeo.ecm.automation.client.model.OperationInput;
061import org.nuxeo.ecm.automation.client.model.OperationRegistry;
062import org.nuxeo.ecm.automation.client.model.PropertyMap;
063
064/**
065 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a>
066 */
067public class JsonMarshalling {
068
069    protected static final Log log = LogFactory.getLog(JsonMarshalling.class);
070
071    /**
072     * @author matic
073     * @since 5.5
074     */
075    public static class ThowrableTypeModifier extends TypeModifier {
076        @Override
077        public JavaType modifyType(JavaType type, Type jdkType, TypeBindings context, TypeFactory typeFactory) {
078            Class<?> raw = type.getRawClass();
079            if (raw.equals(Throwable.class)) {
080                return typeFactory.constructType(RemoteThrowable.class);
081            }
082            return type;
083        }
084    }
085
086    @JsonCachable(false)
087    public static class ThrowableDeserializer extends org.codehaus.jackson.map.deser.ThrowableDeserializer {
088
089        protected Stack<Map<String, JsonNode>> unknownStack = new Stack<>();
090
091        public ThrowableDeserializer(BeanDeserializer src) {
092            super(src);
093        }
094
095        @Override
096        public Object deserializeFromObject(JsonParser jp, DeserializationContext ctxt)
097                throws IOException, JsonProcessingException {
098            unknownStack.push(new HashMap<String, JsonNode>());
099            try {
100            RemoteThrowable t = (RemoteThrowable) super.deserializeFromObject(jp, ctxt);
101            t.getOtherNodes().putAll(unknownStack.peek());
102                return t;
103            } finally {
104                unknownStack.pop();
105            }
106        }
107    }
108
109    private JsonMarshalling() {
110    }
111
112    protected static JsonFactory factory = newJsonFactory();
113
114    protected static final Map<String, JsonMarshaller<?>> marshallersByType = new ConcurrentHashMap<String, JsonMarshaller<?>>();
115
116    protected static final Map<Class<?>, JsonMarshaller<?>> marshallersByJavaType = new ConcurrentHashMap<Class<?>, JsonMarshaller<?>>();
117
118    public static JsonFactory getFactory() {
119        return factory;
120    }
121
122    public static JsonFactory newJsonFactory() {
123        JsonFactory jf = new JsonFactory();
124        ObjectMapper oc = new ObjectMapper(jf);
125        final TypeFactory typeFactoryWithModifier = oc.getTypeFactory().withModifier(new ThowrableTypeModifier());
126        oc.setTypeFactory(typeFactoryWithModifier);
127        oc.getDeserializationConfig().addHandler(new DeserializationProblemHandler() {
128            @Override
129            public boolean handleUnknownProperty(DeserializationContext ctxt, JsonDeserializer<?> deserializer,
130                    Object beanOrClass, String propertyName) throws IOException, JsonProcessingException {
131                if (deserializer instanceof ThrowableDeserializer) {
132                    JsonParser jp = ctxt.getParser();
133                    JsonNode propertyNode = jp.readValueAsTree();
134                    ((ThrowableDeserializer) deserializer).unknownStack.peek().put(propertyName, propertyNode);
135                    return true;
136                }
137                return false;
138            }
139        });
140        final SimpleModule module = new SimpleModule("automation", Version.unknownVersion()) {
141
142            @Override
143            public void setupModule(SetupContext context) {
144                super.setupModule(context);
145
146                context.addBeanDeserializerModifier(new BeanDeserializerModifier() {
147
148                    @Override
149                    public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config,
150                            BasicBeanDescription beanDesc, JsonDeserializer<?> deserializer) {
151                        if (!Throwable.class.isAssignableFrom(beanDesc.getBeanClass())) {
152                            return super.modifyDeserializer(config, beanDesc, deserializer);
153                        }
154                        return new ThrowableDeserializer((BeanDeserializer) deserializer);
155                    }
156                });
157            }
158        };
159        oc.registerModule(module);
160        jf.setCodec(oc);
161        return jf;
162    }
163
164    static {
165        addMarshaller(new DocumentMarshaller());
166        addMarshaller(new DocumentsMarshaller());
167        addMarshaller(new ExceptionMarshaller());
168        addMarshaller(new LoginMarshaller());
169        addMarshaller(new RecordSetMarshaller());
170        addMarshaller(new StringMarshaller());
171        addMarshaller(new BooleanMarshaller());
172        addMarshaller(new NumberMarshaller());
173        addMarshaller(new DateMarshaller());
174    }
175
176    public static void addMarshaller(JsonMarshaller<?> marshaller) {
177        marshallersByType.put(marshaller.getType(), marshaller);
178        marshallersByJavaType.put(marshaller.getJavaType(), marshaller);
179    }
180
181    @SuppressWarnings("unchecked")
182    public static <T> JsonMarshaller<T> getMarshaller(String type) {
183        return (JsonMarshaller<T>) marshallersByType.get(type);
184    }
185
186    @SuppressWarnings("unchecked")
187    public static <T> JsonMarshaller<T> getMarshaller(Class<T> clazz) {
188        return (JsonMarshaller<T>) marshallersByJavaType.get(clazz);
189    }
190
191    public static OperationRegistry readRegistry(String content) throws IOException {
192        HashMap<String, OperationDocumentation> ops = new HashMap<String, OperationDocumentation>();
193        HashMap<String, OperationDocumentation> chains = new HashMap<String, OperationDocumentation>();
194        HashMap<String, String> paths = new HashMap<String, String>();
195
196        JsonParser jp = factory.createJsonParser(content);
197        jp.nextToken(); // start_obj
198        JsonToken tok = jp.nextToken();
199        while (tok != null && tok != JsonToken.END_OBJECT) {
200            String key = jp.getCurrentName();
201            if ("operations".equals(key)) {
202                readOperations(jp, ops);
203            } else if ("chains".equals(key)) {
204                readChains(jp, chains);
205            } else if ("paths".equals(key)) {
206                readPaths(jp, paths);
207            }
208            tok = jp.nextToken();
209        }
210        if (tok == null) {
211            throw new IllegalArgumentException("Unexpected end of stream.");
212        }
213        return new OperationRegistry(paths, ops, chains);
214    }
215
216    private static void readOperations(JsonParser jp, Map<String, OperationDocumentation> ops) throws IOException {
217        jp.nextToken(); // skip [
218        JsonToken tok = jp.nextToken();
219        while (tok != null && tok != JsonToken.END_ARRAY) {
220            OperationDocumentation op = JsonOperationMarshaller.read(jp);
221            ops.put(op.id, op);
222            if (op.aliases != null) {
223                for (String alias : op.aliases) {
224                    ops.put(alias, op);
225                }
226            }
227            tok = jp.nextToken();
228        }
229    }
230
231    private static void readChains(JsonParser jp, Map<String, OperationDocumentation> chains) throws IOException {
232        jp.nextToken(); // skip [
233        JsonToken tok = jp.nextToken();
234        while (tok != null && tok != JsonToken.END_ARRAY) {
235            OperationDocumentation op = JsonOperationMarshaller.read(jp);
236            chains.put(op.id, op);
237            tok = jp.nextToken();
238        }
239    }
240
241    private static void readPaths(JsonParser jp, Map<String, String> paths) throws IOException {
242        jp.nextToken(); // skip {
243        JsonToken tok = jp.nextToken();
244        while (tok != null && tok != JsonToken.END_OBJECT) {
245            jp.nextToken();
246            paths.put(jp.getCurrentName(), jp.getText());
247            tok = jp.nextToken();
248        }
249        if (tok == null) {
250            throw new IllegalArgumentException("Unexpected end of stream.");
251        }
252
253    }
254
255    public static Object readEntity(String content) throws IOException {
256        if (content.length() == 0) { // void response
257            return null;
258        }
259        JsonParser jp = factory.createJsonParser(content);
260        jp.nextToken(); // will return JsonToken.START_OBJECT (verify?)
261        jp.nextToken();
262        if (!Constants.KEY_ENTITY_TYPE.equals(jp.getText())) {
263            throw new RuntimeException("unuspported respone type. No entity-type key found at top of the object");
264        }
265        jp.nextToken();
266        String etype = jp.getText();
267        JsonMarshaller<?> jm = getMarshaller(etype);
268        if (jm == null) {
269            // fall-back on generic java class loading in case etype matches a
270            // valid class name
271            try {
272                // Introspect bundle context to load marshalling class
273                AutomationClientActivator automationClientActivator = AutomationClientActivator.getInstance();
274                Class<?> loadClass;
275                // Java mode or OSGi mode
276                if (automationClientActivator == null) {
277                    loadClass = Thread.currentThread().getContextClassLoader().loadClass(etype);
278                } else {
279                    loadClass = automationClientActivator.getContext().getBundle().loadClass(etype);
280                }
281                ObjectMapper mapper = new ObjectMapper();
282                jp.nextToken(); // move to next field
283                jp.nextToken(); // value field name
284                jp.nextToken(); // value field content
285                return mapper.readValue(jp, loadClass);
286            } catch (ClassNotFoundException e) {
287                log.warn("No marshaller for " + etype + " and not a valid Java class name either.");
288                jp = factory.createJsonParser(content);
289                return jp.readValueAsTree();
290            }
291        }
292        return jm.read(jp);
293    }
294
295    public static String writeRequest(OperationRequest req) throws IOException {
296        StringWriter writer = new StringWriter();
297        Object input = req.getInput();
298        JsonGenerator jg = factory.createJsonGenerator(writer);
299        jg.writeStartObject();
300        if (input instanceof OperationInput) {
301            // Custom String serialization
302            OperationInput operationInput = (OperationInput) input;
303            String ref = operationInput.getInputRef();
304            if (ref != null) {
305                jg.writeStringField("input", ref);
306            }
307        } else if (input != null) {
308
309            JsonMarshaller<?> marshaller = getMarshaller(input.getClass());
310            if (marshaller != null) {
311                // use the registered marshaller for this type
312                jg.writeFieldName("input");
313                marshaller.write(jg, input);
314            } else {
315                // fall-back to direct POJO to JSON mapping
316                jg.writeObjectField("input", input);
317            }
318        }
319        jg.writeObjectFieldStart("params");
320        writeMap(jg, req.getParameters());
321        jg.writeEndObject();
322        jg.writeObjectFieldStart("context");
323        writeMap(jg, req.getContextParameters());
324        jg.writeEndObject();
325        jg.writeEndObject();
326        jg.close();
327        return writer.toString();
328    }
329
330    public static void writeMap(JsonGenerator jg, Map<String, Object> map) throws IOException {
331        for (Map.Entry<String, Object> entry : map.entrySet()) {
332            Object param = entry.getValue();
333            jg.writeFieldName(entry.getKey());
334            write(jg, param);
335        }
336    }
337
338    public static void write(JsonGenerator jg, Object obj) throws IOException {
339        if (obj != null) {
340            JsonMarshaller<?> marshaller = getMarshaller(obj.getClass());
341            if (marshaller != null) {
342                try {
343                    marshaller.write(jg, obj);
344                } catch (UnsupportedOperationException e) {
345                    // Catch this exception to handle builtin marshaller exceptions
346                    jg.writeObject(obj);
347                }
348            } else if (obj instanceof String) {
349                jg.writeString((String) obj);
350            } else if (obj instanceof PropertyMap || obj instanceof OperationInput) {
351                jg.writeString(obj.toString());
352            } else if (obj instanceof Iterable) {
353                jg.writeStartArray();
354                for (Object object : (Iterable) obj) {
355                    write(jg, object);
356                }
357                jg.writeEndArray();
358            } else if (obj.getClass().isArray()) {
359                jg.writeStartArray();
360                for (Object object : (Object[]) obj) {
361                    write(jg, object);
362                }
363                jg.writeEndArray();
364            } else {
365                jg.writeObject(obj);
366            }
367        }
368    }
369
370}