View Javadoc

1   /**
2    *
3    * Copyright (C) 2011 Cloud Conscious, LLC. <info@cloudconscious.com>
4    *
5    * ====================================================================
6    * Licensed under the Apache License, Version 2.0 (the "License");
7    * you may not use this file except in compliance with the License.
8    * You may obtain a copy of the License at
9    *
10   * http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   * ====================================================================
18   */
19  package org.jclouds.blobstore;
20  
21  import static com.google.common.base.Preconditions.checkArgument;
22  import static com.google.common.base.Preconditions.checkNotNull;
23  import static com.google.common.base.Preconditions.checkState;
24  import static com.google.common.base.Throwables.getCausalChain;
25  import static com.google.common.base.Throwables.propagate;
26  import static com.google.common.collect.Iterables.filter;
27  import static com.google.common.collect.Iterables.find;
28  import static com.google.common.collect.Iterables.size;
29  import static com.google.common.collect.Iterables.transform;
30  import static com.google.common.collect.Lists.newArrayList;
31  import static com.google.common.collect.Lists.partition;
32  import static com.google.common.collect.Maps.newHashMap;
33  import static com.google.common.collect.Sets.filter;
34  import static com.google.common.collect.Sets.newTreeSet;
35  import static com.google.common.io.ByteStreams.toByteArray;
36  import static com.google.common.util.concurrent.Futures.immediateFailedFuture;
37  import static com.google.common.util.concurrent.Futures.immediateFuture;
38  
39  import java.io.ByteArrayInputStream;
40  import java.io.ByteArrayOutputStream;
41  import java.io.IOException;
42  import java.io.ObjectInput;
43  import java.io.ObjectInputStream;
44  import java.io.ObjectOutput;
45  import java.io.ObjectOutputStream;
46  import java.net.URI;
47  import java.util.Collection;
48  import java.util.Collections;
49  import java.util.Date;
50  import java.util.List;
51  import java.util.Map;
52  import java.util.Set;
53  import java.util.SortedSet;
54  import java.util.TreeSet;
55  import java.util.Map.Entry;
56  import java.util.concurrent.ConcurrentHashMap;
57  import java.util.concurrent.ConcurrentMap;
58  import java.util.concurrent.ExecutorService;
59  
60  import javax.annotation.Nullable;
61  import javax.inject.Inject;
62  import javax.inject.Named;
63  import javax.inject.Provider;
64  import javax.ws.rs.core.HttpHeaders;
65  import javax.ws.rs.core.UriBuilder;
66  
67  import org.jclouds.Constants;
68  import org.jclouds.blobstore.domain.Blob;
69  import org.jclouds.blobstore.domain.BlobMetadata;
70  import org.jclouds.blobstore.domain.MutableBlobMetadata;
71  import org.jclouds.blobstore.domain.MutableStorageMetadata;
72  import org.jclouds.blobstore.domain.PageSet;
73  import org.jclouds.blobstore.domain.StorageMetadata;
74  import org.jclouds.blobstore.domain.StorageType;
75  import org.jclouds.blobstore.domain.Blob.Factory;
76  import org.jclouds.blobstore.domain.internal.MutableStorageMetadataImpl;
77  import org.jclouds.blobstore.domain.internal.PageSetImpl;
78  import org.jclouds.blobstore.functions.HttpGetOptionsListToGetOptions;
79  import org.jclouds.blobstore.internal.BaseAsyncBlobStore;
80  import org.jclouds.blobstore.options.CreateContainerOptions;
81  import org.jclouds.blobstore.options.GetOptions;
82  import org.jclouds.blobstore.options.ListContainerOptions;
83  import org.jclouds.blobstore.options.PutOptions;
84  import org.jclouds.blobstore.strategy.IfDirectoryReturnNameStrategy;
85  import org.jclouds.blobstore.util.BlobUtils;
86  import org.jclouds.collect.Memoized;
87  import org.jclouds.crypto.Crypto;
88  import org.jclouds.crypto.CryptoStreams;
89  import org.jclouds.date.DateService;
90  import org.jclouds.domain.Location;
91  import org.jclouds.http.HttpCommand;
92  import org.jclouds.http.HttpRequest;
93  import org.jclouds.http.HttpResponse;
94  import org.jclouds.http.HttpResponseException;
95  import org.jclouds.http.HttpUtils;
96  import org.jclouds.http.options.HttpRequestOptions;
97  import org.jclouds.io.ContentMetadata;
98  import org.jclouds.io.MutableContentMetadata;
99  import org.jclouds.io.Payload;
100 import org.jclouds.io.Payloads;
101 import org.jclouds.io.payloads.ByteArrayPayload;
102 import org.jclouds.io.payloads.DelegatingPayload;
103 
104 import com.google.common.base.Function;
105 import com.google.common.base.Predicate;
106 import com.google.common.base.Supplier;
107 import com.google.common.base.Throwables;
108 import com.google.common.collect.Iterables;
109 import com.google.common.collect.Multimaps;
110 import com.google.common.util.concurrent.Futures;
111 import com.google.common.util.concurrent.ListenableFuture;
112 
113 /**
114  * Implementation of {@link BaseAsyncBlobStore} which keeps all data in a local Map object.
115  * 
116  * @author Adrian Cole
117  * @author James Murty
118  */
119 public class TransientAsyncBlobStore extends BaseAsyncBlobStore {
120 
121    protected final DateService dateService;
122    protected final Crypto crypto;
123    protected final ConcurrentMap<String, ConcurrentMap<String, Blob>> containerToBlobs;
124    protected final Provider<UriBuilder> uriBuilders;
125    protected final ConcurrentMap<String, Location> containerToLocation;
126    protected final HttpGetOptionsListToGetOptions httpGetOptionsConverter;
127    protected final IfDirectoryReturnNameStrategy ifDirectoryReturnName;
128    protected final Factory blobFactory;
129 
130    @Inject
131    protected TransientAsyncBlobStore(BlobStoreContext context, DateService dateService, Crypto crypto,
132             ConcurrentMap<String, ConcurrentMap<String, Blob>> containerToBlobs, Provider<UriBuilder> uriBuilders,
133             ConcurrentMap<String, Location> containerToLocation,
134             HttpGetOptionsListToGetOptions httpGetOptionsConverter,
135             IfDirectoryReturnNameStrategy ifDirectoryReturnName, Factory blobFactory, BlobUtils blobUtils,
136             @Named(Constants.PROPERTY_USER_THREADS) ExecutorService service, Supplier<Location> defaultLocation,
137             @Memoized Supplier<Set<? extends Location>> locations) {
138       super(context, blobUtils, service, defaultLocation, locations);
139       this.blobFactory = blobFactory;
140       this.dateService = dateService;
141       this.crypto = crypto;
142       this.containerToBlobs = containerToBlobs;
143       this.uriBuilders = uriBuilders;
144       this.containerToLocation = containerToLocation;
145       this.httpGetOptionsConverter = httpGetOptionsConverter;
146       this.ifDirectoryReturnName = ifDirectoryReturnName;
147       getContainerToLocation().put("stub", defaultLocation.get());
148       getContainerToBlobs().put("stub", new ConcurrentHashMap<String, Blob>());
149    }
150 
151    /**
152     * default maxResults is 1000
153     */
154    @Override
155    public ListenableFuture<PageSet<? extends StorageMetadata>> list(final String container, ListContainerOptions options) {
156       final Map<String, Blob> realContents = getContainerToBlobs().get(container);
157 
158       if (realContents == null)
159          return immediateFailedFuture(cnfe(container));
160 
161       SortedSet<StorageMetadata> contents = newTreeSet(transform(realContents.keySet(),
162                new Function<String, StorageMetadata>() {
163                   public StorageMetadata apply(String key) {
164                      Blob oldBlob = realContents.get(key);
165                      checkState(oldBlob != null, "blob " + key + " is not present although it was in the list of "
166                               + container);
167                      checkState(oldBlob.getMetadata() != null, "blob " + container + "/" + key + " has no metadata");
168                      MutableBlobMetadata md = copy(oldBlob.getMetadata());
169                      String directoryName = ifDirectoryReturnName.execute(md);
170                      if (directoryName != null) {
171                         md.setName(directoryName);
172                         md.setType(StorageType.RELATIVE_PATH);
173                      }
174                      return md;
175                   }
176                }));
177 
178       if (options.getMarker() != null) {
179          final String finalMarker = options.getMarker();
180          StorageMetadata lastMarkerMetadata = find(contents, new Predicate<StorageMetadata>() {
181             public boolean apply(StorageMetadata metadata) {
182                return metadata.getName().equals(finalMarker);
183             }
184          });
185          contents = contents.tailSet(lastMarkerMetadata);
186          contents.remove(lastMarkerMetadata);
187       }
188 
189       final String prefix = options.getDir();
190       if (prefix != null) {
191          contents = newTreeSet(filter(contents, new Predicate<StorageMetadata>() {
192             public boolean apply(StorageMetadata o) {
193                return (o != null && o.getName().startsWith(prefix) && !o.getName().equals(prefix));
194             }
195          }));
196       }
197 
198       String marker = null;
199       Integer maxResults = options.getMaxResults() != null ? options.getMaxResults() : 1000;
200       if (contents.size() > 0) {
201          SortedSet<StorageMetadata> contentsSlice = firstSliceOfSize(contents, maxResults);
202          if (!contentsSlice.contains(contents.last())) {
203             // Partial listing
204             marker = contentsSlice.last().getName();
205          } else {
206             marker = null;
207          }
208          contents = contentsSlice;
209       }
210 
211       final String delimiter = options.isRecursive() ? null : "/";
212       if (delimiter != null) {
213          SortedSet<String> commonPrefixes = null;
214          Iterable<String> iterable = transform(contents, new CommonPrefixes(prefix != null ? prefix : null, delimiter));
215          commonPrefixes = iterable != null ? newTreeSet(iterable) : new TreeSet<String>();
216          commonPrefixes.remove(CommonPrefixes.NO_PREFIX);
217 
218          contents = newTreeSet(filter(contents, new DelimiterFilter(prefix != null ? prefix : null, delimiter)));
219 
220          Iterables.<StorageMetadata> addAll(contents, transform(commonPrefixes,
221                   new Function<String, StorageMetadata>() {
222                      public StorageMetadata apply(String o) {
223                         MutableStorageMetadata md = new MutableStorageMetadataImpl();
224                         md.setType(StorageType.RELATIVE_PATH);
225                         md.setName(o);
226                         return md;
227                      }
228                   }));
229       }
230 
231       // trim metadata, if the response isn't supposed to be detailed.
232       if (!options.isDetailed()) {
233          for (StorageMetadata md : contents) {
234             md.getUserMetadata().clear();
235          }
236       }
237 
238       return Futures.<PageSet<? extends StorageMetadata>> immediateFuture(new PageSetImpl<StorageMetadata>(contents,
239                marker));
240 
241    }
242 
243    private ContainerNotFoundException cnfe(final String name) {
244       return new ContainerNotFoundException(name, String.format("container %s not in %s", name, getContainerToBlobs()
245                .keySet()));
246    }
247 
248    public static MutableBlobMetadata copy(MutableBlobMetadata in) {
249       ByteArrayOutputStream bout = new ByteArrayOutputStream();
250       ObjectOutput os;
251       try {
252          os = new ObjectOutputStream(bout);
253          os.writeObject(in);
254          ObjectInput is = new ObjectInputStream(new ByteArrayInputStream(bout.toByteArray()));
255          MutableBlobMetadata out = (MutableBlobMetadata) is.readObject();
256          convertUserMetadataKeysToLowercase(out);
257          HttpUtils.copy(in.getContentMetadata(), out.getContentMetadata());
258          return out;
259       } catch (Exception e) {
260          propagate(e);
261          assert false : "exception should have propagated: " + e;
262          return null;
263       }
264    }
265 
266    private static void convertUserMetadataKeysToLowercase(MutableBlobMetadata metadata) {
267       Map<String, String> lowerCaseUserMetadata = newHashMap();
268       for (Entry<String, String> entry : metadata.getUserMetadata().entrySet()) {
269          lowerCaseUserMetadata.put(entry.getKey().toLowerCase(), entry.getValue());
270       }
271       metadata.setUserMetadata(lowerCaseUserMetadata);
272    }
273 
274    public static MutableBlobMetadata copy(MutableBlobMetadata in, String newKey) {
275       MutableBlobMetadata newMd = copy(in);
276       newMd.setName(newKey);
277       return newMd;
278    }
279 
280    /**
281     * {@inheritDoc}
282     */
283    @Override
284    public ListenableFuture<Void> removeBlob(final String container, final String key) {
285       if (getContainerToBlobs().containsKey(container)) {
286          getContainerToBlobs().get(container).remove(key);
287       }
288       return immediateFuture(null);
289    }
290 
291    public ListenableFuture<Blob> removeBlobAndReturnOld(String container, String key) {
292       if (getContainerToBlobs().containsKey(container)) {
293          return immediateFuture(getContainerToBlobs().get(container).remove(key));
294       }
295       return immediateFuture(null);
296    }
297 
298    /**
299     * {@inheritDoc}
300     */
301    @Override
302    public ListenableFuture<Void> clearContainer(final String container) {
303       getContainerToBlobs().get(container).clear();
304       return immediateFuture(null);
305    }
306 
307    /**
308     * {@inheritDoc}
309     */
310    @Override
311    public ListenableFuture<Void> deleteContainer(final String container) {
312       if (getContainerToBlobs().containsKey(container)) {
313          getContainerToBlobs().remove(container);
314       }
315       return immediateFuture(null);
316    }
317 
318    public ListenableFuture<Boolean> deleteContainerImpl(final String container) {
319       Boolean returnVal = true;
320       if (getContainerToBlobs().containsKey(container)) {
321          if (getContainerToBlobs().get(container).size() == 0)
322             getContainerToBlobs().remove(container);
323          else
324             returnVal = false;
325       }
326       return immediateFuture(returnVal);
327    }
328 
329    /**
330     * {@inheritDoc}
331     */
332    @Override
333    public ListenableFuture<Boolean> containerExists(final String container) {
334       return immediateFuture(getContainerToBlobs().containsKey(container));
335    }
336 
337    /**
338     * {@inheritDoc}
339     */
340    @Override
341    public ListenableFuture<PageSet<? extends StorageMetadata>> list() {
342       return Futures.<PageSet<? extends StorageMetadata>> immediateFuture(new PageSetImpl<StorageMetadata>(transform(
343                getContainerToBlobs().keySet(), new Function<String, StorageMetadata>() {
344                   public StorageMetadata apply(String name) {
345                      MutableStorageMetadata cmd = create();
346                      cmd.setName(name);
347                      cmd.setType(StorageType.CONTAINER);
348                      cmd.setLocation(getContainerToLocation().get(name));
349                      return cmd;
350                   }
351                }), null));
352    }
353 
354    protected MutableStorageMetadata create() {
355       return new MutableStorageMetadataImpl();
356    }
357 
358    /**
359     * {@inheritDoc}
360     */
361    @Override
362    public ListenableFuture<Boolean> createContainerInLocation(final Location location, final String name) {
363       if (!getContainerToBlobs().containsKey(name)) {
364          getContainerToBlobs().put(name, new ConcurrentHashMap<String, Blob>());
365          getContainerToLocation().put(name, location != null ? location : defaultLocation.get());
366       }
367       return immediateFuture(getContainerToBlobs().containsKey(name));
368    }
369 
370    /**
371     * throws IllegalStateException if the container already exists
372     */
373    public ListenableFuture<Void> createContainerInLocationIfAbsent(final Location location, final String name) {
374       ConcurrentMap<String, Blob> container = getContainerToBlobs().putIfAbsent(name,
375                new ConcurrentHashMap<String, Blob>());
376       if (container == null) {
377          getContainerToLocation().put(name, location != null ? location : defaultLocation.get());
378          return immediateFuture((Void) null);
379       } else {
380          return Futures.immediateFailedFuture(new IllegalStateException("container " + name + " already exists"));
381       }
382    }
383 
384    public String getFirstQueryOrNull(String string, @Nullable HttpRequestOptions options) {
385       if (options == null)
386          return null;
387       Collection<String> values = options.buildQueryParameters().get(string);
388       return (values != null && values.size() >= 1) ? values.iterator().next() : null;
389    }
390 
391    protected static class DelimiterFilter implements Predicate<StorageMetadata> {
392       private final String prefix;
393       private final String delimiter;
394 
395       public DelimiterFilter(String prefix, String delimiter) {
396          this.prefix = prefix;
397          this.delimiter = delimiter;
398       }
399 
400       public boolean apply(StorageMetadata metadata) {
401          if (prefix == null)
402             return metadata.getName().indexOf(delimiter) == -1;
403          // ensure we don't accidentally append twice
404          String toMatch = prefix.endsWith("/") ? prefix : prefix + delimiter;
405          if (metadata.getName().startsWith(toMatch)) {
406             String unprefixedName = metadata.getName().replaceFirst(toMatch, "");
407             if (unprefixedName.equals("")) {
408                // we are the prefix in this case, return false
409                return false;
410             }
411             return unprefixedName.indexOf(delimiter) == -1;
412          }
413          return false;
414       }
415    }
416 
417    protected static class CommonPrefixes implements Function<StorageMetadata, String> {
418       private final String prefix;
419       private final String delimiter;
420       public static final String NO_PREFIX = "NO_PREFIX";
421 
422       public CommonPrefixes(String prefix, String delimiter) {
423          this.prefix = prefix;
424          this.delimiter = delimiter;
425       }
426 
427       public String apply(StorageMetadata metadata) {
428          String working = metadata.getName();
429          if (prefix != null) {
430             // ensure we don't accidentally append twice
431             String toMatch = prefix.endsWith("/") ? prefix : prefix + delimiter;
432             if (working.startsWith(toMatch)) {
433                working = working.replaceFirst(toMatch, "");
434             }
435          }
436          if (working.contains(delimiter)) {
437             return working.substring(0, working.indexOf(delimiter));
438          }
439          return NO_PREFIX;
440       }
441    }
442 
443    public static <T extends Comparable<?>> SortedSet<T> firstSliceOfSize(Iterable<T> elements, int size) {
444       List<List<T>> slices = partition(newArrayList(elements), size);
445       return newTreeSet(slices.get(0));
446    }
447 
448    public static HttpResponseException returnResponseException(int code) {
449       HttpResponse response = null;
450       response = new HttpResponse(code, null, null);
451       return new HttpResponseException(new HttpCommand() {
452 
453          public int getRedirectCount() {
454             return 0;
455          }
456 
457          public int incrementRedirectCount() {
458             return 0;
459          }
460 
461          public boolean isReplayable() {
462             return false;
463          }
464 
465          public Exception getException() {
466             return null;
467          }
468 
469          public int getFailureCount() {
470             return 0;
471          }
472 
473          public HttpRequest getCurrentRequest() {
474             return new HttpRequest("GET", URI.create("http://stub"));
475          }
476 
477          public int incrementFailureCount() {
478             return 0;
479          }
480 
481          public void setException(Exception exception) {
482 
483          }
484 
485          @Override
486          public void setCurrentRequest(HttpRequest request) {
487 
488          }
489 
490       }, response);
491    }
492 
493    /**
494     * {@inheritDoc}
495     */
496    @Override
497    public ListenableFuture<String> putBlob(String containerName, Blob in) {
498       checkArgument(containerName != null, "containerName must be set");
499       checkArgument(in != null, "blob must be set");
500       ConcurrentMap<String, Blob> container = getContainerToBlobs().get(containerName);
501       if (container == null) {
502          new IllegalStateException("containerName not found: " + containerName);
503       }
504 
505       Blob blob = createUpdatedCopyOfBlobInContainer(containerName, in);
506 
507       container.put(blob.getMetadata().getName(), blob);
508 
509       return immediateFuture(Iterables.getOnlyElement(blob.getAllHeaders().get(HttpHeaders.ETAG)));
510    }
511 
512    public ListenableFuture<Blob> putBlobAndReturnOld(String containerName, Blob in) {
513       ConcurrentMap<String, Blob> container = getContainerToBlobs().get(containerName);
514       if (container == null) {
515          new IllegalStateException("containerName not found: " + containerName);
516       }
517 
518       Blob blob = createUpdatedCopyOfBlobInContainer(containerName, in);
519 
520       Blob old = container.put(blob.getMetadata().getName(), blob);
521 
522       return immediateFuture(old);
523    }
524 
525    protected Blob createUpdatedCopyOfBlobInContainer(String containerName, Blob in) {
526       checkNotNull(in, "blob");
527       checkNotNull(in.getPayload(), "blob.payload");
528       ByteArrayPayload payload = (in.getPayload() instanceof ByteArrayPayload) ? ByteArrayPayload.class.cast(in
529                .getPayload()) : null;
530       if (payload == null)
531          payload = (in.getPayload() instanceof DelegatingPayload) ? (DelegatingPayload.class.cast(in.getPayload())
532                   .getDelegate() instanceof ByteArrayPayload) ? ByteArrayPayload.class.cast(DelegatingPayload.class
533                   .cast(in.getPayload()).getDelegate()) : null : null;
534       try {
535          if (payload == null || !(payload instanceof ByteArrayPayload)) {
536             MutableContentMetadata oldMd = in.getPayload().getContentMetadata();
537             ByteArrayOutputStream out = new ByteArrayOutputStream();
538             in.getPayload().writeTo(out);
539             payload = (ByteArrayPayload) Payloads.calculateMD5(Payloads.newPayload(out.toByteArray()));
540             HttpUtils.copy(oldMd, payload.getContentMetadata());
541          } else {
542             if (payload.getContentMetadata().getContentMD5() == null)
543                Payloads.calculateMD5(in, crypto.md5());
544          }
545       } catch (IOException e) {
546          Throwables.propagate(e);
547       }
548       Blob blob = blobFactory.create(copy(in.getMetadata()));
549       blob.setPayload(payload);
550       blob.getMetadata().setContainer(containerName);
551       blob.getMetadata().setUri(
552                uriBuilders.get().scheme("mem").host(containerName).path(in.getMetadata().getName()).build());
553       blob.getMetadata().setLastModified(new Date());
554       String eTag = CryptoStreams.hex(payload.getContentMetadata().getContentMD5());
555       blob.getMetadata().setETag(eTag);
556       // Set HTTP headers to match metadata
557       blob.getAllHeaders().replaceValues(HttpHeaders.LAST_MODIFIED,
558                Collections.singleton(dateService.rfc822DateFormat(blob.getMetadata().getLastModified())));
559       blob.getAllHeaders().replaceValues(HttpHeaders.ETAG, Collections.singleton(eTag));
560       copyPayloadHeadersToBlob(payload, blob);
561       blob.getAllHeaders().putAll(Multimaps.forMap(blob.getMetadata().getUserMetadata()));
562       return blob;
563    }
564 
565    private void copyPayloadHeadersToBlob(Payload payload, Blob blob) {
566       blob.getAllHeaders().putAll(HttpUtils.getContentHeadersFromMetadata(payload.getContentMetadata()));
567    }
568 
569    /**
570     * {@inheritDoc}
571     */
572    @Override
573    public ListenableFuture<Boolean> blobExists(final String containerName, final String key) {
574       if (!getContainerToBlobs().containsKey(containerName))
575          return immediateFailedFuture(cnfe(containerName));
576       Map<String, Blob> realContents = getContainerToBlobs().get(containerName);
577       return immediateFuture(realContents.containsKey(key));
578    }
579 
580    /**
581     * {@inheritDoc}
582     */
583    @Override
584    public ListenableFuture<Blob> getBlob(final String containerName, final String key, GetOptions options) {
585       if (!getContainerToBlobs().containsKey(containerName))
586          return immediateFailedFuture(cnfe(containerName));
587       Map<String, Blob> realContents = getContainerToBlobs().get(containerName);
588       if (!realContents.containsKey(key))
589          return immediateFuture(null);
590 
591       Blob object = realContents.get(key);
592 
593       if (options.getIfMatch() != null) {
594          if (!object.getMetadata().getETag().equals(options.getIfMatch()))
595             return immediateFailedFuture(returnResponseException(412));
596       }
597       if (options.getIfNoneMatch() != null) {
598          if (object.getMetadata().getETag().equals(options.getIfNoneMatch()))
599             return immediateFailedFuture(returnResponseException(304));
600       }
601       if (options.getIfModifiedSince() != null) {
602          Date modifiedSince = options.getIfModifiedSince();
603          if (object.getMetadata().getLastModified().before(modifiedSince)) {
604             HttpResponse response = new HttpResponse(304, null, null);
605             return immediateFailedFuture(new HttpResponseException(String.format("%1$s is before %2$s", object
606                      .getMetadata().getLastModified(), modifiedSince), null, response));
607          }
608 
609       }
610       if (options.getIfUnmodifiedSince() != null) {
611          Date unmodifiedSince = options.getIfUnmodifiedSince();
612          if (object.getMetadata().getLastModified().after(unmodifiedSince)) {
613             HttpResponse response = new HttpResponse(412, null, null);
614             return immediateFailedFuture(new HttpResponseException(String.format("%1$s is after %2$s", object
615                      .getMetadata().getLastModified(), unmodifiedSince), null, response));
616          }
617       }
618       Blob returnVal = copyBlob(object);
619 
620       if (options.getRanges() != null && options.getRanges().size() > 0) {
621          byte[] data;
622          try {
623             data = toByteArray(returnVal.getPayload().getInput());
624          } catch (IOException e) {
625             return immediateFailedFuture(new RuntimeException(e));
626          }
627          ByteArrayOutputStream out = new ByteArrayOutputStream();
628          for (String s : options.getRanges()) {
629             if (s.startsWith("-")) {
630                int length = Integer.parseInt(s.substring(1));
631                out.write(data, data.length - length, length);
632             } else if (s.endsWith("-")) {
633                int offset = Integer.parseInt(s.substring(0, s.length() - 1));
634                out.write(data, offset, data.length - offset);
635             } else if (s.contains("-")) {
636                String[] firstLast = s.split("\\-");
637                int offset = Integer.parseInt(firstLast[0]);
638                int last = Integer.parseInt(firstLast[1]);
639                int length = (last < data.length) ? last + 1 : data.length - offset;
640                out.write(data, offset, length);
641             } else {
642                return immediateFailedFuture(new IllegalArgumentException("first and last were null!"));
643             }
644 
645          }
646          ContentMetadata cmd = returnVal.getPayload().getContentMetadata();
647          returnVal.setPayload(out.toByteArray());
648          HttpUtils.copy(cmd, returnVal.getPayload().getContentMetadata());
649          returnVal.getPayload().getContentMetadata().setContentLength(new Long(out.toByteArray().length));
650       }
651       checkNotNull(returnVal.getPayload(), "payload " + returnVal);
652       return immediateFuture(returnVal);
653    }
654 
655    /**
656     * {@inheritDoc}
657     */
658    @Override
659    public ListenableFuture<BlobMetadata> blobMetadata(final String container, final String key) {
660       try {
661          Blob blob = getBlob(container, key).get();
662          return immediateFuture(blob != null ? (BlobMetadata) copy(blob.getMetadata()) : null);
663       } catch (Exception e) {
664          if (size(filter(getCausalChain(e), KeyNotFoundException.class)) >= 1)
665             return immediateFuture(null);
666          return immediateFailedFuture(e);
667       }
668    }
669 
670    private Blob copyBlob(Blob blob) {
671       Blob returnVal = blobFactory.create(copy(blob.getMetadata()));
672       returnVal.setPayload(blob.getPayload());
673       copyPayloadHeadersToBlob(blob.getPayload(), returnVal);
674       return returnVal;
675    }
676 
677    public ConcurrentMap<String, ConcurrentMap<String, Blob>> getContainerToBlobs() {
678       return containerToBlobs;
679    }
680 
681    @Override
682    protected boolean deleteAndVerifyContainerGone(String container) {
683       getContainerToBlobs().remove(container);
684       return getContainerToBlobs().containsKey(container);
685    }
686 
687    public ConcurrentMap<String, Location> getContainerToLocation() {
688       return containerToLocation;
689    }
690 
691    @Override
692    public ListenableFuture<String> putBlob(String container, Blob blob, PutOptions options) {
693       // TODO implement options
694       return putBlob(container, blob);
695    }
696 
697    @Override
698    public ListenableFuture<Boolean> createContainerInLocation(Location location, String container,
699             CreateContainerOptions options) {
700       if (options.isPublicRead())
701          throw new UnsupportedOperationException("publicRead");
702       return createContainerInLocation(location, container);
703    }
704 
705 }