001/*
002 * (C) Copyright 2006-2015 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 *    Bogdan Stefanescu
018 */
019package org.nuxeo.ecm.automation.core.util;
020
021import java.io.IOException;
022import java.io.UnsupportedEncodingException;
023import java.util.Calendar;
024import java.util.Date;
025
026import org.codehaus.jackson.JsonGenerationException;
027import org.codehaus.jackson.JsonGenerator;
028import org.nuxeo.common.utils.URIUtils;
029import org.nuxeo.ecm.core.api.Blob;
030import org.nuxeo.ecm.core.api.PropertyException;
031import org.nuxeo.ecm.core.api.model.Property;
032import org.nuxeo.ecm.core.api.model.impl.ArrayProperty;
033import org.nuxeo.ecm.core.api.model.impl.ComplexProperty;
034import org.nuxeo.ecm.core.api.model.impl.ListProperty;
035import org.nuxeo.ecm.core.api.model.impl.primitives.BlobProperty;
036import org.nuxeo.ecm.core.schema.types.ListType;
037import org.nuxeo.ecm.core.schema.types.Type;
038import org.nuxeo.ecm.core.schema.types.primitives.BinaryType;
039import org.nuxeo.ecm.core.schema.types.primitives.BooleanType;
040import org.nuxeo.ecm.core.schema.types.primitives.DateType;
041import org.nuxeo.ecm.core.schema.types.primitives.DoubleType;
042import org.nuxeo.ecm.core.schema.types.primitives.IntegerType;
043import org.nuxeo.ecm.core.schema.types.primitives.LongType;
044
045/**
046 * Helper to marshaling properties into JSON.
047 *
048 * @since 7.1
049 */
050public class JSONPropertyWriter {
051
052    /**
053     * The date time format.
054     * 
055     * @since 9.1
056     */
057    protected DateTimeFormat dateTimeFormat = DateTimeFormat.W3C;
058
059    /**
060     * The baseUrl that can be used to locate blob content.
061     * 
062     * @since 9.1
063     */
064    protected String filesBaseUrl;
065
066    /**
067     * The prefix to append to field name.
068     * 
069     * @since 9.1
070     */
071    protected String prefix;
072
073    /**
074     * Whether or not this writer write null values.
075     * 
076     * @since 9.1
077     */
078    protected boolean writeNull = true;
079
080    /**
081     * Whether or not this writer write empty list or object.
082     * 
083     * @since 9.1
084     */
085    protected boolean writeEmpty = true;
086
087    /**
088     * Instantiate a JSONPropertyWriter.
089     */
090    protected JSONPropertyWriter() {
091        // Default constructor
092    }
093
094    /**
095     * Copy constructor.
096     *
097     * @since 9.1
098     */
099    protected JSONPropertyWriter(JSONPropertyWriter writer) {
100        this.dateTimeFormat = writer.dateTimeFormat;
101        this.filesBaseUrl = writer.filesBaseUrl;
102        this.prefix = writer.prefix;
103        this.writeNull = writer.writeNull;
104    }
105
106    /**
107     * @return a {@link JSONPropertyWriter} instance with {@link DateTimeFormat#W3C} as date time formatter.
108     * @since 9.1
109     */
110    public static JSONPropertyWriter create() {
111        return new JSONPropertyWriter();
112    }
113
114    /**
115     * @return this {@link JSONPropertyWriter} filled with the previous configuration and the input dateTimeFormat.
116     * @since 9.1
117     */
118    public JSONPropertyWriter dateTimeFormat(DateTimeFormat dateTimeFormat) {
119        this.dateTimeFormat = dateTimeFormat;
120        return this;
121    }
122
123    /**
124     * @param filesBaseUrl the baseUrl that can be used to locate blob content
125     * @return this {@link JSONPropertyWriter} filled with the previous configuration and the input filesBaseUrl.
126     * @since 9.1
127     */
128    public JSONPropertyWriter filesBaseUrl(String filesBaseUrl) {
129        this.filesBaseUrl = filesBaseUrl;
130        if (this.filesBaseUrl != null && !this.filesBaseUrl.endsWith("/")) {
131            this.filesBaseUrl += "/";
132        }
133        return this;
134    }
135
136    /**
137     * @param prefix the prefix to append for each property
138     * @return this {@link JSONPropertyWriter} filled with the previous configuration and the input prefix.
139     * @since 9.1
140     */
141    public JSONPropertyWriter prefix(String prefix) {
142        this.prefix = prefix;
143        return this;
144    }
145
146    /**
147     * @param writeNull whether or not this writer might write null values
148     * @return this {@link JSONPropertyWriter} filled with the previous configuration and the input writeNull value.
149     * @since 9.1
150     */
151    public JSONPropertyWriter writeNull(boolean writeNull) {
152        this.writeNull = writeNull;
153        return this;
154    }
155
156    /**
157     * @param writeEmpty whether or not this writer might write empty array/list/object
158     * @return this {@link JSONPropertyWriter} filled with the previous configuration and the input writeEmpty value.
159     * @since 9.1
160     */
161    public JSONPropertyWriter writeEmpty(boolean writeEmpty) {
162        this.writeEmpty = writeEmpty;
163        return this;
164    }
165
166    /**
167     * Converts the value of the given core property to JSON.
168     * <p />
169     * CAUTION: this method will write the field name to {@link JsonGenerator} with its prefix without writing the start
170     * and the end of object.
171     *
172     * @since 9.1
173     */
174    public void writeProperty(JsonGenerator jg, Property prop)
175            throws PropertyException, JsonGenerationException, IOException {
176        PropertyConsumer fieldNameWriter;
177        if (prefix == null) {
178            fieldNameWriter = (j, p) -> j.writeFieldName(p.getName());
179        } else {
180            fieldNameWriter = (j, p) -> j.writeFieldName(prefix + ':' + p.getField().getName().getLocalName());
181        }
182        writeProperty(jg, prop, fieldNameWriter);
183    }
184
185    /**
186     * Converts the value of the given core property to JSON.
187     *
188     * @param fieldNameWriter the field name writer is used to write the field name depending on writer configuration,
189     *            this parameter also allows us to handle different cases: field with prefix, field under complex
190     *            property, or nothing for arrays and lists
191     */
192    protected void writeProperty(JsonGenerator jg, Property prop, PropertyConsumer fieldNameWriter)
193            throws PropertyException, JsonGenerationException, IOException {
194        if (prop.isScalar()) {
195            writeScalarProperty(jg, prop, fieldNameWriter);
196        } else if (prop.isList()) {
197            writeListProperty(jg, prop, fieldNameWriter);
198        } else {
199            if (prop.isPhantom()) {
200                if (writeNull) {
201                    fieldNameWriter.accept(jg, prop);
202                    jg.writeNull();
203                }
204            } else if (prop instanceof BlobProperty) { // a blob
205                writeBlobProperty(jg, prop, fieldNameWriter);
206            } else { // a complex property
207                writeMapProperty(jg, (ComplexProperty) prop, fieldNameWriter);
208            }
209        }
210    }
211
212    protected void writeScalarProperty(JsonGenerator jg, Property prop, PropertyConsumer fieldNameWriter)
213            throws PropertyException, JsonGenerationException, IOException {
214        Type type = prop.getType();
215        Object v = prop.getValue();
216        if (v == null) {
217            if (writeNull) {
218                fieldNameWriter.accept(jg, prop);
219                jg.writeNull();
220            }
221        } else {
222            fieldNameWriter.accept(jg, prop);
223            if (type instanceof BooleanType) {
224                jg.writeBoolean((Boolean) v);
225            } else if (type instanceof LongType) {
226                jg.writeNumber((Long) v);
227            } else if (type instanceof DoubleType) {
228                jg.writeNumber((Double) v);
229            } else if (type instanceof IntegerType) {
230                jg.writeNumber((Integer) v);
231            } else if (type instanceof BinaryType) {
232                jg.writeBinary((byte[]) v);
233            } else if (type instanceof DateType && dateTimeFormat == DateTimeFormat.TIME_IN_MILLIS) {
234                if (v instanceof Date) {
235                    jg.writeNumber(((Date) v).getTime());
236                } else if (v instanceof Calendar) {
237                    jg.writeNumber(((Calendar) v).getTimeInMillis());
238                } else {
239                    throw new PropertyException("Unknown class for DateType: " + v.getClass().getName() + ", " + v);
240                }
241            } else {
242                jg.writeString(type.encode(v));
243            }
244        }
245    }
246
247    protected void writeListProperty(JsonGenerator jg, Property prop, PropertyConsumer fieldNameWriter)
248            throws PropertyException, JsonGenerationException, IOException {
249        // test if array/list is empty - don't write empty case
250        if (!writeEmpty && (prop == null || (prop instanceof ArrayProperty && prop.getValue() == null)
251                || (prop instanceof ListProperty && prop.getChildren().isEmpty()))) {
252            return;
253        }
254        fieldNameWriter.accept(jg, prop);
255        jg.writeStartArray();
256        if (prop instanceof ArrayProperty) {
257            Object[] ar = (Object[]) prop.getValue();
258            if (ar == null) {
259                jg.writeEndArray();
260                return;
261            }
262            Type type = ((ListType) prop.getType()).getFieldType();
263            for (Object o : ar) {
264                jg.writeString(type.encode(o));
265            }
266        } else {
267            for (Property p : prop.getChildren()) {
268                // it's a list of complex object, don't write field names
269                writeProperty(jg, p, PropertyConsumer.nothing());
270            }
271        }
272        jg.writeEndArray();
273    }
274
275    protected void writeMapProperty(JsonGenerator jg, ComplexProperty prop, PropertyConsumer fieldNameWriter)
276            throws PropertyException, JsonGenerationException, IOException {
277        if (!writeEmpty && (prop == null || prop.getChildren().isEmpty())) {
278            return;
279        }
280        fieldNameWriter.accept(jg, prop);
281        jg.writeStartObject();
282        PropertyConsumer childFieldWriter = (j, p) -> j.writeFieldName(p.getName());
283        for (Property p : prop.getChildren()) {
284            writeProperty(jg, p, childFieldWriter);
285        }
286        jg.writeEndObject();
287    }
288
289    protected void writeBlobProperty(JsonGenerator jg, Property prop, PropertyConsumer fieldNameWriter)
290            throws PropertyException, JsonGenerationException, IOException {
291        Blob blob = (Blob) prop.getValue();
292        if (blob == null) {
293            if (writeNull) {
294                fieldNameWriter.accept(jg, prop);
295                jg.writeNull();
296            }
297        } else {
298            fieldNameWriter.accept(jg, prop);
299            jg.writeStartObject();
300            String v = blob.getFilename();
301            if (v == null) {
302                if (writeNull) {
303                    jg.writeNullField("name");
304                }
305            } else {
306                jg.writeStringField("name", v);
307            }
308            v = blob.getMimeType();
309            if (v == null) {
310                if (writeNull) {
311                    jg.writeNullField("mime-type");
312                }
313            } else {
314                jg.writeStringField("mime-type", v);
315            }
316            v = blob.getEncoding();
317            if (v == null) {
318                if (writeNull) {
319                    jg.writeNullField("encoding");
320                }
321            } else {
322                jg.writeStringField("encoding", v);
323            }
324            v = blob.getDigest();
325            if (v == null) {
326                if (writeNull) {
327                    jg.writeNullField("digest");
328                }
329            } else {
330                jg.writeStringField("digest", v);
331            }
332            jg.writeNumberField("length", blob.getLength());
333            if (filesBaseUrl != null) {
334                jg.writeStringField("data", getBlobUrl(prop, filesBaseUrl));
335            }
336            jg.writeEndObject();
337        }
338    }
339
340    /**
341     * Gets the full URL of where a blob can be downloaded.
342     *
343     * @since 5.9.3
344     */
345    private static String getBlobUrl(Property prop, String filesBaseUrl)
346            throws UnsupportedEncodingException, PropertyException {
347        StringBuilder blobUrlBuilder = new StringBuilder(filesBaseUrl);
348        String xpath = prop.getXPath();
349        if (!xpath.contains(":")) {
350            // if no prefix, use schema name as prefix:
351            xpath = prop.getSchema().getName() + ":" + xpath;
352        }
353        blobUrlBuilder.append(xpath);
354        blobUrlBuilder.append("/");
355        String filename = ((Blob) prop.getValue()).getFilename();
356        if (filename != null) {
357            blobUrlBuilder.append(URIUtils.quoteURIPathComponent(filename, true));
358        }
359        return blobUrlBuilder.toString();
360    }
361
362    /**
363     * Converts the value of the given core property to JSON. The given filesBaseUrl is the baseUrl that can be used to
364     * locate blob content and is useful to generate blob URLs.
365     */
366    public static void writePropertyValue(JsonGenerator jg, Property prop, DateTimeFormat dateTimeFormat,
367            String filesBaseUrl) throws PropertyException, JsonGenerationException, IOException {
368        JSONPropertyWriter writer = create().dateTimeFormat(dateTimeFormat).filesBaseUrl(filesBaseUrl);
369        // as we just want to write property value, give a nothing consumer
370        writer.writeProperty(jg, prop, PropertyConsumer.nothing());
371    }
372
373    @FunctionalInterface
374    public interface PropertyConsumer {
375
376        void accept(JsonGenerator jg, Property prop) throws JsonGenerationException, IOException;
377
378        static PropertyConsumer nothing() {
379            return (jg, prop) -> {
380            };
381        }
382
383    }
384
385}