/*
* JBoss, Home of Professional Open Source.
* Copyright 2011, Red Hat, Inc., and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This 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 2.1 of
* the License, or (at your option) any later version.
*
* This software 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 this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jboss.capedwarf.blobstore;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.Part;
import com.google.appengine.api.blobstore.BlobInfo;
import com.google.appengine.api.blobstore.BlobKey;
import com.google.appengine.api.blobstore.BlobstoreFailureException;
import com.google.appengine.api.blobstore.ByteRange;
import com.google.appengine.api.blobstore.FileInfo;
import com.google.appengine.api.blobstore.RangeFormatException;
import com.google.appengine.api.blobstore.UnsupportedRangeFormatException;
import com.google.appengine.api.blobstore.UploadOptions;
import com.google.appengine.api.files.AppEngineFile;
import com.google.common.base.Function;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import org.jboss.capedwarf.common.io.IOUtils;
import org.jboss.capedwarf.common.servlet.ServletUtils;
/**
* @author <a href="mailto:ales.justin@jboss.org">Ales Justin</a>
* @author <a href="mailto:marko.luksa@gmail.com">Marko Luksa</a>
*/
public class CapedwarfBlobstoreService implements ExposedBlobstoreService {
private static final Logger log = Logger.getLogger(CapedwarfBlobstoreService.class.getName());
private static final String UPLOADED_BLOBKEY_ATTR = "com.google.appengine.api.blobstore.upload.blobkeys";
private static final String UPLOADED_BLOBKEY_LIST_ATTR = "com.google.appengine.api.blobstore.upload.blobkeylists";
private Function<List<BlobKey>, List<BlobInfo>> BLOB_LIST_KEY_TO_INFO_FN = new Function<List<BlobKey>, List<BlobInfo>>() {
public List<BlobInfo> apply(List<BlobKey> input) {
return Lists.transform(input, BLOB_KEY_TO_INFO_FN);
}
};
private Function<BlobKey, BlobInfo> BLOB_KEY_TO_INFO_FN = new Function<BlobKey, BlobInfo>() {
public BlobInfo apply(BlobKey input) {
return getBlobInfo(input);
}
};
private Function<List<BlobKey>, List<FileInfo>> FILE_LIST_KEY_TO_INFO_FN = new Function<List<BlobKey>, List<FileInfo>>() {
public List<FileInfo> apply(List<BlobKey> input) {
return Lists.transform(input, FILE_KEY_TO_INFO_FN);
}
};
private Function<BlobKey, FileInfo> FILE_KEY_TO_INFO_FN = new Function<BlobKey, FileInfo>() {
public FileInfo apply(BlobKey input) {
return getStorage().getFileInfo(input);
}
};
private Storage storage;
private synchronized Storage getStorage() {
if (storage == null) {
storage = StorageFactory.getStorage();
}
return storage;
}
public String createUploadUrl(String successPath) {
return createUploadUrl(successPath, UploadOptions.Builder.withDefaults());
}
public String createUploadUrl(String successPath, UploadOptions uploadOptions) {
return UploadServlet.createUploadUrl(successPath, uploadOptions);
}
public void delete(BlobKey... blobKeys) {
getStorage().delete(blobKeys);
}
public void serve(BlobKey blobKey, HttpServletResponse response) throws IOException {
serve(blobKey, (ByteRange) null, response);
}
public void serve(BlobKey blobKey, String rangeHeader, HttpServletResponse response) throws IOException {
serve(blobKey, ByteRange.parse(rangeHeader), response);
}
public void serve(BlobKey blobKey, ByteRange byteRange, HttpServletResponse response) throws IOException {
assertNotCommited(response);
response.setStatus(HttpServletResponse.SC_OK);
response.setHeader(BLOB_KEY_HEADER, blobKey.getKeyString());
if (byteRange != null) {
response.setHeader(BLOB_RANGE_HEADER, byteRange.toString());
}
}
public void serveBlob(BlobKey blobKey, String byteRangeStr, HttpServletResponse response) throws IOException {
assertNotCommited(response);
BlobInfo blobInfo = getBlobInfo(blobKey);
if (blobInfo == null) {
throw new IOException(String.format("No such blob for key: %s", blobKey));
}
response.setContentType(blobInfo.getContentType());
ByteRange byteRange = null;
if (byteRangeStr == null) {
response.setStatus(HttpServletResponse.SC_OK);
} else {
try {
byteRange = ByteRange.parse(byteRangeStr);
} catch (RangeFormatException e) {
response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
return;
}
if (byteRange.getStart() >= blobInfo.getSize()) {
response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
return;
}
if ((byteRange.hasEnd() == false) || (byteRange.getEnd() >= blobInfo.getSize())) {
byteRange = new ByteRange(byteRange.getStart(), blobInfo.getSize() - 1);
}
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
response.setHeader("Content-Range", "bytes " + byteRange.getStart() + "-" + byteRange.getEnd() + "/" + blobInfo.getSize());
}
try {
try (InputStream in = getStream(blobKey)) {
copyStream(in, response.getOutputStream(), byteRange);
}
} catch (FileNotFoundException e) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
}
}
private BlobInfo getBlobInfo(BlobKey blobKey) {
return getStorage().getBlobInfo(blobKey);
}
private void assertNotCommited(HttpServletResponse response) {
if (response.isCommitted()) {
throw new IllegalStateException("Response was already committed.");
}
}
private void copyStream(InputStream in, OutputStream out, ByteRange range) throws IOException {
if (range == null) {
IOUtils.copyStream(in, out);
} else {
if (range.hasEnd()) {
long length = range.getEnd() + 1 - range.getStart(); // end is inclusive, hence +1
IOUtils.copyStream(in, out, range.getStart(), length);
} else {
IOUtils.copyStream(in, out, range.getStart());
}
}
}
public byte[] fetchData(BlobKey blobKey, long startIndex, long endIndex) {
if (startIndex < 0) {
throw new IllegalArgumentException("startIndex must be >= 0");
}
if (endIndex < startIndex) {
throw new IllegalArgumentException("endIndex must be >= startIndex");
}
long fetchSize = endIndex - startIndex + 1;
if (fetchSize > MAX_BLOB_FETCH_SIZE) {
throw new IllegalArgumentException("Blob fetch size " + fetchSize + " is larger than MAX_BLOB_FETCH_SIZE (" + MAX_BLOB_FETCH_SIZE + ")");
}
try {
InputStream stream = getStream(blobKey);
return IOUtils.toBytes(stream, startIndex, endIndex, true);
} catch (FileNotFoundException e) {
throw new IllegalArgumentException("Blob does not exist");
} catch (IOException e) {
throw new BlobstoreFailureException("An unexpected error occured", e);
}
}
@SuppressWarnings("unchecked")
public ByteRange getByteRange(HttpServletRequest request) {
Enumeration<String> rangeHeaders = request.getHeaders("range");
if (!rangeHeaders.hasMoreElements()) {
return null;
}
String rangeHeader = rangeHeaders.nextElement();
if (rangeHeaders.hasMoreElements()) {
throw new UnsupportedRangeFormatException("Cannot accept multiple range headers.");
}
return ByteRange.parse(rangeHeader);
}
public boolean storeUploadedBlobs(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
Map<String, BlobKey> map = new HashMap<String, BlobKey>();
Map<String, List<BlobKey>> map2 = new HashMap<String, List<BlobKey>>();
String perBlob = request.getParameter(UploadServlet.MAX_PER_BLOB);
String all = request.getParameter(UploadServlet.MAX_ALL);
long maxPerBlob = (perBlob != null) ? Long.parseLong(perBlob) : -1;
long maxAll = (all != null) ? Long.parseLong(all) : -1;
long total = 0;
for (Part part : request.getParts()) {
if (ServletUtils.isFile(part)) {
long size = part.getSize();
if (size >= 0) {
if (maxPerBlob >= 0 && size > maxPerBlob) {
response.sendError(HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE, "Your client issued a request that was too large. Maximum upload size per blob limit exceeded.");
return false;
}
total += size;
if (maxAll >= 0 && total > maxAll) {
response.sendError(HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE, "Your client issued a request that was too large. Maximum total upload size limit exceeded.");
return false;
}
} else {
log.warning("Unable to determine size of upload part named " + ServletUtils.getFileName(part) + ". Upload limit checks may not be accurate.");
}
BlobKey blobKey = storeUploadedBlob(part, request);
String name = part.getName();
map.put(name, blobKey);
List<BlobKey> list = map2.get(name);
if (list == null) {
list = new LinkedList<BlobKey>();
map2.put(name, list);
}
list.add(blobKey);
}
}
request.setAttribute(UPLOADED_BLOBKEY_ATTR, map);
request.setAttribute(UPLOADED_BLOBKEY_LIST_ATTR, map2);
return true;
}
private BlobKey storeUploadedBlob(final Part part, HttpServletRequest request) throws IOException {
String contentType = part.getContentType();
String fileName = ServletUtils.getFileName(part);
String bucketName = request.getParameter(UploadServlet.BUCKET_NAME);
return getStorage().store(new Storage.StreamProvider() {
public InputStream get() throws IOException {
return part.getInputStream();
}
}, fileName, contentType, bucketName);
}
@SuppressWarnings("unchecked")
public Map<String, BlobKey> getUploadedBlobs(HttpServletRequest request) {
Map<String, BlobKey> map = (Map<String, BlobKey>) request.getAttribute(UPLOADED_BLOBKEY_ATTR);
if (map == null) {
throw new IllegalStateException("Must be called from a blob upload callback request.");
}
return map;
}
@SuppressWarnings("unchecked")
public Map<String, List<BlobKey>> getUploads(HttpServletRequest request) {
Map<String, List<BlobKey>> map = (Map<String, List<BlobKey>>) request.getAttribute(UPLOADED_BLOBKEY_LIST_ATTR);
if (map == null) {
throw new IllegalStateException("Must be called from a blob upload callback request.");
}
return map;
}
protected InputStream getStream(BlobKey blobKey) throws IOException {
return getStorage().getStream(blobKey);
}
public BlobKey createGsBlobKey(String filename) {
if (filename.startsWith("/gs/") == false) {
throw new IllegalArgumentException("Google storage filenames must be prefixed with /gs/");
}
//noinspection deprecation
return getStorage().getBlobKey(new AppEngineFile(filename));
}
public Map<String, List<BlobInfo>> getBlobInfos(HttpServletRequest httpServletRequest) {
return Maps.transformValues(getUploads(httpServletRequest), BLOB_LIST_KEY_TO_INFO_FN);
}
public Map<String, List<FileInfo>> getFileInfos(HttpServletRequest httpServletRequest) {
return Maps.transformValues(getUploads(httpServletRequest), FILE_LIST_KEY_TO_INFO_FN);
}
}