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