001/* 002 * (C) Copyright 2006-2016 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.client.jaxrs.spi; 020 021import static org.nuxeo.ecm.automation.client.Constants.CTYPE_AUTOMATION; 022import static org.nuxeo.ecm.automation.client.Constants.CTYPE_ENTITY; 023import static org.nuxeo.ecm.automation.client.Constants.CTYPE_MULTIPART_EMPTY; 024import static org.nuxeo.ecm.automation.client.Constants.CTYPE_MULTIPART_MIXED; 025import static org.nuxeo.ecm.automation.client.Constants.HEADER_CONTENT_DISPOSITION; 026 027import java.io.File; 028import java.io.FileInputStream; 029import java.io.IOException; 030import java.io.InputStream; 031import java.io.UnsupportedEncodingException; 032import java.net.URLDecoder; 033import java.util.HashMap; 034import java.util.regex.Matcher; 035import java.util.regex.Pattern; 036 037import javax.mail.BodyPart; 038import javax.mail.MessagingException; 039import javax.mail.internet.MimeMultipart; 040import javax.ws.rs.core.Response; 041 042import org.apache.http.Header; 043import org.apache.http.HttpHeaders; 044import org.apache.http.protocol.HttpContext; 045import org.nuxeo.ecm.automation.client.RemoteException; 046import org.nuxeo.ecm.automation.client.jaxrs.spi.marshallers.ExceptionMarshaller; 047import org.nuxeo.ecm.automation.client.jaxrs.util.IOUtils; 048import org.nuxeo.ecm.automation.client.jaxrs.util.InputStreamDataSource; 049import org.nuxeo.ecm.automation.client.jaxrs.util.MultipartInput; 050import org.nuxeo.ecm.automation.client.model.Blob; 051import org.nuxeo.ecm.automation.client.model.Blobs; 052import org.nuxeo.ecm.automation.client.model.FileBlob; 053import org.nuxeo.ecm.automation.client.model.StringBlob; 054 055/** 056 * @author <a href="mailto:[email protected]">Bogdan Stefanescu</a> 057 */ 058public class Request extends HashMap<String, String> { 059 060 public static final int GET = 0; 061 062 public static final int POST = 1; 063 064 private static final long serialVersionUID = 1L; 065 066 protected static Pattern RFC2231_ATTR_PATTERN = Pattern.compile( 067 ";?\\s*filename\\s*\\\\*.*\\*=([^']*)'([^']*)'\\s*([^;]+)\\s*", Pattern.CASE_INSENSITIVE); 068 069 protected static Pattern ATTR_PATTERN = Pattern.compile(";?\\s*filename\\s*=\\s*([^;]+)\\s*", 070 Pattern.CASE_INSENSITIVE); 071 072 protected final int method; 073 074 protected final String url; 075 076 protected final boolean isMultiPart; 077 078 protected Object entity; 079 080 public Request(int method, String url) { 081 this.method = method; 082 this.url = url; 083 isMultiPart = false; 084 } 085 086 public Request(int method, String url, MimeMultipart entity) { 087 this.method = method; 088 this.url = url; 089 this.entity = entity; 090 isMultiPart = true; 091 } 092 093 public Request(int method, String url, String entity) { 094 this.method = method; 095 this.url = url; 096 this.entity = entity; 097 isMultiPart = false; 098 } 099 100 public int getMethod() { 101 return method; 102 } 103 104 public String getUrl() { 105 return url; 106 } 107 108 public Object getEntity() { 109 return entity; 110 } 111 112 public final boolean isMultiPart() { 113 return isMultiPart; 114 } 115 116 public MimeMultipart asMultiPartEntity() { 117 return isMultiPart ? (MimeMultipart) entity : null; 118 } 119 120 public String asStringEntity() { 121 return isMultiPart ? null : (String) entity; 122 } 123 124 /** 125 * Must read the object from the server response and return it or throw a {@link RemoteException} if server sent an 126 * error. 127 */ 128 public Object handleResult(int status, Header[] headers, InputStream stream, HttpContext ctx) 129 throws RemoteException, IOException { 130 // TODO kevin: check if it's enough regarding to entity content type 131 String ctype = getHeaderValue(headers, HttpHeaders.CONTENT_TYPE); 132 133 // Specific http status handling 134 if (status >= Response.Status.BAD_REQUEST.getStatusCode()) { 135 handleException(status, ctype, stream); 136 } else if (status == Response.Status.NO_CONTENT.getStatusCode() || stream == null) { 137 if (ctype != null && ctype.toLowerCase().startsWith(CTYPE_MULTIPART_EMPTY)) { 138 // empty entity and content type of nuxeo empty list 139 return new Blobs(); 140 } 141 // no content 142 return null; 143 } 144 // Check content type 145 if (ctype == null) { 146 if (status != Response.Status.OK.getStatusCode()) { 147 // this may happen when login failed 148 throw new RemoteException(status, "ServerError", "Server Error", ""); 149 } 150 // cannot handle responses with no content type 151 return null; 152 } 153 // Handle result 154 String disp = getHeaderValue(headers, HEADER_CONTENT_DISPOSITION); 155 String lctype = ctype.toLowerCase(); 156 if (lctype.startsWith(CTYPE_AUTOMATION)) { 157 return JsonMarshalling.readRegistry(IOUtils.read(stream)); 158 } else if (lctype.startsWith(CTYPE_ENTITY)) { 159 String body = IOUtils.read(stream); 160 try { 161 return JsonMarshalling.readEntity(body); 162 } catch (IOException | RuntimeException e) { 163 return readStringBlob(ctype, getFileName(disp), body); 164 } 165 } else if (lctype.startsWith(CTYPE_MULTIPART_MIXED)) { // list of blobs 166 return readBlobs(ctype, stream); 167 } else { // a blob? 168 return readBlob(ctype, getFileName(disp), stream); 169 } 170 } 171 172 public static MultipartInput buildMultipartInput(Object input, String content) throws IOException { 173 MultipartInput mpinput = new MultipartInput(); 174 mpinput.setRequest(content); 175 if (input instanceof Blob) { 176 mpinput.setBlob((Blob) input); 177 } else if (input instanceof Blobs) { 178 mpinput.setBlobs((Blobs) input); 179 } else { 180 throw new IllegalArgumentException("Unsupported binary input object: " + input); 181 } 182 return mpinput; 183 } 184 185 protected static Blobs readBlobs(String ctype, InputStream in) throws IOException { 186 Blobs files = new Blobs(); 187 // save the stream to a temporary file 188 File file = IOUtils.copyToTempFile(in); 189 try (FileInputStream fin = new FileInputStream(file)) { 190 MimeMultipart mp = new MimeMultipart(new InputStreamDataSource(fin, ctype)); 191 int size = mp.getCount(); 192 for (int i = 0; i < size; i++) { 193 BodyPart part = mp.getBodyPart(i); 194 String fname = part.getFileName(); 195 files.add(readBlob(part.getContentType(), fname, part.getInputStream())); 196 } 197 } catch (MessagingException e) { 198 throw new IOException(e); 199 } finally { 200 file.delete(); 201 } 202 return files; 203 } 204 205 protected static Blob readBlob(String ctype, String fileName, InputStream in) throws IOException { 206 File file = IOUtils.copyToTempFile(in); 207 FileBlob blob = new FileBlob(file); 208 blob.setMimeType(ctype); 209 if (fileName != null) { 210 blob.setFileName(fileName); 211 } 212 return blob; 213 } 214 215 protected static Blob readStringBlob(String ctype, String fileName, String content) { 216 return new StringBlob(fileName, content, ctype); 217 } 218 219 protected static String getFileName(String ctype) { 220 if (ctype == null) { 221 return null; 222 } 223 224 Matcher m = RFC2231_ATTR_PATTERN.matcher(ctype); 225 if (m.find()) { 226 try { 227 return URLDecoder.decode(m.group(3), m.group(1)); 228 } catch (UnsupportedEncodingException e) { 229 throw new RuntimeException(e); 230 } 231 } 232 m = ATTR_PATTERN.matcher(ctype); 233 if (m.find()) { 234 return m.group(1); 235 } 236 return null; 237 } 238 239 protected void handleException(int status, String ctype, InputStream stream) throws RemoteException { 240 if (stream == null) { 241 throw new RemoteException(status, "ServerError", "Server Error", ""); 242 } 243 String content; 244 try { 245 content = IOUtils.read(stream); 246 } catch (IOException e) { 247 // typically: org.apache.http.ConnectionClosedException: Premature end of chunk coded message body: 248 // closing chunk expected 249 throw new RemoteException(status, "ServerError", "Server Error", ""); 250 } 251 if (CTYPE_ENTITY.equalsIgnoreCase(ctype)) { 252 try { 253 throw ExceptionMarshaller.readException(content); 254 } catch (IOException t) { 255 // JSON decoding error in the payload 256 throw new RemoteException(status, "ServerError", "Server Error", content); 257 } 258 } else { 259 // no JSON payload 260 throw new RemoteException(status, "ServerError", "Server Error", content); 261 } 262 } 263 264 public static String getHeaderValue(Header[] headers, String name) { 265 for (Header header : headers) { 266 if (header.getName().equalsIgnoreCase(name)) { 267 return header.getValue(); 268 } 269 } 270 return null; 271 } 272 273}