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"  &lt;-- 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  &lt;- for boolean property
077 * or
078 * 123  &lt;- for int property
079 * ...
080 * {  &lt;- for complex property
081 *   "subProperty": ...,
082 *    ...
083 * },
084 * [ ... ] &lt;- 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}