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}