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