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