001/*
002 * (C) Copyright 2006-2013 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 *     vpasquier
019 *     slacoin
020 */
021package org.nuxeo.ecm.automation.io.services.codec;
022
023import java.io.ByteArrayInputStream;
024import java.io.ByteArrayOutputStream;
025import java.io.IOException;
026import java.io.InputStream;
027import java.io.OutputStream;
028import java.util.ArrayList;
029import java.util.Calendar;
030import java.util.Collection;
031import java.util.Date;
032import java.util.HashMap;
033import java.util.Iterator;
034import java.util.LinkedHashMap;
035import java.util.List;
036import java.util.Map;
037
038import org.apache.commons.logging.Log;
039import org.apache.commons.logging.LogFactory;
040import org.codehaus.jackson.JsonEncoding;
041import org.codehaus.jackson.JsonFactory;
042import org.codehaus.jackson.JsonGenerator;
043import org.codehaus.jackson.JsonNode;
044import org.codehaus.jackson.JsonParser;
045import org.codehaus.jackson.JsonToken;
046import org.codehaus.jackson.map.ObjectMapper;
047import org.nuxeo.ecm.automation.core.operations.business.adapter.BusinessAdapter;
048import org.nuxeo.ecm.core.api.CoreSession;
049import org.nuxeo.ecm.core.api.DataModel;
050import org.nuxeo.ecm.core.api.DocumentModel;
051import org.nuxeo.ecm.core.api.DocumentModelFactory;
052import org.nuxeo.ecm.core.api.IdRef;
053import org.nuxeo.ecm.core.api.adapter.DocumentAdapterDescriptor;
054import org.nuxeo.ecm.core.api.adapter.DocumentAdapterService;
055import org.nuxeo.ecm.core.schema.utils.DateParser;
056import org.nuxeo.runtime.api.Framework;
057
058/**
059 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a>
060 */
061public class ObjectCodecService {
062
063    protected static final Log log = LogFactory.getLog(ObjectCodecService.class);
064
065    protected Map<Class<?>, ObjectCodec<?>> codecs;
066
067    protected Map<String, ObjectCodec<?>> codecsByName;
068
069    protected Map<Class<?>, ObjectCodec<?>> _codecs;
070
071    protected Map<String, ObjectCodec<?>> _codecsByName;
072
073    private JsonFactory jsonFactory;
074
075    public ObjectCodecService(JsonFactory jsonFactory) {
076        this.jsonFactory = jsonFactory;
077        codecs = new HashMap<Class<?>, ObjectCodec<?>>();
078        codecsByName = new HashMap<String, ObjectCodec<?>>();
079        init();
080    }
081
082    protected void init() {
083        new StringCodec().register(this);
084        new DateCodec().register(this);
085        new CalendarCodec().register(this);
086        new BooleanCodec().register(this);
087        new NumberCodec().register(this);
088    }
089
090    public void postInit() {
091        DocumentAdapterCodec.register(this, Framework.getLocalService(DocumentAdapterService.class));
092    }
093
094    /**
095     * Get all codecs.
096     */
097    public Collection<ObjectCodec<?>> getCodecs() {
098        return codecs().values();
099    }
100
101    public synchronized void addCodec(ObjectCodec<?> codec) {
102        codecs.put(codec.getJavaType(), codec);
103        codecsByName.put(codec.getType(), codec);
104        _codecs = null;
105        _codecsByName = null;
106    }
107
108    public synchronized void removeCodec(String name) {
109        ObjectCodec<?> codec = codecsByName.remove(name);
110        if (codec != null) {
111            codecs.remove(codec.getJavaType());
112            _codecs = null;
113            _codecsByName = null;
114        }
115    }
116
117    public synchronized void removeCodec(Class<?> objectType) {
118        ObjectCodec<?> codec = codecs.remove(objectType);
119        if (codec != null) {
120            codecsByName.remove(codec.getType());
121            _codecs = null;
122            _codecsByName = null;
123        }
124    }
125
126    public ObjectCodec<?> getCodec(Class<?> objectType) {
127        return codecs().get(objectType);
128    }
129
130    public ObjectCodec<?> getCodec(String name) {
131        return codecsByName().get(name);
132    }
133
134    public Map<Class<?>, ObjectCodec<?>> codecs() {
135        Map<Class<?>, ObjectCodec<?>> cache = _codecs;
136        if (cache == null) {
137            synchronized (this) {
138                _codecs = new HashMap<Class<?>, ObjectCodec<?>>(codecs);
139                cache = _codecs;
140            }
141        }
142        return cache;
143    }
144
145    public Map<String, ObjectCodec<?>> codecsByName() {
146        Map<String, ObjectCodec<?>> cache = _codecsByName;
147        if (cache == null) {
148            synchronized (this) {
149                _codecsByName = new HashMap<String, ObjectCodec<?>>(codecsByName);
150                cache = _codecsByName;
151            }
152        }
153        return cache;
154    }
155
156    public String toString(Object object) throws IOException {
157        return toString(object, false);
158    }
159
160    public String toString(Object object, boolean preetyPrint) throws IOException {
161        ByteArrayOutputStream baos = new ByteArrayOutputStream();
162        write(baos, object, preetyPrint);
163        return baos.toString("UTF-8");
164    }
165
166    public void write(OutputStream out, Object object) throws IOException {
167        write(out, object, false);
168    }
169
170    public void write(OutputStream out, Object object, boolean prettyPint) throws IOException {
171
172        JsonGenerator jg = jsonFactory.createJsonGenerator(out, JsonEncoding.UTF8);
173        if (prettyPint) {
174            jg.useDefaultPrettyPrinter();
175        }
176        write(jg, object);
177    }
178
179    @SuppressWarnings({ "rawtypes", "unchecked" })
180    public void write(JsonGenerator jg, Object object) throws IOException {
181        if (object == null) {
182            jg.writeStartObject();
183            jg.writeStringField("entity-type", "null");
184            jg.writeFieldName("value");
185            jg.writeNull();
186            jg.writeEndObject();
187        } else {
188            Class<?> clazz = object.getClass();
189            ObjectCodec<?> codec = getCodec(clazz);
190            if (codec == null) {
191                writeGenericObject(jg, clazz, object);
192            } else {
193                jg.writeStartObject();
194                jg.writeStringField("entity-type", codec.getType());
195                jg.writeFieldName("value");
196                ((ObjectCodec) codec).write(jg, object);
197                jg.writeEndObject();
198            }
199        }
200        jg.flush();
201    }
202
203    public Object read(String json, CoreSession session) throws IOException, ClassNotFoundException {
204        return read(json, null, session);
205    }
206
207    public Object read(String json, ClassLoader cl, CoreSession session) throws IOException, ClassNotFoundException {
208        ByteArrayInputStream in = new ByteArrayInputStream(json.getBytes());
209        return read(in, cl, session);
210    }
211
212    public Object read(InputStream in, CoreSession session) throws IOException, ClassNotFoundException {
213        return read(in, null, session);
214    }
215
216    public Object read(InputStream in, ClassLoader cl, CoreSession session) throws IOException, ClassNotFoundException {
217        JsonParser jp = jsonFactory.createJsonParser(in);
218        return read(jp, cl, session);
219    }
220
221    public Object read(JsonParser jp, ClassLoader cl, CoreSession session) throws IOException, ClassNotFoundException {
222        JsonToken tok = jp.getCurrentToken();
223        if (tok == null) {
224            tok = jp.nextToken();
225        }
226        if (tok == JsonToken.START_OBJECT) {
227            tok = jp.nextToken();
228        } else if (tok != JsonToken.FIELD_NAME) {
229            throw new IllegalStateException(
230                    "Invalid parser state. Current token must be either start_object or field_name");
231        }
232        String key = jp.getCurrentName();
233        if (!"entity-type".equals(key)) {
234            throw new IllegalStateException("Invalid parser state. Current field must be 'entity-type'");
235        }
236        jp.nextToken();
237        String name = jp.getText();
238        if (name == null) {
239            throw new IllegalStateException("Invalid stream. Entity-Type is null");
240        }
241        jp.nextValue(); // move to next value
242        ObjectCodec<?> codec = codecs.get(name);
243        if (codec == null) {
244            return readGenericObject(jp, name, cl);
245        } else {
246            return codec.read(jp, session);
247        }
248    }
249
250    public Object readNode(JsonNode node, ClassLoader cl, CoreSession session) throws IOException {
251        // Handle simple scalar types
252        if (node.isNumber()) {
253            return node.getNumberValue();
254        } else if (node.isBoolean()) {
255            return node.getBooleanValue();
256        } else if (node.isTextual()) {
257            return node.getTextValue();
258        } else if (node.isArray()) {
259            List<Object> result = new ArrayList<>();
260            Iterator<JsonNode> elements = node.getElements();
261            while (elements.hasNext()) {
262                result.add(readNode(elements.next(), cl, session));
263            }
264            return result;
265        }
266        JsonNode entityTypeNode = node.get("entity-type");
267        JsonNode valueNode = node.get("value");
268        if (entityTypeNode != null && entityTypeNode.isTextual()) {
269            String type = entityTypeNode.getTextValue();
270            ObjectCodec<?> codec = codecsByName.get(type);
271            // handle structured entity with an explicit type declaration
272            JsonParser jp = jsonFactory.createJsonParser(node.toString());
273            if (valueNode == null) {
274                if (codec == null) {
275                    return readGenericObject(jp, type, cl);
276                } else {
277                    return codec.read(jp, session);
278                }
279            }
280            JsonParser valueParser = valueNode.traverse();
281            if (valueParser.getCodec() == null) {
282                valueParser.setCodec(new ObjectMapper());
283            }
284            if (valueParser.getCurrentToken() == null) {
285                valueParser.nextToken();
286            }
287            if (codec == null) {
288                return readGenericObject(valueParser, type, cl);
289            } else {
290                return codec.read(valueParser, session);
291            }
292        }
293        // fallback to returning the original json node
294        return node;
295    }
296
297    public Object readNode(JsonNode node, CoreSession session) throws IOException {
298        return readNode(node, null, session);
299    }
300
301    protected final void writeGenericObject(JsonGenerator jg, Class<?> clazz, Object object) throws IOException {
302        jg.writeStartObject();
303        if (clazz.isPrimitive()) {
304            if (clazz == Boolean.TYPE) {
305                jg.writeStringField("entity-type", "boolean");
306                jg.writeBooleanField("value", (Boolean) object);
307            } else if (clazz == Double.TYPE || clazz == Float.TYPE) {
308                jg.writeStringField("entity-type", "number");
309                jg.writeNumberField("value", ((Number) object).doubleValue());
310            } else if (clazz == Integer.TYPE || clazz == Long.TYPE || clazz == Short.TYPE || clazz == Byte.TYPE) {
311                jg.writeStringField("entity-type", "number");
312                jg.writeNumberField("value", ((Number) object).longValue());
313            } else if (clazz == Character.TYPE) {
314                jg.writeStringField("entity-type", "string");
315                jg.writeStringField("value", object.toString());
316            }
317            return;
318        }
319        if (jg.getCodec() == null) {
320            jg.setCodec(new ObjectMapper());
321        }
322        if (object instanceof Iterable && clazz.getName().startsWith("java.")) {
323            jg.writeStringField("entity-type", "list");
324        } else if (object instanceof Map && clazz.getName().startsWith("java.")) {
325            if (object instanceof LinkedHashMap) {
326                jg.writeStringField("entity-type", "orderedMap");
327            } else {
328                jg.writeStringField("entity-type", "map");
329            }
330        } else {
331            jg.writeStringField("entity-type", clazz.getName());
332        }
333        jg.writeObjectField("value", object);
334        jg.writeEndObject();
335    }
336
337    protected final Object readGenericObject(JsonParser jp, String name, ClassLoader cl) throws IOException {
338        if (jp.getCodec() == null) {
339            jp.setCodec(new ObjectMapper());
340        }
341        if ("list".equals(name)) {
342            return jp.readValueAs(ArrayList.class);
343        } else if ("map".equals(name)) {
344            return jp.readValueAs(HashMap.class);
345        } else if ("orderedMap".equals(name)) {
346            return jp.readValueAs(LinkedHashMap.class);
347        }
348        if (cl == null) {
349            cl = Thread.currentThread().getContextClassLoader();
350            if (cl == null) {
351                cl = ObjectCodecService.class.getClassLoader();
352            }
353        }
354        Class<?> clazz;
355        try {
356            clazz = cl.loadClass(name);
357        } catch (ClassNotFoundException e) {
358            throw new IOException(e);
359        }
360        return jp.readValueAs(clazz);
361    }
362
363    public static class StringCodec extends ObjectCodec<String> {
364        public StringCodec() {
365            super(String.class);
366        }
367
368        @Override
369        public String getType() {
370            return "string";
371        }
372
373        @Override
374        public void write(JsonGenerator jg, String value) throws IOException {
375            jg.writeString(value);
376        }
377
378        @Override
379        public String read(JsonParser jp, CoreSession session) throws IOException {
380            return jp.getText();
381        }
382
383        @Override
384        public boolean isBuiltin() {
385            return true;
386        }
387
388        public void register(ObjectCodecService service) {
389            service.codecs.put(String.class, this);
390            service.codecsByName.put(getType(), this);
391        }
392    }
393
394    public static class DateCodec extends ObjectCodec<Date> {
395        public DateCodec() {
396            super(Date.class);
397        }
398
399        @Override
400        public String getType() {
401            return "date";
402        }
403
404        @Override
405        public void write(JsonGenerator jg, Date value) throws IOException {
406            jg.writeString(DateParser.formatW3CDateTime(value));
407        }
408
409        @Override
410        public Date read(JsonParser jp, CoreSession session) throws IOException {
411            return DateParser.parseW3CDateTime(jp.getText());
412        }
413
414        @Override
415        public boolean isBuiltin() {
416            return true;
417        }
418
419        public void register(ObjectCodecService service) {
420            service.codecs.put(Date.class, this);
421            service.codecsByName.put(getType(), this);
422        }
423    }
424
425    public static class CalendarCodec extends ObjectCodec<Calendar> {
426        public CalendarCodec() {
427            super(Calendar.class);
428        }
429
430        @Override
431        public String getType() {
432            return "date";
433        }
434
435        @Override
436        public void write(JsonGenerator jg, Calendar value) throws IOException {
437            jg.writeString(DateParser.formatW3CDateTime(value.getTime()));
438        }
439
440        @Override
441        public Calendar read(JsonParser jp, CoreSession session) throws IOException {
442            Calendar c = Calendar.getInstance();
443            c.setTime(DateParser.parseW3CDateTime(jp.getText()));
444            return c;
445        }
446
447        @Override
448        public boolean isBuiltin() {
449            return true;
450        }
451
452        public void register(ObjectCodecService service) {
453            service.codecs.put(Calendar.class, this);
454        }
455    }
456
457    public static class BooleanCodec extends ObjectCodec<Boolean> {
458        public BooleanCodec() {
459            super(Boolean.class);
460        }
461
462        @Override
463        public String getType() {
464            return "boolean";
465        }
466
467        @Override
468        public void write(JsonGenerator jg, Boolean value) throws IOException {
469            jg.writeBoolean(value);
470        }
471
472        @Override
473        public Boolean read(JsonParser jp, CoreSession session) throws IOException {
474            return jp.getBooleanValue();
475        }
476
477        @Override
478        public boolean isBuiltin() {
479            return true;
480        }
481
482        public void register(ObjectCodecService service) {
483            service.codecs.put(Boolean.class, this);
484            service.codecs.put(Boolean.TYPE, this);
485            service.codecsByName.put(getType(), this);
486        }
487    }
488
489    public static class NumberCodec extends ObjectCodec<Number> {
490        public NumberCodec() {
491            super(Number.class);
492        }
493
494        @Override
495        public String getType() {
496            return "number";
497        }
498
499        @Override
500        public void write(JsonGenerator jg, Number value) throws IOException {
501            Class<?> cl = value.getClass();
502            if (cl == Double.class || cl == Float.class) {
503                jg.writeNumber(value.doubleValue());
504            } else {
505                jg.writeNumber(value.longValue());
506            }
507        }
508
509        @Override
510        public Number read(JsonParser jp, CoreSession session) throws IOException {
511            if (jp.getCurrentToken() == JsonToken.VALUE_NUMBER_FLOAT) {
512                return jp.getDoubleValue();
513            } else {
514                return jp.getLongValue();
515            }
516        }
517
518        @Override
519        public boolean isBuiltin() {
520            return true;
521        }
522
523        public void register(ObjectCodecService service) {
524            service.codecs.put(Integer.class, this);
525            service.codecs.put(Integer.TYPE, this);
526            service.codecs.put(Long.class, this);
527            service.codecs.put(Long.TYPE, this);
528            service.codecs.put(Double.class, this);
529            service.codecs.put(Double.TYPE, this);
530            service.codecs.put(Float.class, this);
531            service.codecs.put(Float.TYPE, this);
532            service.codecs.put(Short.class, this);
533            service.codecs.put(Short.TYPE, this);
534            service.codecs.put(Byte.class, this);
535            service.codecs.put(Byte.TYPE, this);
536            service.codecsByName.put(getType(), this);
537        }
538    }
539
540    public static class DocumentAdapterCodec extends ObjectCodec<BusinessAdapter> {
541
542        protected final DocumentAdapterDescriptor descriptor;
543
544        @SuppressWarnings("unchecked")
545        public DocumentAdapterCodec(DocumentAdapterDescriptor descriptor) {
546            super(descriptor.getInterface());
547            this.descriptor = descriptor;
548        }
549
550        @Override
551        public String getType() {
552            return descriptor.getInterface().getSimpleName();
553        }
554
555        public static void register(ObjectCodecService service, DocumentAdapterService adapterService) {
556            for (DocumentAdapterDescriptor desc : adapterService.getAdapterDescriptors()) {
557                if (!BusinessAdapter.class.isAssignableFrom(desc.getInterface())) {
558                    continue;
559                }
560                DocumentAdapterCodec codec = new DocumentAdapterCodec(desc);
561                if (service.codecsByName.containsKey(codec.getType())) {
562                    log.warn("Be careful, you have already contributed an adapter with the same simple name:"
563                            + codec.getType());
564                    continue;
565                }
566                service.codecs.put(desc.getInterface(), codec);
567                service.codecsByName.put(codec.getType(), codec);
568            }
569        }
570
571        /**
572         * When the object codec is called the stream is positioned on the first value. For inlined objects this is the
573         * first value after the "entity-type" property. For non inlined objects this will be the object itself (i.e.
574         * '{' or '[')
575         *
576         * @param jp
577         * @return
578         * @throws IOException
579         */
580        @Override
581        public BusinessAdapter read(JsonParser jp, CoreSession session) throws IOException {
582            if (jp.getCodec() == null) {
583                jp.setCodec(new ObjectMapper());
584            }
585            BusinessAdapter fromBa = jp.readValueAs(type);
586            DocumentModel doc = fromBa.getId() != null ? session.getDocument(new IdRef(fromBa.getId()))
587                    : DocumentModelFactory.createDocumentModel(fromBa.getType());
588            BusinessAdapter ba = doc.getAdapter(fromBa.getClass());
589
590            // And finally copy the fields sets from the adapter
591            for (String schema : fromBa.getDocument().getSchemas()) {
592                DataModel dataModel = ba.getDocument().getDataModel(schema);
593                DataModel fromDataModel = fromBa.getDocument().getDataModel(schema);
594
595                for (String field : fromDataModel.getDirtyFields()) {
596                    dataModel.setData(field, fromDataModel.getData(field));
597                }
598            }
599            return ba;
600        }
601    }
602
603}