/**
* Copyright 2009 Google Inc. All Rights Reserved.
*
* Licensed 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 com.google.apphosting.utils.servlet;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.mail.BodyPart;
import javax.mail.MessagingException;
import javax.mail.internet.ContentType;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMultipart;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
/**
* {@code ParseBlobUploadFilter} is responsible for the parsing
* multipart/form-data or multipart/mixed requests used to make Blob
* upload callbacks, and storing a set of string-encoded blob keys as
* a servlet request attribute. This allows the {@code
* BlobstoreService.getUploadedBlobs()} method to return the
* appropriate {@code BlobKey} objects.
*
* <p>This filter automatically runs on all dynamic requests in the
* production environment. In the DevAppServer, the equivalent work
* is subsumed by {@code UploadBlobServlet}.
*
*/
public class ParseBlobUploadFilter implements Filter {
private static final Logger logger = Logger.getLogger(
ParseBlobUploadFilter.class.getName());
/**
* An arbitrary HTTP header that is set on all blob upload
* callbacks.
*/
static final String UPLOAD_HEADER = "X-AppEngine-BlobUpload";
static final String UPLOADED_BLOBKEY_ATTR = "com.google.appengine.api.blobstore.upload.blobkeys";
static final String UPLOADED_BLOBINFO_ATTR =
"com.google.appengine.api.blobstore.upload.blobinfos";
// This field has to be the same as X_APPENGINE_CLOUD_STORAGE_OBJECT in http_proto.cc.
// This header will have the creation date in the format YYYY-MM-DD HH:mm:ss.SSS.
static final String UPLOAD_CREATION_HEADER = "X-AppEngine-Upload-Creation";
// This field has to be the same as X_APPENGINE_CLOUD_STORAGE_OBJECT in http_proto.cc.
// This header will have the filename of created the object in Cloud Storage when appropriate.
static final String CLOUD_STORAGE_OBJECT_HEADER = "X-AppEngine-Cloud-Storage-Object";
static final String CONTENT_LENGTH_HEADER = "Content-Length";
public void init(FilterConfig config) {
}
public void destroy() {
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
if (req.getHeader(UPLOAD_HEADER) != null) {
Map<String, List<String>> blobKeys = new HashMap<String, List<String>>();
Map<String, List<Map<String, String>>> blobInfos =
new HashMap<String, List<Map<String, String>>>();
Map<String, List<String>> otherParams = new HashMap<String, List<String>>();
try {
MimeMultipart multipart = MultipartMimeUtils.parseMultipartRequest(req);
int parts = multipart.getCount();
for (int i = 0; i < parts; i++) {
BodyPart part = multipart.getBodyPart(i);
String fieldName = MultipartMimeUtils.getFieldName(part);
if (part.getFileName() != null) {
ContentType contentType = new ContentType(part.getContentType());
if ("message/external-body".equals(contentType.getBaseType())) {
String blobKeyString = contentType.getParameter("blob-key");
List<String> keys = blobKeys.get(fieldName);
if (keys == null) {
keys = new ArrayList<String>();
blobKeys.put(fieldName, keys);
}
keys.add(blobKeyString);
List<Map<String, String>> infos = blobInfos.get(fieldName);
if (infos == null) {
infos = new ArrayList<Map<String, String>>();
blobInfos.put(fieldName, infos);
}
infos.add(getInfoFromBody(MultipartMimeUtils.getTextContent(part), blobKeyString));
}
} else {
List<String> values = otherParams.get(fieldName);
if (values == null) {
values = new ArrayList<String>();
otherParams.put(fieldName, values);
}
values.add(MultipartMimeUtils.getTextContent(part));
}
}
req.setAttribute(UPLOADED_BLOBKEY_ATTR, blobKeys);
req.setAttribute(UPLOADED_BLOBINFO_ATTR, blobInfos);
} catch (MessagingException ex) {
logger.log(Level.WARNING, "Could not parse multipart message:", ex);
}
chain.doFilter(new ParameterServletWrapper(request, otherParams), response);
} else {
chain.doFilter(request, response);
}
}
private Map<String, String> getInfoFromBody(String bodyContent, String key)
throws MessagingException {
MimeBodyPart part = new MimeBodyPart(new ByteArrayInputStream(bodyContent.getBytes()));
Map<String, String> info = new HashMap<String, String>(6);
info.put("key", key);
info.put("content-type", part.getContentType());
info.put("creation-date", part.getHeader(UPLOAD_CREATION_HEADER)[0]);
info.put("filename", part.getFileName());
info.put("size", part.getHeader(CONTENT_LENGTH_HEADER)[0]); // part.getSize() returns 0
info.put("md5-hash", part.getContentMD5());
String[] headers = part.getHeader(CLOUD_STORAGE_OBJECT_HEADER);
if (headers != null && headers.length == 1) {
info.put("gs-name", headers[0]);
}
return info;
}
private static class ParameterServletWrapper extends HttpServletRequestWrapper {
private final Map<String, List<String>> otherParams;
ParameterServletWrapper(ServletRequest request, Map<String, List<String>> otherParams) {
super((HttpServletRequest) request);
this.otherParams = otherParams;
}
@SuppressWarnings("rawtypes")
@Override
public Map getParameterMap() {
@SuppressWarnings("unchecked")
Map<String, String[]> parameters = super.getParameterMap();
if (otherParams.isEmpty()) {
return parameters;
} else {
// HttpServlet.getParameterMap() result is immutable so we need to take a copy.
Map<String, String[]> map = new HashMap<String, String[]>(parameters);
for (Map.Entry<String, List<String>> entry : otherParams.entrySet()) {
map.put(entry.getKey(), entry.getValue().toArray(new String[0]));
}
// Maintain the semantic of ServletRequstWrapper by returning
// an immutable map.
return Collections.unmodifiableMap(map);
}
}
@SuppressWarnings("rawtypes")
@Override
public Enumeration getParameterNames() {
List<String> allNames = new ArrayList<String>();
@SuppressWarnings("unchecked")
Enumeration<String> names = super.getParameterNames();
while (names.hasMoreElements()) {
allNames.add(names.nextElement());
}
allNames.addAll(otherParams.keySet());
return Collections.enumeration(allNames);
}
@Override
public String[] getParameterValues(String name) {
if (otherParams.containsKey(name)) {
return otherParams.get(name).toArray(new String[0]);
} else {
return super.getParameterValues(name);
}
}
@Override
public String getParameter(String name) {
if (otherParams.containsKey(name)) {
return otherParams.get(name).get(0);
} else {
return super.getParameter(name);
}
}
}
}