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