package uws.service.request;
/*
* This file is part of UWSLibrary.
*
* UWSLibrary is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* UWSLibrary is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with UWSLibrary. If not, see <http://www.gnu.org/licenses/>.
*
* Copyright 2014 - Astronomisches Rechen Institut (ARI)
*/
import java.io.File;
import java.io.IOException;
import java.util.Date;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import uws.UWSException;
import uws.service.UWS;
import uws.service.file.UWSFileManager;
import com.oreilly.servlet.MultipartRequest;
import com.oreilly.servlet.multipart.FileRenamePolicy;
/**
* <p>Extract parameters encoded using the Content-type multipart/form-data
* in an {@link HttpServletRequest}.</p>
*
* <p>
* The created file(s) is(are) stored in the temporary upload directory ({@link UWSFileManager#TMP_UPLOAD_DIR} ; this attribute can be modified if needed).
* This directory is supposed to be emptied regularly in case it is forgotten at any moment by the UWS service implementation to delete unused request files.
* </p>
*
* <p>
* The size of the full request body is limited by the static attribute {@link #SIZE_LIMIT} before the creation of the file.
* Its default value is: {@link #DEFAULT_SIZE_LIMIT}={@value #DEFAULT_SIZE_LIMIT} bytes.
* </p>
*
* <p>
* By default, this {@link RequestParser} overwrite parameter occurrences in the map: that's to say if a parameter is provided several times,
* only the last value will be kept. This behavior can be changed by overwriting the function {@link #consumeParameter(String, Object, Map)}
* of this class.
* </p>
*
* @author Grégory Mantelet (ARI)
* @version 4.1 (12/2014)
* @since 4.1
*/
public class MultipartParser implements RequestParser {
/** HTTP content-type for HTTP request formated in multipart. */
public static final String EXPECTED_CONTENT_TYPE = "multipart/form-data";
/** Default maximum allowed size for an HTTP request content: 10 MiB. */
public static final int DEFAULT_SIZE_LIMIT = 10 * 1024 * 1024;
/** <p>Maximum allowed size for an HTTP request content. Over this limit, an exception is thrown and the request is aborted.</p>
* <p><i>Note:
* The default value is {@link #DEFAULT_SIZE_LIMIT} (= {@value #DEFAULT_SIZE_LIMIT} MiB).
* </i></p>
* <p><i>Note:
* This limit is expressed in bytes and can not be negative.
* Its smallest possible value is 0. If the set value is though negative,
* it will be ignored and {@link #DEFAULT_SIZE_LIMIT} will be used instead.
* </i></p> */
public static int SIZE_LIMIT = DEFAULT_SIZE_LIMIT;
/** Indicates whether this parser should allow inline files or not. */
public final boolean allowUpload;
/** File manager to use to create {@link UploadFile} instances.
* It is required by this new object to execute open, move and delete operations whenever it could be asked. */
protected final UWSFileManager fileManager;
/**
* <p>Build a {@link MultipartParser} forbidding uploads (i.e. inline files).</p>
*
* <p>
* With this parser, when an upload (i.e. submitted inline files) is detected, an exception is thrown by {@link #parse(HttpServletRequest)}
* which cancels immediately the request.
* </p>
*/
public MultipartParser(){
this(false, null);
}
/**
* Build a {@link MultipartParser} allowing uploads (i.e. inline files).
*
* @param fileManager The file manager to use in order to store any eventual upload. <b>MUST NOT be NULL</b>
*/
public MultipartParser(final UWSFileManager fileManager){
this(true, fileManager);
}
/**
* <p>Build a {@link MultipartParser}.</p>
*
* <p>
* If the first parameter is <i>false</i>, then when an upload (i.e. submitted inline files) is detected, an exception is thrown
* by {@link #parse(HttpServletRequest)} which cancels immediately the request.
* </p>
*
* @param uploadEnabled <i>true</i> to allow uploads (i.e. inline files), <i>false</i> otherwise.
* If <i>false</i>, the two other parameters are useless.
* @param fileManager The file manager to use in order to store any eventual upload. <b>MUST NOT be NULL</b>
*/
protected MultipartParser(final boolean uploadEnabled, final UWSFileManager fileManager){
if (uploadEnabled && fileManager == null)
throw new NullPointerException("Missing file manager although the upload capability is enabled => can not create a MultipartParser!");
this.allowUpload = uploadEnabled;
this.fileManager = fileManager;
}
@Override
@SuppressWarnings("unchecked")
public final Map<String,Object> parse(final HttpServletRequest request) throws UWSException{
LinkedHashMap<String,Object> parameters = new LinkedHashMap<String,Object>();
MultipartRequest multipart = null;
try{
// Parse the request body:
multipart = new MultipartRequest(request, UWSFileManager.TMP_UPLOAD_DIR.getPath(), (SIZE_LIMIT < 0 ? DEFAULT_SIZE_LIMIT : SIZE_LIMIT), new FileRenamePolicy(){
@Override
public File rename(File file){
Object reqID = request.getAttribute(UWS.REQ_ATTRIBUTE_ID);
if (reqID == null || !(reqID instanceof String))
reqID = (new Date()).getTime();
char uniq = 'A';
File f = new File(file.getParentFile(), "UPLOAD_" + reqID + uniq + "_" + file.getName());
while(f.exists()){
uniq++;
f = new File(file.getParentFile(), "UPLOAD_" + reqID + "_" + file.getName());
}
return f;
}
});
// Extract all "normal" parameters:
String param;
Enumeration<String> e = multipart.getParameterNames();
while(e.hasMoreElements()){
param = e.nextElement();
for(String occurence : multipart.getParameterValues(param))
consumeParameter(param, occurence, parameters);
}
// Extract all inline files as additional parameters:
e = multipart.getFileNames();
if (!allowUpload && e.hasMoreElements())
throw new UWSException(UWSException.BAD_REQUEST, "Uploads are not allowed by this service!");
while(e.hasMoreElements()){
param = e.nextElement();
if (multipart.getFile(param) == null)
continue;
/*
* TODO !!!POSSIBLE ISSUE!!!
* MultipartRequest is not able to deal with multiple files having the same parameter name. However, all files are created/uploaded
* but only the last one is accessible through this object....so only the last can be deleted, which could be a problem later
* (hence the usage of the system temporary directory).
*/
// build its description/pointer:
UploadFile lob = new UploadFile(param, multipart.getOriginalFileName(param), multipart.getFile(param).toURI().toString(), fileManager);
lob.mimeType = multipart.getContentType(param);
lob.length = multipart.getFile(param).length();
// add it inside the parameters map:
consumeParameter(param, lob, parameters);
}
}catch(IOException ioe){
throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, ioe, "Internal Error => Impossible to extract parameters from the Multipart HTTP request!");
}catch(IllegalArgumentException iae){
String confError = iae.getMessage();
if (UWSFileManager.TMP_UPLOAD_DIR == null)
confError = "Missing upload directory!";
throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, iae, "Internal Error: Incorrect UPLOAD configuration: " + confError);
}
return parameters;
}
/**
* <p>Consume the specified parameter: add it inside the given map.</p>
*
* <p>
* By default, this function is just putting the given value inside the map. So, if the parameter already exists in the map,
* its old value will be overwritten by the given one.
* </p>
*
* <p><i>Note:
* If the old value was a file, it will be deleted from the file system before its replacement in the map.
* </i></p>
*
* @param name Name of the parameter to consume.
* @param value Its value.
* @param allParams The list of all parameters read until now.
*/
protected void consumeParameter(final String name, final Object value, final Map<String,Object> allParams){
// If the old value was a file, delete it before replacing its value:
if (allParams.containsKey(name) && allParams.get(name) instanceof UploadFile){
try{
((UploadFile)allParams.get(name)).deleteFile();
}catch(IOException ioe){}
}
// Put the given value in the given map:
allParams.put(name, value);
}
/**
* <p>Utility method that determines whether the content of the given request is a multipart/form-data.</p>
*
* <p><i>Important:
* This function just test the content-type of the request. The HTTP method (e.g. GET, POST, ...) is not tested.
* </i></p>
*
* @param request The servlet request to be evaluated. Must be non-null.
*
* @return <i>true</i> if the request is multipart,
* <i>false</i> otherwise.
*/
public static final boolean isMultipartContent(final HttpServletRequest request){
// Extract the content type and determine if it is a multipart request (its content type should start by multipart/form-data"):
String contentType = request.getContentType();
if (contentType == null)
return false;
else if (contentType.toLowerCase().startsWith(EXPECTED_CONTENT_TYPE))
return true;
else
return false;
}
}