001/*
002 * (C) Copyright 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 *     Nicolas Chapurlat <nchapurlat@nuxeo.com>
018 */
019
020package org.nuxeo.ecm.core.io.marshallers.json;
021
022import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
023import static javax.ws.rs.core.MediaType.APPLICATION_JSON_TYPE;
024
025import java.io.IOException;
026import java.io.OutputStream;
027import java.io.Serializable;
028import java.lang.reflect.Type;
029import java.util.Collection;
030import java.util.Map;
031import java.util.Map.Entry;
032
033import javax.inject.Inject;
034import javax.ws.rs.core.MediaType;
035
036import org.nuxeo.ecm.core.io.registry.MarshallerRegistry;
037import org.nuxeo.ecm.core.io.registry.MarshallingException;
038import org.nuxeo.ecm.core.io.registry.Writer;
039import org.nuxeo.ecm.core.io.registry.context.RenderingContext;
040import org.nuxeo.ecm.core.io.registry.reflect.Supports;
041
042import com.fasterxml.jackson.core.JsonGenerator;
043
044/**
045 * Base class for Json {@link Writer}.
046 * <p>
047 * This class provides a easy way to produce json and also provides the current context: {@link AbstractJsonWriter#ctx}.
048 * It provides you a {@link JsonGenerator} to manage the marshalling.
049 * </p>
050 * <p>
051 * The use of this class optimize the JsonFactory usage especially when aggregating marshallers.
052 * </p>
053 *
054 * @param <EntityType> The Java type to marshall as Json.
055 * @since 7.2
056 */
057@Supports(APPLICATION_JSON)
058public abstract class AbstractJsonWriter<EntityType> implements Writer<EntityType> {
059
060    /**
061     * The current {@link RenderingContext}.
062     */
063    @Inject
064    protected RenderingContext ctx;
065
066    /**
067     * The marshaller registry.
068     */
069    @Inject
070    protected MarshallerRegistry registry;
071
072    @Override
073    public boolean accept(Class<?> clazz, Type genericType, MediaType mediatype) {
074        return true;
075    }
076
077    @Override
078    public void write(EntityType entity, Class<?> clazz, Type genericType, MediaType mediatype, OutputStream out)
079            throws IOException {
080        JsonGenerator jg = getGenerator(out, true);
081        write(entity, jg);
082        jg.flush();
083    }
084
085    /**
086     * Implement this method to writes the entity in the provided {@link JsonGenerator}.
087     * <p>
088     * This method implementation can use injected properties.
089     * </p>
090     * <p>
091     * The {@link JsonGenerator}'s flushing is done by this abstract class, it's also not not necessary to flush it. Do
092     * not close the provided {@link JsonGenerator}. It may be used is another marshaller calling this one.
093     * </p>
094     *
095     * @param entity The entity to marshall as Json.
096     * @param jg The {@link JsonGenerator} used to produce Json output.
097     * @since 7.2
098     */
099    public abstract void write(EntityType entity, JsonGenerator jg) throws IOException;
100
101    /**
102     * Delegates writing of an entity to the {@link MarshallerRegistry}. This will work if a Json {@link Writer} is
103     * registered in the registry for the given clazz.
104     *
105     * @param fieldName The name of the Json field in which the entity will be wrote.
106     * @param entity The entity to write.
107     * @param jg The {@link JsonGenerator} used to write the given entity.
108     * @since 7.2
109     */
110    protected void writeEntityField(String fieldName, Object entity, JsonGenerator jg) throws IOException {
111        jg.writeFieldName(fieldName);
112        writeEntity(entity, jg);
113    }
114
115    /**
116     * Delegates writing of an entity to the {@link MarshallerRegistry}. This will work if a Json {@link Writer} is
117     * registered in the registry for the given clazz.
118     *
119     * @param entity The entity to write.
120     * @param jg The {@link JsonGenerator} used to write the given entity.
121     * @since 7.2
122     */
123    protected void writeEntity(Object entity, JsonGenerator jg) throws IOException {
124        writeEntity(entity, new OutputStreamWithJsonWriter(jg));
125    }
126
127    /**
128     * Delegates writing of an entity to the {@link MarshallerRegistry}. This will work if a Json {@link Writer} is
129     * registered in the registry for the given clazz.
130     *
131     * @param entity The entity to write.
132     * @param out The {@link OutputStream} in which the given entity will be wrote.
133     * @throws IOException If some i/o error append while writing entity.
134     * @since 7.2
135     */
136    protected <ObjectType> void writeEntity(ObjectType entity, OutputStream out) throws IOException {
137        @SuppressWarnings("unchecked")
138        Class<ObjectType> clazz = (Class<ObjectType>) entity.getClass();
139        Writer<ObjectType> writer = registry.getWriter(ctx, clazz, APPLICATION_JSON_TYPE);
140        if (writer == null) {
141            throw new MarshallingException("Unable to get a writer for Java type " + entity.getClass()
142                    + " and mimetype " + APPLICATION_JSON_TYPE);
143        }
144        writer.write(entity, entity.getClass(), entity.getClass(), APPLICATION_JSON_TYPE, out);
145    }
146
147    /**
148     * Get the current Json generator or create it if none was found.
149     *
150     * @param out The {@link OutputStream} on which the generator will generate Json.
151     * @param getCurrentIfAvailable If true, try to get the current generator in the context.
152     * @return The created generator.
153     * @since 7.2
154     */
155    protected JsonGenerator getGenerator(OutputStream out, boolean getCurrentIfAvailable) throws IOException {
156        if (getCurrentIfAvailable && out instanceof OutputStreamWithJsonWriter) {
157            OutputStreamWithJsonWriter casted = (OutputStreamWithJsonWriter) out;
158            return casted.getJsonGenerator();
159        }
160        return JsonFactoryProvider.get().createGenerator(out);
161    }
162
163    /**
164     * Writes a list of {@link Serializable}.
165     *
166     * @param fieldName The name of the Json field in which the serializables will be wrote.
167     * @param serializables The serializables to write.
168     * @param jg The {@link JsonGenerator} used to write the given serializables.
169     * @since 10.1
170     */
171    protected <T extends Serializable> void writeSerializableListField(String fieldName, Collection<T> serializables,
172            JsonGenerator jg) throws IOException {
173        jg.writeArrayFieldStart(fieldName);
174        for (T serializable : serializables) {
175            writeSerializable(serializable, jg);
176        }
177        jg.writeEndArray();
178    }
179
180    /**
181     * Writes a map whose values are {@link Serializable}.
182     *
183     * @param fieldName The name of the Json field in which the serializables will be wrote.
184     * @param map The map to write.
185     * @param jg The {@link JsonGenerator} used to write the given map.
186     * @since 10.1
187     */
188    protected <T extends Serializable> void writeSerializableMapField(String fieldName, Map<String, T> map,
189            JsonGenerator jg) throws IOException {
190        jg.writeObjectFieldStart(fieldName);
191        for (Entry<String, T> entry : map.entrySet()) {
192            writeSerializableField(entry.getKey(), entry.getValue(), jg);
193        }
194        jg.writeEndObject();
195    }
196
197    /**
198     * Writes a {@link Serializable}.
199     * <p>
200     * This method will first try to cast value to {@link Collection}, array, {@link String}, {@link Boolean} and
201     * {@link Number}. If none of previous cast could work, try to write it with marshallers
202     *
203     * @param fieldName The name of the Json field in which the serializable will be wrote.
204     * @param value The value to write.
205     * @param jg The {@link JsonGenerator} used to write the given serializable.
206     * @since 10.1
207     */
208    protected void writeSerializableField(String fieldName, Serializable value, JsonGenerator jg) throws IOException {
209        jg.writeFieldName(fieldName);
210        writeSerializable(value, jg);
211    }
212
213    /**
214     * Writes a {@link Serializable}.
215     * <p>
216     * This method will first try to cast value to {@link Collection}, array, {@link String}, {@link Boolean} and
217     * {@link Number}. If none of previous cast could work, try to write it with marshallers
218     *
219     * @param value The value to write.
220     * @param jg The {@link JsonGenerator} used to write the given serializable.
221     * @since 10.1
222     */
223    @SuppressWarnings("unchecked")
224    protected void writeSerializable(Serializable value, JsonGenerator jg) throws IOException {
225        if (value instanceof Collection) {
226            jg.writeStartArray();
227            for (Serializable serializable : (Collection<Serializable>) value) {
228                writeSerializable(serializable, jg);
229            }
230            jg.writeEndArray();
231        } else if (value instanceof Serializable[]) {
232            jg.writeStartArray();
233            for (Serializable serializable : (Serializable[]) value) {
234                writeSerializable(serializable, jg);
235            }
236            jg.writeEndArray();
237        } else if (value instanceof String) {
238            jg.writeString((String) value);
239        } else if (value instanceof Boolean) {
240            jg.writeBoolean((boolean) value);
241        } else if (value instanceof Number) {
242            jg.writeNumber(value.toString());
243        } else {
244            // try with marshallers
245            writeEntity(value, jg);
246        }
247    }
248
249}