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        try (InputStream in = new InputStreamWithJsonNode(jn)) {
129            return reader.read(clazz, effectiveGenericType, APPLICATION_JSON_TYPE, in);
130        }
131    }
132
133    /**
134     * Try to get a string property of the given {@link JsonNode}. Return null if the node is null.
135     *
136     * @param jn The {@link JsonNode} to parse.
137     * @param elName The property name.
138     * @return The property text if it exists and it's a text, null otherwise.
139     * @since 7.2
140     */
141    protected String getStringField(JsonNode jn, String elName) {
142        JsonNode elNode = jn.get(elName);
143        if (elNode != null && !elNode.isNull() && elNode.isTextual()) {
144            return elNode.textValue();
145        } else {
146            return null;
147        }
148    }
149
150    /**
151     * Tries to get a boolean property of the given {@link JsonNode}. Return {@code null} if the node is {@code null} or
152     * not a boolean.
153     *
154     * @param jn the {@link JsonNode} to parse
155     * @param elName the property name
156     * @return the boolean value if it exists and is a boolean property, {@code null} otherwise
157     * @since 9.2
158     */
159    protected Boolean getBooleanField(JsonNode jn, String elName) {
160        JsonNode elNode = jn.get(elName);
161        if (elNode != null && !elNode.isNull()) {
162            if (elNode.isBoolean()) {
163                return elNode.booleanValue();
164            } else if (elNode.isTextual()) {
165                return Boolean.valueOf(elNode.textValue());
166            } else {
167                return null;
168            }
169        } else {
170            return null;
171        }
172    }
173
174    /**
175     * Tries to get a long property of the given {@link JsonNode}. Return {@code null} if the node is {@code null} or
176     * not a number.
177     *
178     * @param jn the {@link JsonNode} to parse
179     * @param elName the property name
180     * @return the long value if it exists and is a long property, {@code null} otherwise
181     * @since 10.2
182     */
183    protected Long getLongField(JsonNode jn, String elName) {
184        JsonNode elNode = jn.get(elName);
185        if (elNode != null && !elNode.isNull()) {
186            if (elNode.isNumber()) {
187                return elNode.longValue();
188            } else if (elNode.isTextual()) {
189                return Long.valueOf(elNode.textValue());
190            } else {
191                return null;
192            }
193        } else {
194            return null;
195        }
196    }
197
198    /**
199     * Tries to get a string list property of the given {@link JsonNode}. Return {@code null} if the node is
200     * {@code null} or not a string list.
201     *
202     * @param jn the {@link JsonNode} to parse
203     * @param elName the property name
204     * @return a string list if it exists and is a valid string list property, {@code null} otherwise
205     * @since 9.2
206     */
207    protected List<String> getStringListField(JsonNode jn, String elName) {
208        JsonNode elNode = jn.get(elName);
209        if (elNode != null && !elNode.isNull()) {
210            if (elNode.isArray()) {
211                List<String> result = new ArrayList<>();
212                String value;
213                for (JsonNode subNode : elNode) {
214                    if (subNode != null && !subNode.isNull() && subNode.isTextual()) {
215                        value = subNode.textValue();
216                    } else {
217                        value = null;
218                    }
219                    result.add(value);
220                }
221                return result;
222            } else {
223                return null;
224            }
225        } else {
226            return null;
227        }
228    }
229
230}