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