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