/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.stanbol.entityhub.jersey.utils; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Map; import javax.mail.BodyPart; import javax.mail.MessagingException; import javax.mail.internet.MimeMultipart; import javax.mail.internet.ParseException; import javax.mail.util.ByteArrayDataSource; import javax.ws.rs.FormParam; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response.Status; import javax.ws.rs.ext.MessageBodyReader; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Utilities for implementing {@link MessageBodyReader}. * @author Rupert Westenthaler * */ public final class MessageBodyReaderUtils { private MessageBodyReaderUtils(){ /*do not instantiate Util classes */ }; private final static Logger log = LoggerFactory.getLogger(MessageBodyReaderUtils.class); /** * Returns content parsed as {@link MediaType#APPLICATION_FORM_URLENCODED}. * It assumes that the encoding and the content is defined by own parameters. * For the content this method allows to parse several parameters. The first * existing one is used to get the content. The parameter actually used to * retrieve the content will be available via {@link RequestData#getName()}.<p> * This Method will load the content several time into memory and should * not be used for big contents. However this should be fine in cases * data are parsed as {@link MediaType#APPLICATION_FORM_URLENCODED}<p> * This Method is necessary because within {@link MessageBodyReader} one * can not use the {@link FormParam} annotations because only the * {@link InputStream} is parsed to the * {@link MessageBodyReader#readFrom(Class, Type, java.lang.annotation.Annotation[], MediaType, javax.ws.rs.core.MultivaluedMap, InputStream)} * method<p> * To test this Method with curl use: * <code><pre> * curl -v -X POST --data-urlencode "{encodingParam}=application/rdf+xml" * --data-urlencode "{contentParam}@{datafile}" * {serviceURL} * </pre></code> * Note that between {contentParam} and the datafile MUST NOT be a '='! * @param formData the data of the form as stream * @param charset the charset used for the form data * @param encodingParam the parameter name used to parse the encoding * @param contentParams the list of parameters used for the content. The first * existing parameter is used to parse the content. Additional ones are * ignored. * @return The parsed content (MediaType and InputStream) * @throws IOException On any exception while reading from the parsed stream. * @throws UnsupportedEncodingException if the parsed charset is not supported * by this plattform * @throws IllegalArgumentException In case of a {@link Status#BAD_REQUEST} */ public static RequestData formForm(InputStream formData,String charset, String encodingParam,List<String> contentParams) throws IOException,UnsupportedEncodingException,IllegalArgumentException{ Map<String,String> params = JerseyUtils.parseForm(formData, charset); log.debug("Read from Form:"); MediaType mediaType; if(encodingParam != null){ String mediaTypeString = params.get(encodingParam); log.debug(" > encoding: {}={}",encodingParam,mediaTypeString); if(mediaTypeString != null){ try { mediaType = MediaType.valueOf(mediaTypeString); } catch (IllegalArgumentException e) { throw new IllegalStateException(String.format( "Illegal formatted Content-Type %s parsed by parameter %s", encodingParam,mediaTypeString),e); } } else { mediaType = null; } } else { log.debug(" > encoding: no encoding prameter set"); mediaType = null; } log.debug(" <- mediaType = {}",mediaType); InputStream entityStream = null; String contentParam = null; Iterator<String> contentParamIterator = contentParams.iterator(); while(entityStream == null && contentParamIterator.hasNext()){ contentParam = contentParamIterator.next(); String content = params.get(contentParam); log.debug(" > content: {}={}",contentParam,content); if(content != null){ entityStream = new ByteArrayInputStream(content.getBytes(charset)); } } if(entityStream == null){ throw new IllegalArgumentException(String.format( "No content found for any of the following parameters %s", contentParams)); } return new RequestData(mediaType,contentParam,entityStream); } /** * Returns content parsed from {@link MediaType#MULTIPART_FORM_DATA}. * It iterates over all {@link BodyPart}s and tries to create {@link RequestData} * instances. In case the {@link BodyPart#getContentType()} is not present or * can not be parsed, the {@link RequestData#getMediaType()} is set to * <code>null</code>. If {@link BodyPart#getInputStream()} is not defined an * {@link IllegalArgumentException} is thrown. The {@link BodyPart#getFileName()} * is used for {@link RequestData#getName()}. The ordering of the returned * Content instances is the same as within the {@link MimeMultipart} instance * parsed from the input stream. <p> * This Method does NOT load the data into memory, but returns directly the * {@link InputStream}s as returned by the {@link BodyPart}s. Therefore * it is saved to be used with big attachments.<p> * This Method is necessary because within {@link MessageBodyReader} one * can not use the usual annotations as used within Resources. so this method * allows to access the data directly from the parameters available from the * {@link MessageBodyReader#readFrom(Class, Type, java.lang.annotation.Annotation[], MediaType, javax.ws.rs.core.MultivaluedMap, InputStream)} * method<p> * To test this Method with curl use: * <code><pre> * curl -v -X POST -F "content=@{dataFile};type={mimeType}" * {serviceURL} * </pre></code> * Note that between {contentParam} and the datafile MUST NOT be a '='! * @param mimeData the mime encoded data * @param mediaType the mediaType (parsed to the {@link ByteArrayDataSource} * constructor) * @return the contents parsed from the {@link BodyPart}s * @throws IOException an any Exception while reading the stream or * {@link MessagingException} exceptions other than {@link ParseException}s * @throws IllegalArgumentException If a {@link InputStream} is not available * for any {@link BodyPart} or on {@link ParseException}s while reading the * MimeData from the stream. */ public static List<RequestData> fromMultipart(InputStream mimeData, MediaType mediaType) throws IOException, IllegalArgumentException{ ByteArrayDataSource ds = new ByteArrayDataSource(mimeData, mediaType.toString()); List<RequestData> contents = new ArrayList<RequestData>(); try { MimeMultipart data = new MimeMultipart(ds); //For now search the first bodypart that fits and only debug the others for(int i = 0;i < data.getCount();i++){ BodyPart bp = data.getBodyPart(i); String fileName = bp.getFileName(); MediaType mt; try { mt = bp.getContentType()!=null?MediaType.valueOf(bp.getContentType()):null; }catch (IllegalArgumentException e) { log.warn(String.format( "Unable to parse MediaType form Mime Bodypart %s: " + " fileName %s | Disposition %s | Description %s", i+1,fileName,bp.getDisposition(),bp.getDescription()),e); mt = null; } InputStream stream = bp.getInputStream(); if(stream == null){ throw new IllegalArgumentException(String.format( "Unable to get InputStream for Mime Bodypart %s: " + "mediaType %s fileName %s | Disposition %s | Description %s", i+1,fileName,bp.getDisposition(),bp.getDescription())); } else { contents.add(new RequestData(mt,bp.getFileName(),stream)); } } } catch (ParseException e) { throw new IllegalStateException(String.format( "Unable to parse data from %s request", MediaType.MULTIPART_FORM_DATA_TYPE),e); } catch (MessagingException e) { throw new IOException("Exception while reading "+ MediaType.MULTIPART_FORM_DATA_TYPE+" request",e); } return contents; } /** * Simple class that holds the MediaType, Name and the content as * {@link InputStream}. * @author Rupert Westenthaler * */ public static class RequestData { private final MediaType mediaType; private final InputStream entityStream; private final String contentName; public RequestData(MediaType mediaType,String contentName,InputStream entityStream) { if(entityStream == null){ throw new IllegalArgumentException("The parsed Inputstream MUST NOT be NULL!"); } this.mediaType = mediaType; this.entityStream = entityStream; this.contentName = contentName; } /** * The media type or <code>null</code> if not known * @return the mediaType */ public MediaType getMediaType() { return mediaType; } /** * The stream with the data * @return the entityStream */ public InputStream getEntityStream() { return entityStream; } /** * The name of the content (e.g. the parameter used to parse the content * or the filename provided by the {@link BodyPart}) or <code>null</code> * if not present. The documentation of methods returning instances of * this class should document what is used as name. * @return the contentName */ public String getName() { return contentName; } } }