001/*
002 * (C) Copyright 2015 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 *     Nicolas Chapurlat <nchapurlat@nuxeo.com>
018 */
019
020package org.nuxeo.ecm.core.io.marshallers.json.document;
021
022import static org.nuxeo.ecm.core.io.marshallers.json.document.DocumentModelJsonWriter.ENTITY_TYPE;
023import static org.nuxeo.ecm.core.io.registry.reflect.Instantiations.SINGLETON;
024import static org.nuxeo.ecm.core.io.registry.reflect.Priorities.REFERENCE;
025
026import java.io.IOException;
027import java.io.InputStream;
028import java.io.Serializable;
029import java.lang.reflect.ParameterizedType;
030import java.lang.reflect.Type;
031import java.util.List;
032
033import javax.ws.rs.core.MediaType;
034
035import org.apache.commons.lang.StringUtils;
036import org.apache.commons.lang3.reflect.TypeUtils;
037import org.codehaus.jackson.JsonNode;
038import org.nuxeo.ecm.core.api.Blob;
039import org.nuxeo.ecm.core.api.CoreSession;
040import org.nuxeo.ecm.core.api.DocumentModel;
041import org.nuxeo.ecm.core.api.IdRef;
042import org.nuxeo.ecm.core.api.impl.DataModelImpl;
043import org.nuxeo.ecm.core.api.impl.SimpleDocumentModel;
044import org.nuxeo.ecm.core.api.model.Property;
045import org.nuxeo.ecm.core.api.model.PropertyNotFoundException;
046import org.nuxeo.ecm.core.api.model.impl.ComplexProperty;
047import org.nuxeo.ecm.core.api.model.impl.ListProperty;
048import org.nuxeo.ecm.core.api.model.impl.primitives.BlobProperty;
049import org.nuxeo.ecm.core.io.marshallers.json.EntityJsonReader;
050import org.nuxeo.ecm.core.io.registry.Reader;
051import org.nuxeo.ecm.core.io.registry.context.RenderingContext.SessionWrapper;
052import org.nuxeo.ecm.core.io.registry.reflect.Setup;
053import org.nuxeo.ecm.core.schema.DocumentType;
054import org.nuxeo.ecm.core.schema.SchemaManager;
055import org.nuxeo.ecm.core.schema.types.Field;
056import org.nuxeo.ecm.core.schema.types.Schema;
057import org.nuxeo.runtime.api.Framework;
058
059/**
060 * Convert Json as {@link DocumentModel}.
061 * <p>
062 * Format is (any additional json property is ignored):
063 *
064 * <pre>
065 * {
066 *   "entity-type": "document",
067 *   "uid": "EXISTING_DOCUMENT_UID", <- use it to update an existing document
068 *   "repository": "REPOSITORY_NAME" , <- explicitely specify the repository name
069 *   "name": "DOCUMENT_NAME", <- use it to create an new document
070 *   "type": "DOCUMENT_TYPE", <- use it to create an new document
071 *   "changeToken": "CHANGE_TOKEN", <- pass the previous change token for optimistic locking
072 *   "properties": ...  <-- see {@link DocumentPropertiesJsonReader}
073 * }
074 * </pre>
075 *
076 * </p>
077 *
078 * @since 7.2
079 */
080@Setup(mode = SINGLETON, priority = REFERENCE)
081public class DocumentModelJsonReader extends EntityJsonReader<DocumentModel> {
082
083    public static final String LEGACY_MODE_READER = "DocumentModelLegacyModeReader";
084
085    public DocumentModelJsonReader() {
086        super(ENTITY_TYPE);
087    }
088
089    @Override
090    public DocumentModel read(Class<?> clazz, Type genericType, MediaType mediaType, InputStream in) throws IOException {
091        Reader<DocumentModel> reader = ctx.getParameter(LEGACY_MODE_READER);
092        if (reader != null) {
093            DocumentModel doc = reader.read(clazz, genericType, mediaType, in);
094            return doc;
095        } else {
096            return super.read(clazz, genericType, mediaType, in);
097        }
098    }
099
100    @Override
101    protected DocumentModel readEntity(JsonNode jn) throws IOException {
102
103        SimpleDocumentModel simpleDoc = new SimpleDocumentModel();
104        String name = getStringField(jn, "name");
105        if (StringUtils.isNotBlank(name)) {
106            simpleDoc.setPathInfo(null, name);
107        }
108        String type = getStringField(jn, "type");
109        if (StringUtils.isNotBlank(type)) {
110            simpleDoc.setType(type);
111        }
112
113        JsonNode propsNode = jn.get("properties");
114        if (propsNode != null && !propsNode.isNull() && propsNode.isObject()) {
115            ParameterizedType genericType = TypeUtils.parameterize(List.class, Property.class);
116            List<Property> properties = readEntity(List.class, genericType, propsNode);
117            for (Property property : properties) {
118                String propertyName = property.getName();
119                // handle schema with no prefix
120                if (!propertyName.contains(":")) {
121                    propertyName = property.getField().getDeclaringType().getName() + ":" + propertyName;
122                }
123                simpleDoc.setPropertyValue(propertyName, property.getValue());
124            }
125        }
126
127        DocumentModel doc = null;
128
129        String uid = getStringField(jn, "uid");
130        if (StringUtils.isNotBlank(uid)) {
131            try (SessionWrapper wrapper = ctx.getSession(null)) {
132                doc = wrapper.getSession().getDocument(new IdRef(uid));
133            }
134            avoidBlobUpdate(simpleDoc, doc);
135            applyDirtyPropertyValues(simpleDoc, doc);
136            String changeToken = getStringField(jn, "changeToken");
137            doc.putContextData(CoreSession.CHANGE_TOKEN, changeToken);
138        } else if (StringUtils.isNotBlank(type)) {
139            SimpleDocumentModel createdDoc = new SimpleDocumentModel();
140            if (StringUtils.isNotBlank(name)) {
141                createdDoc.setPathInfo(null, name);
142            }
143            createdDoc.setType(simpleDoc.getType());
144            applyAllPropertyValues(simpleDoc, createdDoc);
145            doc = createdDoc;
146        } else {
147            doc = simpleDoc;
148        }
149
150        return doc;
151    }
152
153    /**
154     * Avoid the blob updates. It's managed by custom ways.
155     */
156    private static void avoidBlobUpdate(DocumentModel docToClean, DocumentModel docRef) {
157        for (String schema : docToClean.getSchemas()) {
158            for (String field : docToClean.getDataModel(schema).getDirtyFields()) {
159                avoidBlobUpdate(docToClean.getProperty(field), docRef);
160            }
161        }
162    }
163
164    private static void avoidBlobUpdate(Property propToClean, DocumentModel docRef) {
165        if (propToClean instanceof BlobProperty) {
166            // if the blob used to exist
167            if (propToClean.getValue() == null) {
168                Serializable value = docRef.getPropertyValue(propToClean.getXPath());
169                propToClean.setValue(value);
170            }
171        } else if (propToClean instanceof ComplexProperty) {
172            ComplexProperty complexPropToClean = (ComplexProperty) propToClean;
173            for (Field field : complexPropToClean.getType().getFields()) {
174                Property childPropToClean = complexPropToClean.get(field.getName().getLocalName());
175                avoidBlobUpdate(childPropToClean, docRef);
176            }
177        } else if (propToClean instanceof ListProperty) {
178            ListProperty listPropToClean = (ListProperty) propToClean;
179            for (int i = 0; i < listPropToClean.size(); i++) {
180                Property elPropToClean = listPropToClean.get(i);
181                avoidBlobUpdate(elPropToClean, docRef);
182            }
183        }
184    }
185
186    public static void applyPropertyValues(DocumentModel src, DocumentModel dst) {
187        avoidBlobUpdate(src, dst);
188        applyPropertyValues(src, dst, true);
189        // copy change token
190        dst.getContextData().putAll(src.getContextData());
191    }
192
193    public static void applyPropertyValues(DocumentModel src, DocumentModel dst, boolean dirtyOnly) {
194        // if not "dirty only", it handles all the schemas for the given type
195        // so it will trigger the default values initialization
196        if (dirtyOnly) {
197            applyDirtyPropertyValues(src, dst);
198        } else {
199            applyAllPropertyValues(src, dst);
200        }
201    }
202
203    public static void applyDirtyPropertyValues(DocumentModel src, DocumentModel dst) {
204        String[] schemas = src.getSchemas();
205        for (String schema : schemas) {
206            DataModelImpl dataModel = (DataModelImpl) dst.getDataModel(schema);
207            DataModelImpl fromDataModel = (DataModelImpl) src.getDataModel(schema);
208            for (String field : fromDataModel.getDirtyFields()) {
209                Serializable data = (Serializable) fromDataModel.getData(field);
210                try {
211                    if (!(dataModel.getDocumentPart().get(field) instanceof BlobProperty)) {
212                        dataModel.setData(field, data);
213                    } else {
214                        dataModel.setData(field, decodeBlob(data));
215                    }
216                } catch (PropertyNotFoundException e) {
217                    continue;
218                }
219            }
220        }
221    }
222
223    public static void applyAllPropertyValues(DocumentModel src, DocumentModel dst) {
224        SchemaManager service = Framework.getService(SchemaManager.class);
225        DocumentType type = service.getDocumentType(src.getType());
226        String[] schemas = type.getSchemaNames();
227        for (String schemaName : schemas) {
228            Schema schema = service.getSchema(schemaName);
229            DataModelImpl dataModel = (DataModelImpl) dst.getDataModel(schemaName);
230            DataModelImpl fromDataModel = (DataModelImpl) src.getDataModel(schemaName);
231            for (Field field : schema.getFields()) {
232                String fieldName = field.getName().getLocalName();
233                Serializable data = (Serializable) fromDataModel.getData(fieldName);
234                try {
235                    if (!(dataModel.getDocumentPart().get(fieldName) instanceof BlobProperty)) {
236                        dataModel.setData(fieldName, data);
237                    } else {
238                        dataModel.setData(fieldName, decodeBlob(data));
239                    }
240                } catch (PropertyNotFoundException e) {
241                    continue;
242                }
243            }
244        }
245    }
246
247    private static Serializable decodeBlob(Serializable data) {
248        if (data instanceof Blob) {
249            return data;
250        } else {
251            return null;
252        }
253    }
254
255}