Revision created by MOE tool push_codebase.
[gae.git] / java / src / main / com / google / appengine / api / blobstore / BlobstoreServiceImpl.java
blob81395d1c7af754b9ce16952913cae268df12105a
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 @Override
43 public String createUploadUrl(String successPath) {
44 return createUploadUrl(successPath, UploadOptions.Builder.withDefaults());
47 @Override
48 public String createUploadUrl(String successPath, UploadOptions uploadOptions) {
49 if (successPath == null) {
50 throw new NullPointerException("Success path must not be null.");
53 CreateUploadURLRequest request = new CreateUploadURLRequest();
54 request.setSuccessPath(successPath);
56 if (uploadOptions.hasMaxUploadSizeBytesPerBlob()) {
57 request.setMaxUploadSizePerBlobBytes(uploadOptions.getMaxUploadSizeBytesPerBlob());
60 if (uploadOptions.hasMaxUploadSizeBytes()) {
61 request.setMaxUploadSizeBytes(uploadOptions.getMaxUploadSizeBytes());
64 if (uploadOptions.hasGoogleStorageBucketName()) {
65 request.setGsBucketName(uploadOptions.getGoogleStorageBucketName());
68 byte[] responseBytes;
69 try {
70 responseBytes = ApiProxy.makeSyncCall(PACKAGE, "CreateUploadURL", request.toByteArray());
71 } catch (ApiProxy.ApplicationException ex) {
72 switch (BlobstoreServiceError.ErrorCode.valueOf(ex.getApplicationError())) {
73 case URL_TOO_LONG:
74 throw new IllegalArgumentException("The resulting URL was too long.");
75 case INTERNAL_ERROR:
76 throw new BlobstoreFailureException("An internal blobstore error occured.");
77 default:
78 throw new BlobstoreFailureException("An unexpected error occurred.", ex);
82 CreateUploadURLResponse response = new CreateUploadURLResponse();
83 response.mergeFrom(responseBytes);
84 return response.getUrl();
87 @Override
88 public void serve(BlobKey blobKey, HttpServletResponse response) {
89 serve(blobKey, (ByteRange) null, response);
92 @Override
93 public void serve(BlobKey blobKey, String rangeHeader, HttpServletResponse response) {
94 serve(blobKey, ByteRange.parse(rangeHeader), response);
97 @Override
98 public void serve(BlobKey blobKey, ByteRange byteRange, HttpServletResponse response) {
99 if (response.isCommitted()) {
100 throw new IllegalStateException("Response was already committed.");
103 response.setStatus(HttpServletResponse.SC_OK);
104 response.setHeader(SERVE_HEADER, blobKey.getKeyString());
105 if (byteRange != null) {
106 response.setHeader(BLOB_RANGE_HEADER, byteRange.toString());
110 @Override
111 public ByteRange getByteRange(HttpServletRequest request) {
112 @SuppressWarnings("unchecked")
113 Enumeration<String> rangeHeaders = request.getHeaders("range");
114 if (!rangeHeaders.hasMoreElements()) {
115 return null;
118 String rangeHeader = rangeHeaders.nextElement();
119 if (rangeHeaders.hasMoreElements()) {
120 throw new UnsupportedRangeFormatException("Cannot accept multiple range headers.");
123 return ByteRange.parse(rangeHeader);
126 @Override
127 public void delete(BlobKey... blobKeys) {
128 DeleteBlobRequest request = new DeleteBlobRequest();
129 for (BlobKey blobKey : blobKeys) {
130 request.addBlobKey(blobKey.getKeyString());
133 if (request.blobKeySize() == 0) {
134 return;
137 byte[] responseBytes;
138 try {
139 responseBytes = ApiProxy.makeSyncCall(PACKAGE, "DeleteBlob", request.toByteArray());
140 } catch (ApiProxy.ApplicationException ex) {
141 switch (BlobstoreServiceError.ErrorCode.valueOf(ex.getApplicationError())) {
142 case INTERNAL_ERROR:
143 throw new BlobstoreFailureException("An internal blobstore error occured.");
144 default:
145 throw new BlobstoreFailureException("An unexpected error occurred.", ex);
150 @Override
151 @Deprecated public Map<String, BlobKey> getUploadedBlobs(HttpServletRequest request) {
152 Map<String, List<BlobKey>> blobKeys = getUploads(request);
153 Map<String, BlobKey> result = new HashMap<String, BlobKey>(blobKeys.size());
155 for (Map.Entry<String, List<BlobKey>> entry : blobKeys.entrySet()) {
156 if (!entry.getValue().isEmpty()) {
157 result.put(entry.getKey(), entry.getValue().get(0));
160 return result;
163 @Override
164 public Map<String, List<BlobKey>> getUploads(HttpServletRequest request) {
165 @SuppressWarnings("unchecked")
166 Map<String, List<String>> attributes =
167 (Map<String, List<String>>) request.getAttribute(UPLOADED_BLOBKEY_ATTR);
168 if (attributes == null) {
169 throw new IllegalStateException("Must be called from a blob upload callback request.");
171 Map<String, List<BlobKey>> blobKeys = new HashMap<String, List<BlobKey>>(attributes.size());
172 for (Map.Entry<String, List<String>> attr : attributes.entrySet()) {
173 List<BlobKey> blobs = new ArrayList<BlobKey>(attr.getValue().size());
174 for (String key : attr.getValue()) {
175 blobs.add(new BlobKey(key));
177 blobKeys.put(attr.getKey(), blobs);
179 return blobKeys;
182 @Override
183 public Map<String, List<BlobInfo>> getBlobInfos(HttpServletRequest request) {
184 @SuppressWarnings("unchecked")
185 Map<String, List<Map<String, String>>> attributes =
186 (Map<String, List<Map<String, String>>>) request.getAttribute(UPLOADED_BLOBINFO_ATTR);
187 if (attributes == null) {
188 throw new IllegalStateException("Must be called from a blob upload callback request.");
190 Map<String, List<BlobInfo>> blobInfos = new HashMap<String, List<BlobInfo>>(attributes.size());
191 for (Map.Entry<String, List<Map<String, String>>> attr : attributes.entrySet()) {
192 List<BlobInfo> blobs = new ArrayList<BlobInfo>(attr.getValue().size());
193 for (Map<String, String> info : attr.getValue()) {
194 BlobKey key = new BlobKey(info.get("key"));
195 String contentType = info.get("content-type");
196 Date creationDate = parseCreationDate(info.get("creation-date"));
197 String filename = info.get("filename");
198 int size = Integer.parseInt(info.get("size"));
199 String md5Hash = info.get("md5-hash");
200 blobs.add(new BlobInfo(key, contentType, creationDate, filename, size, md5Hash));
202 blobInfos.put(attr.getKey(), blobs);
204 return blobInfos;
207 @Override
208 public Map<String, List<FileInfo>> getFileInfos(HttpServletRequest request) {
209 @SuppressWarnings("unchecked")
210 Map<String, List<Map<String, String>>> attributes =
211 (Map<String, List<Map<String, String>>>) request.getAttribute(UPLOADED_BLOBINFO_ATTR);
212 if (attributes == null) {
213 throw new IllegalStateException("Must be called from a blob upload callback request.");
215 Map<String, List<FileInfo>> fileInfos = new HashMap<String, List<FileInfo>>(attributes.size());
216 for (Map.Entry<String, List<Map<String, String>>> attr : attributes.entrySet()) {
217 List<FileInfo> files = new ArrayList<FileInfo>(attr.getValue().size());
218 for (Map<String, String> info : attr.getValue()) {
219 String contentType = info.get("content-type");
220 Date creationDate = parseCreationDate(info.get("creation-date"));
221 String filename = info.get("filename");
222 long size = Long.parseLong(info.get("size"));
223 String md5Hash = info.get("md5-hash");
224 String gsObjectName = null;
225 if (info.containsKey("gs-name")) {
226 gsObjectName = info.get("gs-name");
228 files.add(new FileInfo(contentType, creationDate, filename, size, md5Hash,
229 gsObjectName));
231 fileInfos.put(attr.getKey(), files);
233 return fileInfos;
236 @VisibleForTesting
237 protected static Date parseCreationDate(String date) {
238 Date creationDate = null;
239 try {
240 date = date.trim().substring(0, CREATION_DATE_FORMAT.length());
241 SimpleDateFormat dateFormat = new SimpleDateFormat(CREATION_DATE_FORMAT);
242 dateFormat.setLenient(false);
243 creationDate = dateFormat.parse(date);
244 } catch (IndexOutOfBoundsException e) {
245 } catch (ParseException e) {
247 return creationDate;
250 @Override
251 public byte[] fetchData(BlobKey blobKey, long startIndex, long endIndex) {
252 if (startIndex < 0) {
253 throw new IllegalArgumentException("Start index must be >= 0.");
256 if (endIndex < startIndex) {
257 throw new IllegalArgumentException("End index must be >= startIndex.");
260 long fetchSize = endIndex - startIndex + 1;
261 if (fetchSize > MAX_BLOB_FETCH_SIZE) {
262 throw new IllegalArgumentException("Blob fetch size " + fetchSize + " is larger " +
263 "than maximum size " + MAX_BLOB_FETCH_SIZE + " bytes.");
266 FetchDataRequest request = new FetchDataRequest();
267 request.setBlobKey(blobKey.getKeyString());
268 request.setStartIndex(startIndex);
269 request.setEndIndex(endIndex);
271 byte[] responseBytes;
272 try {
273 responseBytes = ApiProxy.makeSyncCall(PACKAGE, "FetchData", request.toByteArray());
274 } catch (ApiProxy.ApplicationException ex) {
275 switch (BlobstoreServiceError.ErrorCode.valueOf(ex.getApplicationError())) {
276 case PERMISSION_DENIED:
277 throw new SecurityException("This application does not have access to that blob.");
278 case BLOB_NOT_FOUND:
279 throw new IllegalArgumentException("Blob not found.");
280 case INTERNAL_ERROR:
281 throw new BlobstoreFailureException("An internal blobstore error occured.");
282 default:
283 throw new BlobstoreFailureException("An unexpected error occurred.", ex);
287 FetchDataResponse response = new FetchDataResponse();
288 response.mergeFrom(responseBytes);
289 return response.getDataAsBytes();
292 @Override
293 public BlobKey createGsBlobKey(String filename) {
295 if (!filename.startsWith("/gs/")) {
296 throw new IllegalArgumentException("Google storage filenames must be" +
297 " prefixed with /gs/");
299 CreateEncodedGoogleStorageKeyRequest request = new CreateEncodedGoogleStorageKeyRequest();
300 request.setFilename(filename);
302 byte[] responseBytes;
303 try {
304 responseBytes = ApiProxy.makeSyncCall(PACKAGE,
305 "CreateEncodedGoogleStorageKey", request.toByteArray());
306 } catch (ApiProxy.ApplicationException ex) {
307 switch (BlobstoreServiceError.ErrorCode.valueOf(ex.getApplicationError())) {
308 case INTERNAL_ERROR:
309 throw new BlobstoreFailureException("An internal blobstore error occured.");
310 default:
311 throw new BlobstoreFailureException("An unexpected error occurred.", ex);
315 CreateEncodedGoogleStorageKeyResponse response = new CreateEncodedGoogleStorageKeyResponse();
316 response.mergeFrom(responseBytes);
317 return new BlobKey(response.getBlobKey());