org.nuxeo.ecm.automation.io.services.codec.ObjectCodecService.java Source code

Java tutorial

Introduction

Here is the source code for org.nuxeo.ecm.automation.io.services.codec.ObjectCodecService.java

Source

/*
 * (C) Copyright 2006-2013 Nuxeo SA (http://nuxeo.com/) and others.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * Contributors:
 *     bstefanescu
 *     vpasquier
 *     slacoin
 */
package org.nuxeo.ecm.automation.io.services.codec;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.codehaus.jackson.JsonEncoding;
import org.codehaus.jackson.JsonFactory;
import org.codehaus.jackson.JsonGenerator;
import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.JsonParser;
import org.codehaus.jackson.JsonToken;
import org.codehaus.jackson.map.ObjectMapper;
import org.nuxeo.ecm.automation.core.operations.business.adapter.BusinessAdapter;
import org.nuxeo.ecm.core.api.CoreSession;
import org.nuxeo.ecm.core.api.DataModel;
import org.nuxeo.ecm.core.api.DocumentModel;
import org.nuxeo.ecm.core.api.DocumentModelFactory;
import org.nuxeo.ecm.core.api.IdRef;
import org.nuxeo.ecm.core.api.adapter.DocumentAdapterDescriptor;
import org.nuxeo.ecm.core.api.adapter.DocumentAdapterService;
import org.nuxeo.ecm.core.schema.utils.DateParser;
import org.nuxeo.runtime.api.Framework;

/**
 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a>
 */
public class ObjectCodecService {

    protected static final Log log = LogFactory.getLog(ObjectCodecService.class);

    protected Map<Class<?>, ObjectCodec<?>> codecs;

    protected Map<String, ObjectCodec<?>> codecsByName;

    protected Map<Class<?>, ObjectCodec<?>> _codecs;

    protected Map<String, ObjectCodec<?>> _codecsByName;

    private JsonFactory jsonFactory;

    public ObjectCodecService(JsonFactory jsonFactory) {
        this.jsonFactory = jsonFactory;
        codecs = new HashMap<Class<?>, ObjectCodec<?>>();
        codecsByName = new HashMap<String, ObjectCodec<?>>();
        init();
    }

    protected void init() {
        new StringCodec().register(this);
        new DateCodec().register(this);
        new CalendarCodec().register(this);
        new BooleanCodec().register(this);
        new NumberCodec().register(this);
    }

    public void postInit() {
        DocumentAdapterCodec.register(this, Framework.getLocalService(DocumentAdapterService.class));
    }

    /**
     * Get all codecs.
     */
    public Collection<ObjectCodec<?>> getCodecs() {
        return codecs().values();
    }

    public synchronized void addCodec(ObjectCodec<?> codec) {
        codecs.put(codec.getJavaType(), codec);
        codecsByName.put(codec.getType(), codec);
        _codecs = null;
        _codecsByName = null;
    }

    public synchronized void removeCodec(String name) {
        ObjectCodec<?> codec = codecsByName.remove(name);
        if (codec != null) {
            codecs.remove(codec.getJavaType());
            _codecs = null;
            _codecsByName = null;
        }
    }

    public synchronized void removeCodec(Class<?> objectType) {
        ObjectCodec<?> codec = codecs.remove(objectType);
        if (codec != null) {
            codecsByName.remove(codec.getType());
            _codecs = null;
            _codecsByName = null;
        }
    }

    public ObjectCodec<?> getCodec(Class<?> objectType) {
        return codecs().get(objectType);
    }

    public ObjectCodec<?> getCodec(String name) {
        return codecsByName().get(name);
    }

    public Map<Class<?>, ObjectCodec<?>> codecs() {
        Map<Class<?>, ObjectCodec<?>> cache = _codecs;
        if (cache == null) {
            synchronized (this) {
                _codecs = new HashMap<Class<?>, ObjectCodec<?>>(codecs);
                cache = _codecs;
            }
        }
        return cache;
    }

    public Map<String, ObjectCodec<?>> codecsByName() {
        Map<String, ObjectCodec<?>> cache = _codecsByName;
        if (cache == null) {
            synchronized (this) {
                _codecsByName = new HashMap<String, ObjectCodec<?>>(codecsByName);
                cache = _codecsByName;
            }
        }
        return cache;
    }

    public String toString(Object object) throws IOException {
        return toString(object, false);
    }

    public String toString(Object object, boolean preetyPrint) throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        write(baos, object, preetyPrint);
        return baos.toString("UTF-8");
    }

    public void write(OutputStream out, Object object) throws IOException {
        write(out, object, false);
    }

    public void write(OutputStream out, Object object, boolean prettyPint) throws IOException {

        JsonGenerator jg = jsonFactory.createJsonGenerator(out, JsonEncoding.UTF8);
        if (prettyPint) {
            jg.useDefaultPrettyPrinter();
        }
        write(jg, object);
    }

    @SuppressWarnings({ "rawtypes", "unchecked" })
    public void write(JsonGenerator jg, Object object) throws IOException {
        if (object == null) {
            jg.writeStartObject();
            jg.writeStringField("entity-type", "null");
            jg.writeFieldName("value");
            jg.writeNull();
            jg.writeEndObject();
        } else {
            Class<?> clazz = object.getClass();
            ObjectCodec<?> codec = getCodec(clazz);
            if (codec == null) {
                writeGenericObject(jg, clazz, object);
            } else {
                jg.writeStartObject();
                jg.writeStringField("entity-type", codec.getType());
                jg.writeFieldName("value");
                ((ObjectCodec) codec).write(jg, object);
                jg.writeEndObject();
            }
        }
        jg.flush();
    }

    public Object read(String json, CoreSession session) throws IOException, ClassNotFoundException {
        return read(json, null, session);
    }

    public Object read(String json, ClassLoader cl, CoreSession session)
            throws IOException, ClassNotFoundException {
        ByteArrayInputStream in = new ByteArrayInputStream(json.getBytes());
        return read(in, cl, session);
    }

    public Object read(InputStream in, CoreSession session) throws IOException, ClassNotFoundException {
        return read(in, null, session);
    }

    public Object read(InputStream in, ClassLoader cl, CoreSession session)
            throws IOException, ClassNotFoundException {
        JsonParser jp = jsonFactory.createJsonParser(in);
        return read(jp, cl, session);
    }

    public Object read(JsonParser jp, ClassLoader cl, CoreSession session)
            throws IOException, ClassNotFoundException {
        JsonToken tok = jp.getCurrentToken();
        if (tok == null) {
            tok = jp.nextToken();
        }
        if (tok == JsonToken.START_OBJECT) {
            tok = jp.nextToken();
        } else if (tok != JsonToken.FIELD_NAME) {
            throw new IllegalStateException(
                    "Invalid parser state. Current token must be either start_object or field_name");
        }
        String key = jp.getCurrentName();
        if (!"entity-type".equals(key)) {
            throw new IllegalStateException("Invalid parser state. Current field must be 'entity-type'");
        }
        jp.nextToken();
        String name = jp.getText();
        if (name == null) {
            throw new IllegalStateException("Invalid stream. Entity-Type is null");
        }
        jp.nextValue(); // move to next value
        ObjectCodec<?> codec = codecs.get(name);
        if (codec == null) {
            return readGenericObject(jp, name, cl);
        } else {
            return codec.read(jp, session);
        }
    }

    public Object readNode(JsonNode node, ClassLoader cl, CoreSession session) throws IOException {
        // Handle simple scalar types
        if (node.isNumber()) {
            return node.getNumberValue();
        } else if (node.isBoolean()) {
            return node.getBooleanValue();
        } else if (node.isTextual()) {
            return node.getTextValue();
        } else if (node.isArray()) {
            List<Object> result = new ArrayList<>();
            Iterator<JsonNode> elements = node.getElements();
            while (elements.hasNext()) {
                result.add(readNode(elements.next(), cl, session));
            }
            return result;
        }
        JsonNode entityTypeNode = node.get("entity-type");
        JsonNode valueNode = node.get("value");
        if (entityTypeNode != null && entityTypeNode.isTextual()) {
            String type = entityTypeNode.getTextValue();
            ObjectCodec<?> codec = codecsByName.get(type);
            // handle structured entity with an explicit type declaration
            JsonParser jp = jsonFactory.createJsonParser(node.toString());
            if (valueNode == null) {
                if (codec == null) {
                    return readGenericObject(jp, type, cl);
                } else {
                    return codec.read(jp, session);
                }
            }
            JsonParser valueParser = valueNode.traverse();
            if (valueParser.getCodec() == null) {
                valueParser.setCodec(new ObjectMapper());
            }
            if (valueParser.getCurrentToken() == null) {
                valueParser.nextToken();
            }
            if (codec == null) {
                return readGenericObject(valueParser, type, cl);
            } else {
                return codec.read(valueParser, session);
            }
        }
        // fallback to returning the original json node
        return node;
    }

    public Object readNode(JsonNode node, CoreSession session) throws IOException {
        return readNode(node, null, session);
    }

    protected final void writeGenericObject(JsonGenerator jg, Class<?> clazz, Object object) throws IOException {
        jg.writeStartObject();
        if (clazz.isPrimitive()) {
            if (clazz == Boolean.TYPE) {
                jg.writeStringField("entity-type", "boolean");
                jg.writeBooleanField("value", (Boolean) object);
            } else if (clazz == Double.TYPE || clazz == Float.TYPE) {
                jg.writeStringField("entity-type", "number");
                jg.writeNumberField("value", ((Number) object).doubleValue());
            } else if (clazz == Integer.TYPE || clazz == Long.TYPE || clazz == Short.TYPE || clazz == Byte.TYPE) {
                jg.writeStringField("entity-type", "number");
                jg.writeNumberField("value", ((Number) object).longValue());
            } else if (clazz == Character.TYPE) {
                jg.writeStringField("entity-type", "string");
                jg.writeStringField("value", object.toString());
            }
            return;
        }
        if (jg.getCodec() == null) {
            jg.setCodec(new ObjectMapper());
        }
        if (object instanceof Iterable && clazz.getName().startsWith("java.")) {
            jg.writeStringField("entity-type", "list");
        } else if (object instanceof Map && clazz.getName().startsWith("java.")) {
            if (object instanceof LinkedHashMap) {
                jg.writeStringField("entity-type", "orderedMap");
            } else {
                jg.writeStringField("entity-type", "map");
            }
        } else {
            jg.writeStringField("entity-type", clazz.getName());
        }
        jg.writeObjectField("value", object);
        jg.writeEndObject();
    }

    protected final Object readGenericObject(JsonParser jp, String name, ClassLoader cl) throws IOException {
        if (jp.getCodec() == null) {
            jp.setCodec(new ObjectMapper());
        }
        if ("list".equals(name)) {
            return jp.readValueAs(ArrayList.class);
        } else if ("map".equals(name)) {
            return jp.readValueAs(HashMap.class);
        } else if ("orderedMap".equals(name)) {
            return jp.readValueAs(LinkedHashMap.class);
        }
        if (cl == null) {
            cl = Thread.currentThread().getContextClassLoader();
            if (cl == null) {
                cl = ObjectCodecService.class.getClassLoader();
            }
        }
        Class<?> clazz;
        try {
            clazz = cl.loadClass(name);
        } catch (ClassNotFoundException e) {
            throw new IOException(e);
        }
        return jp.readValueAs(clazz);
    }

    public static class StringCodec extends ObjectCodec<String> {
        public StringCodec() {
            super(String.class);
        }

        @Override
        public String getType() {
            return "string";
        }

        @Override
        public void write(JsonGenerator jg, String value) throws IOException {
            jg.writeString(value);
        }

        @Override
        public String read(JsonParser jp, CoreSession session) throws IOException {
            return jp.getText();
        }

        @Override
        public boolean isBuiltin() {
            return true;
        }

        public void register(ObjectCodecService service) {
            service.codecs.put(String.class, this);
            service.codecsByName.put(getType(), this);
        }
    }

    public static class DateCodec extends ObjectCodec<Date> {
        public DateCodec() {
            super(Date.class);
        }

        @Override
        public String getType() {
            return "date";
        }

        @Override
        public void write(JsonGenerator jg, Date value) throws IOException {
            jg.writeString(DateParser.formatW3CDateTime(value));
        }

        @Override
        public Date read(JsonParser jp, CoreSession session) throws IOException {
            return DateParser.parseW3CDateTime(jp.getText());
        }

        @Override
        public boolean isBuiltin() {
            return true;
        }

        public void register(ObjectCodecService service) {
            service.codecs.put(Date.class, this);
            service.codecsByName.put(getType(), this);
        }
    }

    public static class CalendarCodec extends ObjectCodec<Calendar> {
        public CalendarCodec() {
            super(Calendar.class);
        }

        @Override
        public String getType() {
            return "date";
        }

        @Override
        public void write(JsonGenerator jg, Calendar value) throws IOException {
            jg.writeString(DateParser.formatW3CDateTime(value.getTime()));
        }

        @Override
        public Calendar read(JsonParser jp, CoreSession session) throws IOException {
            Calendar c = Calendar.getInstance();
            c.setTime(DateParser.parseW3CDateTime(jp.getText()));
            return c;
        }

        @Override
        public boolean isBuiltin() {
            return true;
        }

        public void register(ObjectCodecService service) {
            service.codecs.put(Calendar.class, this);
        }
    }

    public static class BooleanCodec extends ObjectCodec<Boolean> {
        public BooleanCodec() {
            super(Boolean.class);
        }

        @Override
        public String getType() {
            return "boolean";
        }

        @Override
        public void write(JsonGenerator jg, Boolean value) throws IOException {
            jg.writeBoolean(value);
        }

        @Override
        public Boolean read(JsonParser jp, CoreSession session) throws IOException {
            return jp.getBooleanValue();
        }

        @Override
        public boolean isBuiltin() {
            return true;
        }

        public void register(ObjectCodecService service) {
            service.codecs.put(Boolean.class, this);
            service.codecs.put(Boolean.TYPE, this);
            service.codecsByName.put(getType(), this);
        }
    }

    public static class NumberCodec extends ObjectCodec<Number> {
        public NumberCodec() {
            super(Number.class);
        }

        @Override
        public String getType() {
            return "number";
        }

        @Override
        public void write(JsonGenerator jg, Number value) throws IOException {
            Class<?> cl = value.getClass();
            if (cl == Double.class || cl == Float.class) {
                jg.writeNumber(value.doubleValue());
            } else {
                jg.writeNumber(value.longValue());
            }
        }

        @Override
        public Number read(JsonParser jp, CoreSession session) throws IOException {
            if (jp.getCurrentToken() == JsonToken.VALUE_NUMBER_FLOAT) {
                return jp.getDoubleValue();
            } else {
                return jp.getLongValue();
            }
        }

        @Override
        public boolean isBuiltin() {
            return true;
        }

        public void register(ObjectCodecService service) {
            service.codecs.put(Integer.class, this);
            service.codecs.put(Integer.TYPE, this);
            service.codecs.put(Long.class, this);
            service.codecs.put(Long.TYPE, this);
            service.codecs.put(Double.class, this);
            service.codecs.put(Double.TYPE, this);
            service.codecs.put(Float.class, this);
            service.codecs.put(Float.TYPE, this);
            service.codecs.put(Short.class, this);
            service.codecs.put(Short.TYPE, this);
            service.codecs.put(Byte.class, this);
            service.codecs.put(Byte.TYPE, this);
            service.codecsByName.put(getType(), this);
        }
    }

    public static class DocumentAdapterCodec extends ObjectCodec<BusinessAdapter> {

        protected final DocumentAdapterDescriptor descriptor;

        @SuppressWarnings("unchecked")
        public DocumentAdapterCodec(DocumentAdapterDescriptor descriptor) {
            super(descriptor.getInterface());
            this.descriptor = descriptor;
        }

        @Override
        public String getType() {
            return descriptor.getInterface().getSimpleName();
        }

        public static void register(ObjectCodecService service, DocumentAdapterService adapterService) {
            for (DocumentAdapterDescriptor desc : adapterService.getAdapterDescriptors()) {
                if (!BusinessAdapter.class.isAssignableFrom(desc.getInterface())) {
                    continue;
                }
                DocumentAdapterCodec codec = new DocumentAdapterCodec(desc);
                if (service.codecsByName.containsKey(codec.getType())) {
                    log.warn("Be careful, you have already contributed an adapter with the same simple name:"
                            + codec.getType());
                    continue;
                }
                service.codecs.put(desc.getInterface(), codec);
                service.codecsByName.put(codec.getType(), codec);
            }
        }

        /**
         * When the object codec is called the stream is positioned on the first value. For inlined objects this is the
         * first value after the "entity-type" property. For non inlined objects this will be the object itself (i.e.
         * '{' or '[')
         *
         * @param jp
         * @return
         * @throws IOException
         */
        @Override
        public BusinessAdapter read(JsonParser jp, CoreSession session) throws IOException {
            if (jp.getCodec() == null) {
                jp.setCodec(new ObjectMapper());
            }
            BusinessAdapter fromBa = jp.readValueAs(type);
            DocumentModel doc = fromBa.getId() != null ? session.getDocument(new IdRef(fromBa.getId()))
                    : DocumentModelFactory.createDocumentModel(fromBa.getType());
            BusinessAdapter ba = doc.getAdapter(fromBa.getClass());

            // And finally copy the fields sets from the adapter
            for (String schema : fromBa.getDocument().getSchemas()) {
                DataModel dataModel = ba.getDocument().getDataModel(schema);
                DataModel fromDataModel = fromBa.getDocument().getDataModel(schema);

                for (String field : fromDataModel.getDirtyFields()) {
                    dataModel.setData(field, fromDataModel.getData(field));
                }
            }
            return ba;
        }
    }

}