Revision created by MOE tool push_codebase.
[gae.git] / java / src / main / com / google / appengine / api / blobstore / BlobstoreServiceImpl.java
blob60c2d84e478d47b0136a847a8b114c3aff94af27
1 // Copyright 2009 Google Inc. All Rights Reserved.
3 package com.google.appengine.api.blobstore;
5 import com.google.appengine.api.blobstore.BlobstoreServicePb.BlobstoreServiceError;
6 import com.google.appengine.api.blobstore.BlobstoreServicePb.CreateEncodedGoogleStorageKeyRequest;
7 import com.google.appengine.api.blobstore.BlobstoreServicePb.CreateEncodedGoogleStorageKeyResponse;
8 import com.google.appengine.api.blobstore.BlobstoreServicePb.CreateUploadURLRequest;
9 import com.google.appengine.api.blobstore.BlobstoreServicePb.CreateUploadURLResponse;
10 import com.google.appengine.api.blobstore.BlobstoreServicePb.DeleteBlobRequest;
11 import com.google.appengine.api.blobstore.BlobstoreServicePb.FetchDataRequest;
12 import com.google.appengine.api.blobstore.BlobstoreServicePb.FetchDataResponse;
13 import com.google.apphosting.api.ApiProxy;
14 import com.google.common.annotations.VisibleForTesting;
16 import java.text.ParseException;
17 import java.text.SimpleDateFormat;
18 import java.util.ArrayList;
19 import java.util.Date;
20 import java.util.Enumeration;
21 import java.util.HashMap;
22 import java.util.List;
23 import java.util.Map;
25 import javax.servlet.http.HttpServletRequest;
26 import javax.servlet.http.HttpServletResponse;
28 /**
29 * {@code BlobstoreServiceImpl} is an implementation of {@link
30 * BlobstoreService} that makes API calls to {@link ApiProxy}.
33 class BlobstoreServiceImpl implements BlobstoreService {
34 static final String PACKAGE = "blobstore";
35 static final String SERVE_HEADER = "X-AppEngine-BlobKey";
36 static final String UPLOADED_BLOBKEY_ATTR = "com.google.appengine.api.blobstore.upload.blobkeys";
37 static final String UPLOADED_BLOBINFO_ATTR =
38 "com.google.appengine.api.blobstore.upload.blobinfos";
39 static final String BLOB_RANGE_HEADER = "X-AppEngine-BlobRange";
40 static final String CREATION_DATE_FORMAT = "yyyy-MM-dd HH:mm:ss.SSS";
42 public String createUploadUrl(String successPath) {
43 return createUploadUrl(successPath, UploadOptions.Builder.withDefaults());
46 public String createUploadUrl(String successPath, UploadOptions uploadOptions) {
47 if (successPath == null) {
48 throw new NullPointerException("Success path must not be null.");
51 CreateUploadURLRequest request = new CreateUploadURLRequest();
52 request.setSuccessPath(successPath);
54 if (uploadOptions.hasMaxUploadSizeBytesPerBlob()) {
55 request.setMaxUploadSizePerBlobBytes(uploadOptions.getMaxUploadSizeBytesPerBlob());
58 if (uploadOptions.hasMaxUploadSizeBytes()) {
59 request.setMaxUploadSizeBytes(uploadOptions.getMaxUploadSizeBytes());
62 if (uploadOptions.hasGoogleStorageBucketName()) {
63 request.setGsBucketName(uploadOptions.getGoogleStorageBucketName());
66 byte[] responseBytes;
67 try {
68 responseBytes = ApiProxy.makeSyncCall(PACKAGE, "CreateUploadURL", request.toByteArray());
69 } catch (ApiProxy.ApplicationException ex) {
70 switch (BlobstoreServiceError.ErrorCode.valueOf(ex.getApplicationError())) {
71 case URL_TOO_LONG:
72 throw new IllegalArgumentException("The resulting URL was too long.");
73 case INTERNAL_ERROR:
74 throw new BlobstoreFailureException("An internal blobstore error occured.");
75 default:
76 throw new BlobstoreFailureException("An unexpected error occurred.", ex);
80 CreateUploadURLResponse response = new CreateUploadURLResponse();
81 response.mergeFrom(responseBytes);
82 return response.getUrl();
85 public void serve(BlobKey blobKey, HttpServletResponse response) {
86 serve(blobKey, (ByteRange) null, response);
89 public void serve(BlobKey blobKey, String rangeHeader, HttpServletResponse response) {
90 serve(blobKey, ByteRange.parse(rangeHeader), response);
93 public void serve(BlobKey blobKey, ByteRange byteRange, HttpServletResponse response) {
94 if (response.isCommitted()) {
95 throw new IllegalStateException("Response was already committed.");
98 response.setStatus(HttpServletResponse.SC_OK);
99 response.setHeader(SERVE_HEADER, blobKey.getKeyString());
100 if (byteRange != null) {
101 response.setHeader(BLOB_RANGE_HEADER, byteRange.toString());
105 public ByteRange getByteRange(HttpServletRequest request) {
106 @SuppressWarnings("unchecked")
107 Enumeration<String> rangeHeaders = request.getHeaders("range");
108 if (!rangeHeaders.hasMoreElements()) {
109 return null;
112 String rangeHeader = rangeHeaders.nextElement();
113 if (rangeHeaders.hasMoreElements()) {
114 throw new UnsupportedRangeFormatException("Cannot accept multiple range headers.");
117 return ByteRange.parse(rangeHeader);
120 public void delete(BlobKey... blobKeys) {
121 DeleteBlobRequest request = new DeleteBlobRequest();
122 for (BlobKey blobKey : blobKeys) {
123 request.addBlobKey(blobKey.getKeyString());
126 if (request.blobKeySize() == 0) {
127 return;
130 byte[] responseBytes;
131 try {
132 responseBytes = ApiProxy.makeSyncCall(PACKAGE, "DeleteBlob", request.toByteArray());
133 } catch (ApiProxy.ApplicationException ex) {
134 switch (BlobstoreServiceError.ErrorCode.valueOf(ex.getApplicationError())) {
135 case INTERNAL_ERROR:
136 throw new BlobstoreFailureException("An internal blobstore error occured.");
137 default:
138 throw new BlobstoreFailureException("An unexpected error occurred.", ex);
143 @Deprecated public Map<String, BlobKey> getUploadedBlobs(HttpServletRequest request) {
144 Map<String, List<BlobKey>> blobKeys = getUploads(request);
145 Map<String, BlobKey> result = new HashMap<String, BlobKey>(blobKeys.size());
147 for (Map.Entry<String, List<BlobKey>> entry : blobKeys.entrySet()) {
148 if (!entry.getValue().isEmpty()) {
149 result.put(entry.getKey(), entry.getValue().get(0));
152 return result;
155 public Map<String, List<BlobKey>> getUploads(HttpServletRequest request) {
156 @SuppressWarnings("unchecked")
157 Map<String, List<String>> attributes =
158 (Map<String, List<String>>) request.getAttribute(UPLOADED_BLOBKEY_ATTR);
159 if (attributes == null) {
160 throw new IllegalStateException("Must be called from a blob upload callback request.");
162 Map<String, List<BlobKey>> blobKeys = new HashMap<String, List<BlobKey>>(attributes.size());
163 for (Map.Entry<String, List<String>> attr : attributes.entrySet()) {
164 List<BlobKey> blobs = new ArrayList<BlobKey>(attr.getValue().size());
165 for (String key : attr.getValue()) {
166 blobs.add(new BlobKey(key));
168 blobKeys.put(attr.getKey(), blobs);
170 return blobKeys;
173 public Map<String, List<BlobInfo>> getBlobInfos(HttpServletRequest request) {
174 @SuppressWarnings("unchecked")
175 Map<String, List<Map<String, String>>> attributes =
176 (Map<String, List<Map<String, String>>>) request.getAttribute(UPLOADED_BLOBINFO_ATTR);
177 if (attributes == null) {
178 throw new IllegalStateException("Must be called from a blob upload callback request.");
180 Map<String, List<BlobInfo>> blobInfos = new HashMap<String, List<BlobInfo>>(attributes.size());
181 for (Map.Entry<String, List<Map<String, String>>> attr : attributes.entrySet()) {
182 List<BlobInfo> blobs = new ArrayList<BlobInfo>(attr.getValue().size());
183 for (Map<String, String> info : attr.getValue()) {
184 BlobKey key = new BlobKey(info.get("key"));
185 String contentType = info.get("content-type");
186 Date creationDate = parseCreationDate(info.get("creation-date"));
187 String filename = info.get("filename");
188 int size = Integer.parseInt(info.get("size"));
189 String md5Hash = info.get("md5-hash");
190 blobs.add(new BlobInfo(key, contentType, creationDate, filename, size, md5Hash));
192 blobInfos.put(attr.getKey(), blobs);
194 return blobInfos;
197 public Map<String, List<FileInfo>> getFileInfos(HttpServletRequest request) {
198 @SuppressWarnings("unchecked")
199 Map<String, List<Map<String, String>>> attributes =
200 (Map<String, List<Map<String, String>>>) request.getAttribute(UPLOADED_BLOBINFO_ATTR);
201 if (attributes == null) {
202 throw new IllegalStateException("Must be called from a blob upload callback request.");
204 Map<String, List<FileInfo>> fileInfos = new HashMap<String, List<FileInfo>>(attributes.size());
205 for (Map.Entry<String, List<Map<String, String>>> attr : attributes.entrySet()) {
206 List<FileInfo> files = new ArrayList<FileInfo>(attr.getValue().size());
207 for (Map<String, String> info : attr.getValue()) {
208 String contentType = info.get("content-type");
209 Date creationDate = parseCreationDate(info.get("creation-date"));
210 String filename = info.get("filename");
211 int size = Integer.parseInt(info.get("size"));
212 String md5Hash = info.get("md5-hash");
213 String gsObjectName = null;
214 if (info.containsKey("gs-name")) {
215 gsObjectName = info.get("gs-name");
217 files.add(new FileInfo(contentType, creationDate, filename, size, md5Hash,
218 gsObjectName));
220 fileInfos.put(attr.getKey(), files);
222 return fileInfos;
225 @VisibleForTesting
226 protected static Date parseCreationDate(String date) {
227 Date creationDate = null;
228 try {
229 date = date.trim().substring(0, CREATION_DATE_FORMAT.length());
230 SimpleDateFormat dateFormat = new SimpleDateFormat(CREATION_DATE_FORMAT);
231 dateFormat.setLenient(false);
232 creationDate = dateFormat.parse(date);
233 } catch (IndexOutOfBoundsException e) {
234 } catch (ParseException e) {
236 return creationDate;
239 public byte[] fetchData(BlobKey blobKey, long startIndex, long endIndex) {
240 if (startIndex < 0) {
241 throw new IllegalArgumentException("Start index must be >= 0.");
244 if (endIndex < startIndex) {
245 throw new IllegalArgumentException("End index must be >= startIndex.");
248 long fetchSize = endIndex - startIndex + 1;
249 if (fetchSize > MAX_BLOB_FETCH_SIZE) {
250 throw new IllegalArgumentException("Blob fetch size " + fetchSize + " it larger " +
251 "than maximum size " + MAX_BLOB_FETCH_SIZE + " bytes.");
254 FetchDataRequest request = new FetchDataRequest();
255 request.setBlobKey(blobKey.getKeyString());
256 request.setStartIndex(startIndex);
257 request.setEndIndex(endIndex);
259 byte[] responseBytes;
260 try {
261 responseBytes = ApiProxy.makeSyncCall(PACKAGE, "FetchData", request.toByteArray());
262 } catch (ApiProxy.ApplicationException ex) {
263 switch (BlobstoreServiceError.ErrorCode.valueOf(ex.getApplicationError())) {
264 case PERMISSION_DENIED:
265 throw new SecurityException("This application does not have access to that blob.");
266 case BLOB_NOT_FOUND:
267 throw new IllegalArgumentException("Blob not found.");
268 case INTERNAL_ERROR:
269 throw new BlobstoreFailureException("An internal blobstore error occured.");
270 default:
271 throw new BlobstoreFailureException("An unexpected error occurred.", ex);
275 FetchDataResponse response = new FetchDataResponse();
276 response.mergeFrom(responseBytes);
277 return response.getDataAsBytes();
280 public BlobKey createGsBlobKey(String filename) {
282 if (!filename.startsWith("/gs/")) {
283 throw new IllegalArgumentException("Google storage filenames must be" +
284 " prefixed with /gs/");
286 CreateEncodedGoogleStorageKeyRequest request = new CreateEncodedGoogleStorageKeyRequest();
287 request.setFilename(filename);
289 byte[] responseBytes;
290 try {
291 responseBytes = ApiProxy.makeSyncCall(PACKAGE,
292 "CreateEncodedGoogleStorageKey", request.toByteArray());
293 } catch (ApiProxy.ApplicationException ex) {
294 switch (BlobstoreServiceError.ErrorCode.valueOf(ex.getApplicationError())) {
295 case INTERNAL_ERROR:
296 throw new BlobstoreFailureException("An internal blobstore error occured.");
297 default:
298 throw new BlobstoreFailureException("An unexpected error occurred.", ex);
302 CreateEncodedGoogleStorageKeyResponse response = new CreateEncodedGoogleStorageKeyResponse();
303 response.mergeFrom(responseBytes);
304 return new BlobKey(response.getBlobKey());