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}