001/*
002 * (C) Copyright 2006-2011 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 *     bstefanescu
018 */
019package org.nuxeo.ecm.automation.jaxrs.io.operations;
020
021import java.io.IOException;
022import java.io.InputStream;
023import java.lang.annotation.Annotation;
024import java.lang.reflect.Type;
025import java.util.HashMap;
026
027import javax.servlet.http.HttpServletRequest;
028import javax.ws.rs.Consumes;
029import javax.ws.rs.WebApplicationException;
030import javax.ws.rs.core.Context;
031import javax.ws.rs.core.MediaType;
032import javax.ws.rs.core.MultivaluedMap;
033import javax.ws.rs.core.Response;
034import javax.ws.rs.ext.MessageBodyReader;
035import javax.ws.rs.ext.Provider;
036
037import org.apache.commons.io.IOUtils;
038import org.nuxeo.ecm.automation.io.services.codec.ObjectCodecService;
039import org.nuxeo.ecm.core.api.CoreSession;
040import org.nuxeo.ecm.core.io.registry.MarshallingConstants;
041import org.nuxeo.ecm.webengine.jaxrs.session.SessionFactory;
042import org.nuxeo.runtime.api.Framework;
043
044import com.fasterxml.jackson.core.JsonFactory;
045import com.fasterxml.jackson.core.JsonParser;
046import com.fasterxml.jackson.core.JsonToken;
047import com.fasterxml.jackson.databind.JsonNode;
048
049/**
050 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a>
051 */
052@Provider
053@Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_JSON + "+nxrequest" })
054public class JsonRequestReader implements MessageBodyReader<ExecutionRequest> {
055
056    @Context
057    private HttpServletRequest request;
058
059    @Context
060    JsonFactory factory;
061
062    public CoreSession getCoreSession() {
063        return SessionFactory.getSession(request);
064    }
065
066    /**
067     * @deprecated since 10.3. only 'application/json' media type should be used.
068     */
069    @Deprecated
070    public static final MediaType targetMediaTypeNXReq = new MediaType("application", "json+nxrequest");
071
072    protected static final HashMap<String, InputResolver<?>> inputResolvers = new HashMap<String, InputResolver<?>>();
073
074    static {
075        addInputResolver(new DocumentInputResolver());
076        addInputResolver(new DocumentsInputResolver());
077        addInputResolver(new BlobInputResolver());
078        addInputResolver(new BlobsInputResolver());
079    }
080
081    public static void addInputResolver(InputResolver<?> resolver) {
082        inputResolvers.put(resolver.getType(), resolver);
083    }
084
085    public static Object resolveInput(String input) throws IOException {
086        int p = input.indexOf(':');
087        if (p <= 0) {
088            // pass the String object directly
089            return input;
090        }
091        String type = input.substring(0, p);
092        String ref = input.substring(p + 1);
093        InputResolver<?> ir = inputResolvers.get(type);
094        if (ir != null) {
095            return ir.getInput(ref);
096        }
097        // no resolver found, pass the String object directly.
098        return input;
099    }
100
101    @Override
102    public boolean isReadable(Class<?> arg0, Type arg1, Annotation[] arg2, MediaType arg3) {
103        return ((targetMediaTypeNXReq.isCompatible(arg3) || MediaType.APPLICATION_JSON_TYPE.isCompatible(arg3))
104                && ExecutionRequest.class.isAssignableFrom(arg0));
105    }
106
107    @Override
108    public ExecutionRequest readFrom(Class<ExecutionRequest> arg0, Type arg1, Annotation[] arg2, MediaType arg3,
109            MultivaluedMap<String, String> headers, InputStream in) throws IOException, WebApplicationException {
110        return readRequest(in, headers, getCoreSession());
111    }
112
113    public ExecutionRequest readRequest(InputStream in, MultivaluedMap<String, String> headers, CoreSession session)
114            throws IOException, WebApplicationException {
115        // As stated in http://tools.ietf.org/html/rfc4627.html UTF-8 is the
116        // default encoding for JSON content
117        // TODO: add introspection on the first bytes to detect other admissible
118        // json encodings, namely: UTF-8, UTF-16 (BE or LE), or UTF-32 (BE or
119        // LE)
120        String content = IOUtils.toString(in, "UTF-8");
121        if (content.isEmpty()) {
122            throw new WebApplicationException(Response.Status.BAD_REQUEST);
123        }
124        return readRequest(content, headers, session);
125    }
126
127    public ExecutionRequest readRequest(String content, MultivaluedMap<String, String> headers, CoreSession session)
128            throws WebApplicationException {
129        try {
130            return readRequest0(content, headers, session);
131        } catch (WebApplicationException e) {
132            throw e;
133        } catch (IOException e) {
134            throw new WebApplicationException(e);
135        }
136    }
137
138    public ExecutionRequest readRequest0(String content, MultivaluedMap<String, String> headers, CoreSession session)
139            throws IOException {
140
141        JsonParser jp = factory.createJsonParser(content);
142
143        return readRequest(jp, headers, session);
144    }
145
146    /**
147     * @param jp
148     * @param headers
149     * @param session
150     * @return
151     * @since TODO
152     */
153    public static ExecutionRequest readRequest(JsonParser jp, MultivaluedMap<String, String> headers,
154            CoreSession session) throws IOException {
155        ExecutionRequest req = new ExecutionRequest();
156
157        ObjectCodecService codecService = Framework.getService(ObjectCodecService.class);
158        jp.nextToken(); // skip {
159        JsonToken tok = jp.nextToken();
160        while (tok != null && tok != JsonToken.END_OBJECT) {
161            String key = jp.getCurrentName();
162            jp.nextToken();
163            if ("input".equals(key)) {
164                JsonNode inputNode = jp.readValueAsTree();
165                if (inputNode.isTextual()) {
166                    // string values are expected to be micro-parsed with
167                    // the "type:value" syntax for backward compatibility
168                    // reasons.
169                    req.setInput(resolveInput(inputNode.textValue()));
170                } else {
171                    req.setInput(codecService.readNode(inputNode, session));
172                }
173            } else if ("params".equals(key)) {
174                readParams(jp, req, session);
175            } else if ("context".equals(key)) {
176                readContext(jp, req, session);
177            } else if ("documentProperties".equals(key)) {
178                // TODO XXX - this is wrong - headers are ready only! see with
179                // td
180                String documentProperties = jp.getText();
181                if (documentProperties != null) {
182                    headers.putSingle(MarshallingConstants.EMBED_PROPERTIES, documentProperties);
183                }
184            }
185            tok = jp.nextToken();
186        }
187        if (tok == null) {
188            throw new IllegalArgumentException("Unexpected end of stream.");
189        }
190        return req;
191    }
192
193    private static void readParams(JsonParser jp, ExecutionRequest req, CoreSession session) throws IOException {
194        ObjectCodecService codecService = Framework.getService(ObjectCodecService.class);
195        JsonToken tok = jp.nextToken(); // move to first entry
196        while (tok != null && tok != JsonToken.END_OBJECT) {
197            String key = jp.getCurrentName();
198            tok = jp.nextToken();
199            req.setParam(key, codecService.readNode(jp.readValueAsTree(), session));
200            tok = jp.nextToken();
201        }
202        if (tok == null) {
203            throw new IllegalArgumentException("Unexpected end of stream.");
204        }
205    }
206
207    private static void readContext(JsonParser jp, ExecutionRequest req, CoreSession session) throws IOException {
208        ObjectCodecService codecService = Framework.getService(ObjectCodecService.class);
209        JsonToken tok = jp.nextToken(); // move to first entry
210        while (tok != null && tok != JsonToken.END_OBJECT) {
211            String key = jp.getCurrentName();
212            tok = jp.nextToken();
213            req.setContextParam(key, codecService.readNode(jp.readValueAsTree(), session));
214            tok = jp.nextToken();
215        }
216        if (tok == null) {
217            throw new IllegalArgumentException("Unexpected end of stream.");
218        }
219    }
220
221}