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