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