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