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 static org.apache.http.HttpStatus.SC_BAD_REQUEST; 022 023import java.io.IOException; 024import java.io.InputStream; 025import java.lang.annotation.Annotation; 026import java.lang.reflect.Type; 027import java.util.HashMap; 028 029import javax.servlet.http.HttpServletRequest; 030import javax.ws.rs.Consumes; 031import javax.ws.rs.WebApplicationException; 032import javax.ws.rs.core.Context; 033import javax.ws.rs.core.MediaType; 034import javax.ws.rs.core.MultivaluedMap; 035import javax.ws.rs.ext.MessageBodyReader; 036import javax.ws.rs.ext.Provider; 037 038import org.apache.commons.io.IOUtils; 039import org.nuxeo.ecm.automation.io.services.codec.ObjectCodecService; 040import org.nuxeo.ecm.core.api.CoreSession; 041import org.nuxeo.ecm.core.io.registry.MarshallingConstants; 042import org.nuxeo.ecm.webengine.jaxrs.session.SessionFactory; 043import org.nuxeo.runtime.api.Framework; 044 045import com.fasterxml.jackson.core.JsonFactory; 046import com.fasterxml.jackson.core.JsonParser; 047import com.fasterxml.jackson.core.JsonProcessingException; 048import com.fasterxml.jackson.core.JsonToken; 049import com.fasterxml.jackson.databind.JsonNode; 050 051/** 052 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a> 053 */ 054@Provider 055@Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_JSON + "+nxrequest" }) 056public class JsonRequestReader implements MessageBodyReader<ExecutionRequest> { 057 058 @Context 059 private HttpServletRequest request; 060 061 @Context 062 JsonFactory factory; 063 064 public CoreSession getCoreSession() { 065 return SessionFactory.getSession(request); 066 } 067 068 /** 069 * @deprecated since 10.3. only 'application/json' media type should be used. 070 */ 071 @Deprecated 072 public static final MediaType targetMediaTypeNXReq = new MediaType("application", "json+nxrequest"); 073 074 protected static final HashMap<String, InputResolver<?>> inputResolvers = new HashMap<>(); 075 076 static { 077 addInputResolver(new DocumentInputResolver()); 078 addInputResolver(new DocumentsInputResolver()); 079 addInputResolver(new BlobInputResolver()); 080 addInputResolver(new BlobsInputResolver()); 081 } 082 083 public static void addInputResolver(InputResolver<?> resolver) { 084 inputResolvers.put(resolver.getType(), resolver); 085 } 086 087 public static Object resolveInput(String input) throws IOException { 088 int p = input.indexOf(':'); 089 if (p <= 0) { 090 // pass the String object directly 091 return input; 092 } 093 String type = input.substring(0, p); 094 String ref = input.substring(p + 1); 095 InputResolver<?> ir = inputResolvers.get(type); 096 if (ir != null) { 097 return ir.getInput(ref); 098 } 099 // no resolver found, pass the String object directly. 100 return input; 101 } 102 103 @Override 104 public boolean isReadable(Class<?> arg0, Type arg1, Annotation[] arg2, MediaType arg3) { 105 return ((targetMediaTypeNXReq.isCompatible(arg3) || MediaType.APPLICATION_JSON_TYPE.isCompatible(arg3)) 106 && ExecutionRequest.class.isAssignableFrom(arg0)); 107 } 108 109 @Override 110 public ExecutionRequest readFrom(Class<ExecutionRequest> arg0, Type arg1, Annotation[] arg2, MediaType arg3, 111 MultivaluedMap<String, String> headers, InputStream in) throws IOException, WebApplicationException { 112 return readRequest(in, headers, getCoreSession()); 113 } 114 115 public ExecutionRequest readRequest(InputStream in, MultivaluedMap<String, String> headers, CoreSession session) 116 throws IOException, WebApplicationException { 117 // As stated in http://tools.ietf.org/html/rfc4627.html UTF-8 is the 118 // default encoding for JSON content 119 // TODO: add introspection on the first bytes to detect other admissible 120 // json encodings, namely: UTF-8, UTF-16 (BE or LE), or UTF-32 (BE or 121 // LE) 122 String content = IOUtils.toString(in, "UTF-8"); 123 if (content.isEmpty()) { 124 throw new WebApplicationException(SC_BAD_REQUEST); 125 } 126 return readRequest(content, headers, session); 127 } 128 129 public ExecutionRequest readRequest(String content, MultivaluedMap<String, String> headers, CoreSession session) 130 throws WebApplicationException { 131 try { 132 return readRequest0(content, headers, session); 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 try (JsonParser jp = factory.createParser(content)) { 141 return readRequest(jp, headers, session); 142 } catch (JsonProcessingException e) { 143 throw new WebApplicationException(e, SC_BAD_REQUEST); 144 } 145 } 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.getService(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.textValue())); 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.getService(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.getService(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}