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