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.InputStream;
027import java.lang.reflect.Type;
028import java.util.ArrayList;
029import java.util.List;
030
031import javax.inject.Inject;
032import javax.ws.rs.core.MediaType;
033
034import org.nuxeo.ecm.core.io.registry.MarshallerRegistry;
035import org.nuxeo.ecm.core.io.registry.Reader;
036import org.nuxeo.ecm.core.io.registry.context.RenderingContext;
037import org.nuxeo.ecm.core.io.registry.reflect.Supports;
038
039import com.fasterxml.jackson.core.JsonParseException;
040import com.fasterxml.jackson.core.JsonParser;
041import com.fasterxml.jackson.core.JsonProcessingException;
042import com.fasterxml.jackson.databind.JsonNode;
043
044/**
045 * Base class for Json {@link Reader}.
046 * <p>
047 * This class provides an easy way to create java object from json and also provides the current context:
048 * {@link AbstractJsonReader#ctx}. It provides you a {@link JsonNode} to manage the unmarshalling.
049 * </p>
050 * <p>
051 * The use of this class optimize the JsonFactory usage especially when aggregating unmarshallers.
052 * </p>
053 *
054 * @param <EntityType> The expected Java type.
055 * @since 7.2
056 */
057@Supports(APPLICATION_JSON)
058public abstract class AbstractJsonReader<EntityType> implements Reader<EntityType> {
059
060    /**
061     * The current {@link RenderingContext}.
062     */
063    @Inject
064    protected RenderingContext ctx;
065
066    /**
067     * The marshaller registry. You may use it to use other marshallers.
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 EntityType read(Class<?> clazz, Type genericType, MediaType mediaType, InputStream in) throws IOException {
079        JsonNode jn = getNode(in, true);
080        return read(jn);
081    }
082
083    /**
084     * Provide a {@link JsonNode}, try to get it from the context.
085     *
086     * @param in The current {@link InputStream}.
087     * @param getCurrentIfAvailable If true, try to get it from the context (if another marshaller already create it and
088     *            call this marshaller).
089     * @return A valid {@link JsonNode}.
090     * @since 7.2
091     */
092    protected JsonNode getNode(InputStream in, boolean getCurrentIfAvailable)
093            throws IOException, JsonParseException, JsonProcessingException {
094        if (getCurrentIfAvailable && in instanceof InputStreamWithJsonNode) {
095            return ((InputStreamWithJsonNode) in).getJsonNode();
096        }
097        try (JsonParser jp = JsonFactoryProvider.get().createParser(in)) {
098            return jp.readValueAsTree();
099        }
100    }
101
102    /**
103     * Implement this method, read the entity data in the provided {@link JsonNode} and return corresponding java
104     * object.
105     *
106     * @param jn A ready to use {@link JsonNode}.
107     * @return The unmarshalled entity.
108     * @since 7.2
109     */
110    public abstract EntityType read(JsonNode jn) throws IOException;
111
112    /**
113     * Use this method to delegate the unmarshalling of a part or your Json to the {@link MarshallerRegistry}. This will
114     * work only if a Json {@link Reader} is registered for the provided clazz and if the node format is the same as the
115     * one expected by the marshaller.
116     *
117     * @param clazz The expected Java class.
118     * @param genericType The generic type of the expected object: usefull if it's a generic List for example (use
119     *            TypeUtils to create the parametrize type).
120     * @param jn The {@link JsonNode} to unmarshall.
121     * @return An object implementing the expected clazz.
122     * @since 7.2
123     */
124    @SuppressWarnings("unchecked")
125    protected <T> T readEntity(Class<?> clazz, Type genericType, JsonNode jn) throws IOException {
126        Type effectiveGenericType = genericType != null ? genericType : clazz;
127        Reader<T> reader = (Reader<T>) registry.getReader(ctx, clazz, effectiveGenericType, APPLICATION_JSON_TYPE);
128        return reader.read(clazz, effectiveGenericType, APPLICATION_JSON_TYPE, new InputStreamWithJsonNode(jn));
129    }
130
131    /**
132     * Try to get a string property of the given {@link JsonNode}. Return null if the node is null.
133     *
134     * @param jn The {@link JsonNode} to parse.
135     * @param elName The property name.
136     * @return The property text if it exists and it's a text, null otherwise.
137     * @since 7.2
138     */
139    protected String getStringField(JsonNode jn, String elName) {
140        JsonNode elNode = jn.get(elName);
141        if (elNode != null && !elNode.isNull() && elNode.isTextual()) {
142            return elNode.textValue();
143        } else {
144            return null;
145        }
146    }
147
148    /**
149     * Tries to get a boolean property of the given {@link JsonNode}. Return {@code null} if the node is {@code null} or
150     * not a boolean.
151     *
152     * @param jn the {@link JsonNode} to parse
153     * @param elName the property name
154     * @return the boolean value if it exists and is a boolean property, {@code null} otherwise
155     * @since 9.2
156     */
157    protected Boolean getBooleanField(JsonNode jn, String elName) {
158        JsonNode elNode = jn.get(elName);
159        if (elNode != null && !elNode.isNull()) {
160            if (elNode.isBoolean()) {
161                return elNode.booleanValue();
162            } else if (elNode.isTextual()) {
163                return Boolean.valueOf(elNode.textValue());
164            } else {
165                return null;
166            }
167        } else {
168            return null;
169        }
170    }
171
172    /**
173     * Tries to get a long property of the given {@link JsonNode}. Return {@code null} if the node is {@code null} or
174     * not a number.
175     *
176     * @param jn the {@link JsonNode} to parse
177     * @param elName the property name
178     * @return the long value if it exists and is a long property, {@code null} otherwise
179     * @since 10.2
180     */
181    protected Long getLongField(JsonNode jn, String elName) {
182        JsonNode elNode = jn.get(elName);
183        if (elNode != null && !elNode.isNull()) {
184            if (elNode.isNumber()) {
185                return elNode.longValue();
186            } else if (elNode.isTextual()) {
187                return Long.valueOf(elNode.textValue());
188            } else {
189                return null;
190            }
191        } else {
192            return null;
193        }
194    }
195
196    /**
197     * Tries to get a string list property of the given {@link JsonNode}. Return {@code null} if the node is
198     * {@code null} or not a string list.
199     *
200     * @param jn the {@link JsonNode} to parse
201     * @param elName the property name
202     * @return a string list if it exists and is a valid string list property, {@code null} otherwise
203     * @since 9.2
204     */
205    protected List<String> getStringListField(JsonNode jn, String elName) {
206        JsonNode elNode = jn.get(elName);
207        if (elNode != null && !elNode.isNull()) {
208            if (elNode.isArray()) {
209                List<String> result = new ArrayList<>();
210                String value;
211                for (JsonNode subNode : elNode) {
212                    if (subNode != null && !subNode.isNull() && subNode.isTextual()) {
213                        value = subNode.textValue();
214                    } else {
215                        value = null;
216                    }
217                    result.add(value);
218                }
219                return result;
220            } else {
221                return null;
222            }
223        } else {
224            return null;
225        }
226    }
227
228}