001/* 002 * (C) Copyright 2015-2016 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 * Ronan DANIELLOU <rdaniellou@nuxeo.com> 019 */ 020 021package org.nuxeo.ecm.core.io.marshallers.json.document; 022 023import static org.nuxeo.ecm.core.io.marshallers.json.document.DocumentModelJsonWriter.ENTITY_TYPE; 024import static org.nuxeo.ecm.core.io.registry.reflect.Instantiations.SINGLETON; 025import static org.nuxeo.ecm.core.io.registry.reflect.Priorities.REFERENCE; 026 027import java.io.ByteArrayOutputStream; 028import java.io.IOException; 029import java.util.Set; 030 031import org.apache.commons.logging.Log; 032import org.apache.commons.logging.LogFactory; 033import org.nuxeo.ecm.core.api.Blob; 034import org.nuxeo.ecm.core.api.CoreSession; 035import org.nuxeo.ecm.core.api.DocumentModel; 036import org.nuxeo.ecm.core.api.model.Property; 037import org.nuxeo.ecm.core.api.model.impl.ArrayProperty; 038import org.nuxeo.ecm.core.api.model.impl.ListProperty; 039import org.nuxeo.ecm.core.api.model.impl.primitives.BlobProperty; 040import org.nuxeo.ecm.core.io.download.DownloadService; 041import org.nuxeo.ecm.core.io.marshallers.json.AbstractJsonWriter; 042import org.nuxeo.ecm.core.io.registry.MarshallingException; 043import org.nuxeo.ecm.core.io.registry.reflect.Setup; 044import org.nuxeo.ecm.core.schema.types.ListType; 045import org.nuxeo.ecm.core.schema.types.SimpleType; 046import org.nuxeo.ecm.core.schema.types.Type; 047import org.nuxeo.ecm.core.schema.types.primitives.BinaryType; 048import org.nuxeo.ecm.core.schema.types.primitives.BooleanType; 049import org.nuxeo.ecm.core.schema.types.primitives.DoubleType; 050import org.nuxeo.ecm.core.schema.types.primitives.IntegerType; 051import org.nuxeo.ecm.core.schema.types.primitives.LongType; 052import org.nuxeo.ecm.core.schema.types.resolver.ObjectResolver; 053import org.nuxeo.runtime.api.Framework; 054 055import com.fasterxml.jackson.core.JsonGenerator; 056 057/** 058 * Convert {@link Property} to Json. 059 * <p> 060 * Format is: 061 * 062 * <pre> 063 * "stringPropertyValue" <-- for string property, each property may be fetched if a resolver is associated with that property and if a parameter fetch.document=propXPath is present, in this case, an object will be marshalled as value 064 * or 065 * true|false <- for boolean property 066 * or 067 * 123 <- for int property 068 * ... 069 * { <- for complex property 070 * "subProperty": ..., 071 * ... 072 * }, 073 * [ ... ] <- for list property 074 * } 075 * </pre> 076 * </p> 077 * 078 * @since 7.2 079 */ 080@Setup(mode = SINGLETON, priority = REFERENCE) 081public class DocumentPropertyJsonWriter extends AbstractJsonWriter<Property> { 082 083 private static final Log log = LogFactory.getLog(DocumentPropertyJsonWriter.class); 084 085 @Override 086 public void write(Property prop, JsonGenerator jg) throws IOException { 087 writeProperty(jg, prop); 088 jg.flush(); 089 } 090 091 protected void writeProperty(JsonGenerator jg, Property prop) throws IOException { 092 if (prop.isScalar()) { 093 writeScalarProperty(jg, prop); 094 } else if (prop.isList()) { 095 writeListProperty(jg, prop); 096 } else if (prop instanceof BlobProperty) { // a blob 097 writeBlobProperty(jg, prop); 098 } else if (prop.isComplex()) { 099 writeComplexProperty(jg, prop); 100 } else if (prop.isPhantom()) { 101 jg.writeNull(); 102 } 103 } 104 105 protected void writeScalarProperty(JsonGenerator jg, Property prop) throws IOException { 106 Type type = prop.getType(); 107 Object value = prop.getValue(); 108 if (!fetchProperty(jg, prop.getType().getObjectResolver(), value, prop.getXPath())) { 109 writeScalarPropertyValue(jg, ((SimpleType) type).getPrimitiveType(), value); 110 } 111 } 112 113 private void writeScalarPropertyValue(JsonGenerator jg, Type type, Object value) throws IOException { 114 if (value == null) { 115 jg.writeNull(); 116 } else if (type instanceof BooleanType) { 117 jg.writeBoolean((Boolean) value); 118 } else if (type instanceof LongType) { 119 jg.writeNumber(((Number) value).longValue()); // value may be a DeltaLong 120 } else if (type instanceof DoubleType) { 121 jg.writeNumber((Double) value); 122 } else if (type instanceof IntegerType) { 123 jg.writeNumber((Integer) value); 124 } else if (type instanceof BinaryType) { 125 jg.writeBinary((byte[]) value); 126 } else { 127 jg.writeString(type.encode(value)); 128 } 129 } 130 131 protected boolean fetchProperty(JsonGenerator jg, ObjectResolver resolver, Object value, String path) 132 throws IOException { 133 if (value == null) { 134 return false; 135 } 136 boolean fetched = false; 137 if (resolver != null) { 138 String genericPropertyPath = path.replaceAll("/[0-9]*/", "/*/"); 139 Set<String> fetchElements = ctx.getFetched(ENTITY_TYPE); 140 boolean fetch = false; 141 for (String fetchElement : fetchElements) { 142 if ("properties".equals(fetchElement) || path.startsWith(fetchElement) 143 || genericPropertyPath.startsWith(fetchElement)) { 144 fetch = true; 145 break; 146 } 147 } 148 if (fetch) { 149 // use the current doc's session as the resolver context to fetch properties 150 DocumentModel doc = ctx.getParameter(ENTITY_TYPE); 151 CoreSession context = doc == null ? null : doc.getCoreSession(); 152 Object object = resolver.fetch(value, context); 153 if (object != null) { 154 try { 155 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 156 writeEntity(object, baos); 157 jg.writeRawValue(baos.toString()); 158 fetched = true; 159 } catch (MarshallingException e) { 160 log.error("Unable to marshall as json the entity referenced by the property " + path, e); 161 } 162 } 163 } 164 } 165 return fetched; 166 } 167 168 protected void writeListProperty(JsonGenerator jg, Property prop) throws IOException { 169 jg.writeStartArray(); 170 if (prop instanceof ArrayProperty) { 171 Object[] ar = (Object[]) prop.getValue(); 172 if (ar == null) { 173 jg.writeEndArray(); 174 return; 175 } 176 Type itemType = ((ListType) prop.getType()).getFieldType(); 177 ObjectResolver resolver = itemType.getObjectResolver(); 178 String path = prop.getXPath(); 179 for (Object o : ar) { 180 if (!fetchProperty(jg, resolver, o, path)) { 181 writeScalarPropertyValue(jg, ((SimpleType) itemType).getPrimitiveType(), o); 182 } 183 } 184 } else { 185 ListProperty listp = (ListProperty) prop; 186 for (Property p : listp.getChildren()) { 187 writeProperty(jg, p); 188 } 189 } 190 jg.writeEndArray(); 191 } 192 193 protected void writeComplexProperty(JsonGenerator jg, Property prop) throws IOException { 194 jg.writeStartObject(); 195 for (Property p : prop.getChildren()) { 196 jg.writeFieldName(p.getName()); 197 writeProperty(jg, p); 198 } 199 jg.writeEndObject(); 200 } 201 202 protected void writeBlobProperty(JsonGenerator jg, Property prop) throws IOException { 203 Blob blob = (Blob) prop.getValue(); 204 if (blob == null) { 205 jg.writeNull(); 206 return; 207 } 208 jg.writeStartObject(); 209 String v = blob.getFilename(); 210 if (v == null) { 211 jg.writeNullField("name"); 212 } else { 213 jg.writeStringField("name", v); 214 } 215 v = blob.getMimeType(); 216 if (v == null) { 217 jg.writeNullField("mime-type"); 218 } else { 219 jg.writeStringField("mime-type", v); 220 } 221 v = blob.getEncoding(); 222 if (v == null) { 223 jg.writeNullField("encoding"); 224 } else { 225 jg.writeStringField("encoding", v); 226 } 227 v = blob.getDigestAlgorithm(); 228 if (v == null) { 229 jg.writeNullField("digestAlgorithm"); 230 } else { 231 jg.writeStringField("digestAlgorithm", v); 232 } 233 v = blob.getDigest(); 234 if (v == null) { 235 jg.writeNullField("digest"); 236 } else { 237 jg.writeStringField("digest", v); 238 } 239 jg.writeStringField("length", Long.toString(blob.getLength())); 240 241 String blobUrl = getBlobUrl(prop); 242 if (blobUrl == null) { 243 blobUrl = ""; 244 } 245 jg.writeStringField("data", blobUrl); 246 jg.writeEndObject(); 247 } 248 249 /** 250 * Gets the full URL of where a blob can be downloaded. 251 * 252 * @since 7.2 253 */ 254 private String getBlobUrl(Property prop) { 255 DocumentModel doc = ctx.getParameter(ENTITY_TYPE); 256 if (doc == null) { 257 return ""; 258 } 259 DownloadService downloadService = Framework.getService(DownloadService.class); 260 261 String xpath = prop.getXPath(); 262 // if no prefix, use schema name as prefix: 263 if (!xpath.contains(":")) { 264 xpath = prop.getSchema().getName() + ":" + xpath; 265 } 266 267 String filename = ((Blob) prop.getValue()).getFilename(); 268 return ctx.getBaseUrl() + downloadService.getDownloadUrl(doc, xpath, filename); 269 } 270 271}