/* * (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; } } }