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