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