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