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 * "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 * </p> 076 * 077 * @since 7.2 078 */ 079@Setup(mode = SINGLETON, priority = REFERENCE) 080public class DocumentModelJsonReader extends EntityJsonReader<DocumentModel> { 081 082 public static final String LEGACY_MODE_READER = "DocumentModelLegacyModeReader"; 083 084 public DocumentModelJsonReader() { 085 super(ENTITY_TYPE); 086 } 087 088 @Override 089 public DocumentModel read(Class<?> clazz, Type genericType, MediaType mediaType, InputStream in) 090 throws IOException { 091 Reader<DocumentModel> reader = ctx.getParameter(LEGACY_MODE_READER); 092 if (reader != null) { 093 return reader.read(clazz, genericType, mediaType, in); 094 } else { 095 return super.read(clazz, genericType, mediaType, in); 096 } 097 } 098 099 @Override 100 protected DocumentModel readEntity(JsonNode jn) throws IOException { 101 102 SimpleDocumentModel simpleDoc = new SimpleDocumentModel(); 103 String name = getStringField(jn, "name"); 104 if (StringUtils.isNotBlank(name)) { 105 simpleDoc.setPathInfo(null, name); 106 } 107 String type = getStringField(jn, "type"); 108 if (StringUtils.isNotBlank(type)) { 109 simpleDoc.setType(type); 110 } 111 112 JsonNode propsNode = jn.get("properties"); 113 if (propsNode != null && !propsNode.isNull() && propsNode.isObject()) { 114 ParameterizedType genericType = TypeUtils.parameterize(List.class, Property.class); 115 List<Property> properties = readEntity(List.class, genericType, propsNode); 116 for (Property property : properties) { 117 String propertyName = property.getName(); 118 // handle schema with no prefix 119 if (!propertyName.contains(":")) { 120 propertyName = property.getField().getDeclaringType().getName() + ":" + propertyName; 121 } 122 simpleDoc.setPropertyValue(propertyName, property.getValue()); 123 } 124 } 125 126 DocumentModel doc; 127 128 String uid = getStringField(jn, "uid"); 129 if (StringUtils.isNotBlank(uid)) { 130 try (SessionWrapper wrapper = ctx.getSession(null)) { 131 doc = wrapper.getSession().getDocument(new IdRef(uid)); 132 } 133 avoidBlobUpdate(simpleDoc, doc); 134 applyDirtyPropertyValues(simpleDoc, doc); 135 String changeToken = getStringField(jn, "changeToken"); 136 doc.putContextData(CoreSession.CHANGE_TOKEN, changeToken); 137 } else if (StringUtils.isNotBlank(type)) { 138 SimpleDocumentModel createdDoc = new SimpleDocumentModel(); 139 if (StringUtils.isNotBlank(name)) { 140 createdDoc.setPathInfo(null, name); 141 } 142 createdDoc.setType(simpleDoc.getType()); 143 applyAllPropertyValues(simpleDoc, createdDoc); 144 doc = createdDoc; 145 } else { 146 doc = simpleDoc; 147 } 148 149 return doc; 150 } 151 152 /** 153 * Avoid the blob updates. It's managed by custom ways. 154 */ 155 private static void avoidBlobUpdate(DocumentModel docToClean, DocumentModel docRef) { 156 for (String schema : docToClean.getSchemas()) { 157 for (String field : docToClean.getDataModel(schema).getDirtyFields()) { 158 avoidBlobUpdate(docToClean.getProperty(field), docRef); 159 } 160 } 161 } 162 163 private static void avoidBlobUpdate(Property propToClean, DocumentModel docRef) { 164 if (propToClean instanceof BlobProperty) { 165 // if the blob used to exist 166 if (propToClean.getValue() == null) { 167 try { 168 Serializable value = docRef.getPropertyValue(propToClean.getXPath()); 169 propToClean.setValue(value); 170 } catch (PropertyNotFoundException e) { 171 // As the blob property doesn't exist in the document in the first place, ignore the operation 172 } 173 } 174 } else if (propToClean instanceof ComplexProperty) { 175 ComplexProperty complexPropToClean = (ComplexProperty) propToClean; 176 for (Field field : complexPropToClean.getType().getFields()) { 177 Property childPropToClean = complexPropToClean.get(field.getName().getLocalName()); 178 avoidBlobUpdate(childPropToClean, docRef); 179 } 180 } else if (propToClean instanceof ListProperty) { 181 ListProperty listPropToClean = (ListProperty) propToClean; 182 for (int i = 0; i < listPropToClean.size(); i++) { 183 Property elPropToClean = listPropToClean.get(i); 184 avoidBlobUpdate(elPropToClean, docRef); 185 } 186 } 187 } 188 189 public static void applyPropertyValues(DocumentModel src, DocumentModel dst) { 190 avoidBlobUpdate(src, dst); 191 applyPropertyValues(src, dst, true); 192 // copy change token 193 dst.getContextData().putAll(src.getContextData()); 194 } 195 196 public static void applyPropertyValues(DocumentModel src, DocumentModel dst, boolean dirtyOnly) { 197 // if not "dirty only", it handles all the schemas for the given type 198 // so it will trigger the default values initialization 199 if (dirtyOnly) { 200 applyDirtyPropertyValues(src, dst); 201 } else { 202 applyAllPropertyValues(src, dst); 203 } 204 } 205 206 public static void applyDirtyPropertyValues(DocumentModel src, DocumentModel dst) { 207 String[] schemas = src.getSchemas(); 208 for (String schema : schemas) { 209 DataModelImpl dataModel = (DataModelImpl) dst.getDataModel(schema); 210 DataModelImpl fromDataModel = (DataModelImpl) src.getDataModel(schema); 211 for (String field : fromDataModel.getDirtyFields()) { 212 Serializable data = (Serializable) fromDataModel.getData(field); 213 try { 214 if (!(dataModel.getDocumentPart().get(field) instanceof BlobProperty)) { 215 dataModel.setData(field, data); 216 } else { 217 dataModel.setData(field, decodeBlob(data)); 218 } 219 } catch (PropertyNotFoundException e) { 220 continue; 221 } 222 } 223 } 224 } 225 226 public static void applyAllPropertyValues(DocumentModel src, DocumentModel dst) { 227 SchemaManager service = Framework.getService(SchemaManager.class); 228 DocumentType type = service.getDocumentType(src.getType()); 229 String[] schemas = type.getSchemaNames(); 230 for (String schemaName : schemas) { 231 Schema schema = service.getSchema(schemaName); 232 DataModelImpl dataModel = (DataModelImpl) dst.getDataModel(schemaName); 233 DataModelImpl fromDataModel = (DataModelImpl) src.getDataModel(schemaName); 234 for (Field field : schema.getFields()) { 235 String fieldName = field.getName().getLocalName(); 236 Serializable data = (Serializable) fromDataModel.getData(fieldName); 237 try { 238 if (!(dataModel.getDocumentPart().get(fieldName) instanceof BlobProperty)) { 239 dataModel.setData(fieldName, data); 240 } else { 241 dataModel.setData(fieldName, decodeBlob(data)); 242 } 243 } catch (PropertyNotFoundException e) { 244 continue; 245 } 246 } 247 } 248 } 249 250 private static Serializable decodeBlob(Serializable data) { 251 if (data instanceof Blob) { 252 return data; 253 } else { 254 return null; 255 } 256 } 257 258}