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