001/* 002 * (C) Copyright 2015-2020 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 */ 019package org.nuxeo.ecm.core.io.marshallers.json.document; 020 021import static org.nuxeo.ecm.core.io.registry.reflect.Instantiations.SINGLETON; 022import static org.nuxeo.ecm.core.io.registry.reflect.Priorities.REFERENCE; 023 024import java.io.IOException; 025import java.lang.reflect.Array; 026import java.util.ArrayList; 027import java.util.Calendar; 028import java.util.Iterator; 029import java.util.List; 030import java.util.Map.Entry; 031 032import javax.inject.Inject; 033 034import org.apache.logging.log4j.LogManager; 035import org.apache.logging.log4j.Logger; 036import org.nuxeo.ecm.core.api.Blob; 037import org.nuxeo.ecm.core.api.model.Property; 038import org.nuxeo.ecm.core.api.model.impl.ArrayProperty; 039import org.nuxeo.ecm.core.api.model.impl.ComplexProperty; 040import org.nuxeo.ecm.core.api.model.impl.DocumentPartImpl; 041import org.nuxeo.ecm.core.api.model.impl.PropertyFactory; 042import org.nuxeo.ecm.core.api.model.impl.primitives.BlobProperty; 043import org.nuxeo.ecm.core.io.marshallers.json.AbstractJsonReader; 044import org.nuxeo.ecm.core.io.registry.MarshallingException; 045import org.nuxeo.ecm.core.io.registry.reflect.Setup; 046import org.nuxeo.ecm.core.schema.SchemaManager; 047import org.nuxeo.ecm.core.schema.types.ComplexType; 048import org.nuxeo.ecm.core.schema.types.Field; 049import org.nuxeo.ecm.core.schema.types.ListType; 050import org.nuxeo.ecm.core.schema.types.Schema; 051import org.nuxeo.ecm.core.schema.types.SimpleType; 052import org.nuxeo.ecm.core.schema.types.Type; 053import org.nuxeo.ecm.core.schema.types.primitives.BinaryType; 054import org.nuxeo.ecm.core.schema.types.primitives.BooleanType; 055import org.nuxeo.ecm.core.schema.types.primitives.DateType; 056import org.nuxeo.ecm.core.schema.types.primitives.DoubleType; 057import org.nuxeo.ecm.core.schema.types.primitives.IntegerType; 058import org.nuxeo.ecm.core.schema.types.primitives.LongType; 059import org.nuxeo.ecm.core.schema.types.primitives.StringType; 060import org.nuxeo.ecm.core.schema.types.resolver.ObjectResolver; 061 062import com.fasterxml.jackson.databind.JsonNode; 063 064/** 065 * Convert Json as {@link List}<{@link Property}>. 066 * <p> 067 * Format is: 068 * 069 * <pre> 070 * { 071 * "schema1Prefix:stringProperty": "stringPropertyValue", <-- each property may be marshall as object if a resolver is associated with that property and if a marshaller exists for the object, in this case, the resulting property will have the corresponding reference value. 072 * "schema1Prefix:booleanProperty": true|false, 073 * "schema2Prefix:integerProperty": 123, 074 * ... 075 * "schema3Prefix:complexProperty": { 076 * "subProperty": ..., 077 * ... 078 * }, 079 * "schema4Prefix:listProperty": [ 080 * ... 081 * ] 082 * } 083 * </pre> 084 * 085 * @since 7.2 086 */ 087@Setup(mode = SINGLETON, priority = REFERENCE) 088public class DocumentPropertiesJsonReader extends AbstractJsonReader<List<Property>> { 089 090 private static final Logger log = LogManager.getLogger(DocumentPropertiesJsonReader.class); 091 092 public static final String DEFAULT_SCHEMA_NAME = "DEFAULT_SCHEMA_NAME"; 093 094 /** @since 11.2 */ 095 public static final String FALLBACK_RESOLVER = "resolver."; 096 097 @Inject 098 private SchemaManager schemaManager; 099 100 @Override 101 public List<Property> read(JsonNode jn) throws IOException { 102 List<Property> properties = new ArrayList<>(); 103 Iterator<Entry<String, JsonNode>> propertyNodes = jn.fields(); 104 while (propertyNodes.hasNext()) { 105 Entry<String, JsonNode> propertyNode = propertyNodes.next(); 106 String propertyName = propertyNode.getKey(); 107 Field field; 108 Property parent; 109 if (propertyName.contains(":")) { 110 field = schemaManager.getField(propertyName); 111 if (field == null) { 112 continue; 113 } 114 parent = new DocumentPartImpl(field.getDeclaringType().getSchema()); 115 } else { 116 String shemaName = ctx.getParameter(DEFAULT_SCHEMA_NAME); 117 Schema schema = schemaManager.getSchema(shemaName); 118 if (schema == null) { 119 continue; 120 } 121 field = schema.getField(propertyName); 122 parent = new DocumentPartImpl(schema); 123 } 124 if (field == null) { 125 continue; 126 } 127 Property property = readProperty(parent, field, propertyNode.getValue()); 128 if (property != null) { 129 properties.add(property); 130 } 131 } 132 return properties; 133 } 134 135 protected Property readProperty(Property parent, Field field, JsonNode jn) throws IOException { 136 Property property = PropertyFactory.createProperty(parent, field, 0); 137 property.setForceDirty(true); 138 if (jn.isNull()) { 139 property.setValue(null); 140 } else if (property.isScalar()) { 141 fillScalarProperty(property, jn); 142 } else if (property.isList()) { 143 fillListProperty(property, jn); 144 } else { 145 if (!(property instanceof BlobProperty)) { 146 fillComplexProperty(property, jn); 147 } else { 148 Blob blob = readEntity(Blob.class, Blob.class, jn); 149 property.setValue(blob); 150 } 151 } 152 property.setForceDirty(false); 153 return property; 154 } 155 156 private void fillScalarProperty(Property property, JsonNode jn) throws IOException { 157 if ((property instanceof ArrayProperty) && jn.isArray()) { 158 List<Object> values = new ArrayList<>(); 159 Iterator<JsonNode> it = jn.elements(); 160 JsonNode item; 161 Type fieldType = ((ListType) property.getType()).getFieldType(); 162 while (it.hasNext()) { 163 item = it.next(); 164 values.add(getScalarPropertyValue(property, item, fieldType)); 165 } 166 property.setValue(castArrayPropertyValue(((SimpleType) fieldType).getPrimitiveType(), values)); 167 } else { 168 property.setValue(getScalarPropertyValue(property, jn, property.getType())); 169 } 170 } 171 172 @SuppressWarnings({ "unchecked" }) 173 private <T> T[] castArrayPropertyValue(Type type, List<Object> values) { 174 if (type instanceof StringType) { 175 return values.toArray((T[]) Array.newInstance(String.class, values.size())); 176 } else if (type instanceof BooleanType) { 177 return values.toArray((T[]) Array.newInstance(Boolean.class, values.size())); 178 } else if (type instanceof LongType) { 179 return values.toArray((T[]) Array.newInstance(Long.class, values.size())); 180 } else if (type instanceof DoubleType) { 181 return values.toArray((T[]) Array.newInstance(Double.class, values.size())); 182 } else if (type instanceof IntegerType) { 183 return values.toArray((T[]) Array.newInstance(Integer.class, values.size())); 184 } else if (type instanceof BinaryType) { 185 return values.toArray((T[]) Array.newInstance(Byte.class, values.size())); 186 } else if (type instanceof DateType) { 187 return values.toArray((T[]) Array.newInstance(Calendar.class, values.size())); 188 } 189 throw new MarshallingException("Primitive type not found: " + type.getName()); 190 } 191 192 private Object getScalarPropertyValue(Property property, JsonNode jn, Type type) throws IOException { 193 Object value; 194 if (jn.isObject()) { 195 ObjectResolver resolver = type.getObjectResolver(); 196 if (resolver == null) { 197 // fallback on resolver present in rendering context (for example xvocabulary parent field) 198 resolver = ctx.getParameter(FALLBACK_RESOLVER + property.getName()); 199 } 200 if (resolver == null) { 201 // Let's assume it is a blob of which content has to be stored in a string property. 202 if (type.getSuperType() instanceof StringType) { 203 Blob blob = readEntity(Blob.class, Blob.class, jn); 204 if (blob != null) { 205 return blob.getString(); 206 } 207 } 208 throw new MarshallingException("Unable to parse the property " + property.getXPath()); 209 } 210 Object object = null; 211 for (Class<?> clazz : resolver.getManagedClasses()) { 212 try { 213 object = readEntity(clazz, clazz, jn); 214 if (object != null) { 215 break; 216 } 217 } catch (MarshallingException e) { 218 log.info("Unable to read the entity - {}", e::getMessage, () -> e); 219 } 220 } 221 if (object == null) { 222 throw new MarshallingException("Unable to parse the property " + property.getXPath()); 223 } 224 value = resolver.getReference(object); 225 if (value == null) { 226 throw new MarshallingException("Property " + property.getXPath() 227 + " value cannot be resolved by the matching resolver " + resolver.getName()); 228 } 229 } else { 230 value = getPropertyValue(((SimpleType) type).getPrimitiveType(), jn); 231 } 232 return value; 233 } 234 235 private Object getPropertyValue(Type type, JsonNode jn) throws IOException { 236 Object value; 237 if (jn.isNull()) { 238 value = null; 239 } else if (type instanceof BooleanType) { 240 value = jn.asBoolean(); 241 } else if (type instanceof LongType) { 242 value = jn.asLong(); 243 } else if (type instanceof DoubleType) { 244 value = jn.asDouble(); 245 } else if (type instanceof IntegerType) { 246 value = jn.asInt(); 247 } else if (type instanceof BinaryType) { 248 value = jn.binaryValue(); 249 } else { 250 value = type.decode(jn.asText()); 251 } 252 return value; 253 } 254 255 private void fillListProperty(Property property, JsonNode jn) throws IOException { 256 ListType listType = (ListType) property.getType(); 257 if (property instanceof ArrayProperty) { 258 fillScalarProperty(property, jn); 259 } else if (jn.isEmpty()) { 260 property.setValue(null); 261 } else { 262 JsonNode elNode; 263 Iterator<JsonNode> it = jn.elements(); 264 while (it.hasNext()) { 265 elNode = it.next(); 266 Property child = readProperty(property, listType.getField(), elNode); 267 property.addValue(child.getValue()); 268 } 269 } 270 } 271 272 private void fillComplexProperty(Property property, JsonNode jn) throws IOException { 273 Entry<String, JsonNode> elNode; 274 Iterator<Entry<String, JsonNode>> it = jn.fields(); 275 ComplexProperty complexProperty = (ComplexProperty) property; 276 ComplexType type = complexProperty.getType(); 277 while (it.hasNext()) { 278 elNode = it.next(); 279 String elName = elNode.getKey(); 280 Field field = type.getField(elName); 281 if (field != null) { 282 Property child = readProperty(property, field, elNode.getValue()); 283 property.set(elName, child); 284 } 285 } 286 } 287 288}