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    }
075
076    public static void addInputResolver(InputResolver<?> resolver) {
077        inputResolvers.put(resolver.getType(), resolver);
078    }
079
080    public static Object resolveInput(String input) throws IOException {
081        int p = input.indexOf(':');
082        if (p <= 0) {
083            // pass the String object directly
084            return input;
085        }
086        String type = input.substring(0, p);
087        String ref = input.substring(p + 1);
088        InputResolver<?> ir = inputResolvers.get(type);
089        if (ir != null) {
090            return ir.getInput(ref);
091        }
092        // no resolver found, pass the String object directly.
093        return input;
094    }
095
096    @Override
097    public boolean isReadable(Class<?> arg0, Type arg1, Annotation[] arg2, MediaType arg3) {
098        return ((targetMediaTypeNXReq.isCompatible(arg3) || targetMediaType.isCompatible(arg3)) && ExecutionRequest.class.isAssignableFrom(arg0));
099    }
100
101    @Override
102    public ExecutionRequest readFrom(Class<ExecutionRequest> arg0, Type arg1, Annotation[] arg2, MediaType arg3,
103            MultivaluedMap<String, String> headers, InputStream in) throws IOException, WebApplicationException {
104        return readRequest(in, headers, getCoreSession());
105    }
106
107    public ExecutionRequest readRequest(InputStream in, MultivaluedMap<String, String> headers, CoreSession session)
108            throws IOException, WebApplicationException {
109        // As stated in http://tools.ietf.org/html/rfc4627.html UTF-8 is the
110        // default encoding for JSON content
111        // TODO: add introspection on the first bytes to detect other admissible
112        // json encodings, namely: UTF-8, UTF-16 (BE or LE), or UTF-32 (BE or
113        // LE)
114        String content = IOUtils.toString(in, "UTF-8");
115        if (content.isEmpty()) {
116            throw new WebApplicationException(Response.Status.BAD_REQUEST);
117        }
118        return readRequest(content, headers, session);
119    }
120
121    public ExecutionRequest readRequest(String content, MultivaluedMap<String, String> headers, CoreSession session)
122            throws WebApplicationException {
123        try {
124            return readRequest0(content, headers, session);
125        } catch (WebApplicationException e) {
126            throw e;
127        } catch (IOException e) {
128            throw new WebApplicationException(e);
129        }
130    }
131
132    public ExecutionRequest readRequest0(String content, MultivaluedMap<String, String> headers, CoreSession session)
133            throws IOException {
134
135        JsonParser jp = factory.createJsonParser(content);
136
137        return readRequest(jp, headers, session);
138    }
139
140    /**
141     * @param jp
142     * @param headers
143     * @param session
144     * @return
145     * @since TODO
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.getLocalService(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.getTextValue()));
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.getLocalService(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.getLocalService(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}