View Javadoc
1   /**
2    * Copyright (c) 2012-2017, s3auth.com
3    * All rights reserved.
4    *
5    * Redistribution and use in source and binary forms, with or without
6    * modification, are permitted provided that the following conditions
7    * are met: 1) Redistributions of source code must retain the above
8    * copyright notice, this list of conditions and the following
9    * disclaimer. 2) Redistributions in binary form must reproduce the above
10   * copyright notice, this list of conditions and the following
11   * disclaimer in the documentation and/or other materials provided
12   * with the distribution. 3) Neither the name of the s3auth.com nor
13   * the names of its contributors may be used to endorse or promote
14   * products derived from this software without specific prior written
15   * permission.
16   *
17   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18   * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
19   * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
20   * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
21   * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
22   * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23   * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24   * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
25   * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
26   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
28   * OF THE POSSIBILITY OF SUCH DAMAGE.
29   */
30  package com.s3auth.hosts;
31  
32  import com.amazonaws.services.s3.AmazonS3;
33  import com.amazonaws.services.s3.model.GetObjectRequest;
34  import com.amazonaws.services.s3.model.ObjectMetadata;
35  import com.amazonaws.services.s3.model.S3Object;
36  import com.jcabi.aspects.Loggable;
37  import java.io.IOException;
38  import java.io.InputStream;
39  import java.io.OutputStream;
40  import java.net.HttpURLConnection;
41  import java.util.Collection;
42  import java.util.Date;
43  import java.util.LinkedList;
44  import javax.validation.constraints.NotNull;
45  import javax.ws.rs.core.HttpHeaders;
46  import lombok.EqualsAndHashCode;
47  import org.apache.commons.io.IOUtils;
48  import org.apache.commons.lang3.StringUtils;
49  
50  /**
51   * Default implementation of {@link Resource}.
52   *
53   * <p>The class is mutable and thread-safe.
54   *
55   * @author Yegor Bugayenko (yegor@tpc2.com)
56   * @version $Id: 19fb9b24a1194e932e4b9b219929e2a96707cffc $
57   * @since 0.0.1
58   */
59  @EqualsAndHashCode(of = { "bucket", "key", "range" })
60  @Loggable(Loggable.DEBUG)
61  @SuppressWarnings("PMD.TooManyMethods")
62  final class DefaultResource implements Resource {
63      /**
64       * Amazon S3 client.
65       */
66      private final transient AmazonS3 client;
67  
68      /**
69       * Bucket name.
70       */
71      private final transient String bucket;
72  
73      /**
74       * Key in the bucket.
75       */
76      private final transient String key;
77  
78      /**
79       * The range.
80       */
81      private final transient Range range;
82  
83      /**
84       * The version.
85       */
86      private final transient Version version;
87  
88      /**
89       * The object retrieved on construction.
90       */
91      private final transient S3Object object;
92  
93      /**
94       * Domain Stats.
95       */
96      private final transient DomainStatsData stats;
97  
98      /**
99       * Public ctor.
100      * @param clnt Amazon S3 client
101      * @param bckt Bucket name
102      * @param name Key name
103      * @param rng Range to deliver
104      * @param ver Version of object to retrieve
105      * @param dstats Domain stats data
106      * @checkstyle ParameterNumber (5 lines)
107      */
108     DefaultResource(@NotNull final AmazonS3 clnt,
109         @NotNull final String bckt, @NotNull final String name,
110         @NotNull final Range rng, @NotNull final Version ver,
111         @NotNull final DomainStatsData dstats) {
112         this.client = clnt;
113         this.bucket = bckt;
114         this.key = name;
115         this.range = rng;
116         this.version = ver;
117         this.object = this.client.getObject(
118             this.request(this.range, this.version)
119         );
120         this.stats = dstats;
121     }
122 
123     @Override
124     public String toString() {
125         return String.format("%s:%s", this.bucket, this.key);
126     }
127 
128     @Override
129     public int status() {
130         final int status;
131         if (this.range.equals(Range.ENTIRE)) {
132             status = HttpURLConnection.HTTP_OK;
133         } else {
134             status = HttpURLConnection.HTTP_PARTIAL;
135         }
136         return status;
137     }
138 
139     @Override
140     @Loggable(
141         value = Loggable.DEBUG, limit = Integer.MAX_VALUE,
142         ignore = DefaultResource.StreamingException.class
143     )
144     public long writeTo(@NotNull final OutputStream output) throws IOException {
145         final InputStream input = this.object.getObjectContent();
146         assert input != null;
147         int total = 0;
148         // @checkstyle MagicNumber (1 line)
149         final byte[] buffer = new byte[16 * 1024];
150         try {
151             while (true) {
152                 final int count;
153                 try {
154                     count = input.read(buffer);
155                 } catch (final IOException ex) {
156                     throw new DefaultResource.StreamingException(
157                         String.format(
158                             "failed to read %s/%s, range=%s, total=%d",
159                             this.bucket,
160                             this.key,
161                             this.range,
162                             total
163                         ),
164                         ex
165                     );
166                 }
167                 if (count == -1) {
168                     break;
169                 }
170                 try {
171                     output.write(buffer, 0, count);
172                 } catch (final IOException ex) {
173                     throw new DefaultResource.StreamingException(
174                         String.format(
175                             // @checkstyle LineLength (1 line)
176                             "failed to write %s/%s, range=%s, total=%d, count=%d",
177                             this.bucket,
178                             this.key,
179                             this.range,
180                             total,
181                             count
182                         ),
183                         ex
184                     );
185                 }
186                 total += count;
187             }
188             this.stats.put(this.bucket, new Stats.Simple(total));
189         } finally {
190             input.close();
191         }
192         return total;
193     }
194 
195     @Override
196     @NotNull
197     public Collection<String> headers() {
198         final ObjectMetadata meta = this.object.getObjectMetadata();
199         final Collection<String> headers = new LinkedList<String>();
200         headers.add(
201             DefaultResource.header(
202                 HttpHeaders.CONTENT_LENGTH,
203                 Long.toString(meta.getContentLength())
204             )
205         );
206         if (meta.getContentType() != null) {
207             headers.add(
208                 DefaultResource.header(
209                     HttpHeaders.CONTENT_TYPE,
210                     meta.getContentType()
211                 )
212             );
213         }
214         if (meta.getContentEncoding() != null) {
215             headers.add(
216                 DefaultResource.header(
217                     HttpHeaders.CONTENT_ENCODING,
218                     meta.getContentEncoding()
219                 )
220             );
221         }
222         if (meta.getETag() != null) {
223             headers.add(
224                 DefaultResource.header(
225                     HttpHeaders.ETAG,
226                     meta.getETag()
227                 )
228             );
229         }
230         headers.add(
231             DefaultResource.header(
232                 HttpHeaders.CACHE_CONTROL,
233                 StringUtils.defaultString(
234                     meta.getCacheControl(),
235                     "must-revalidate"
236                 )
237             )
238         );
239         headers.add(DefaultResource.header("Accept-Ranges", "bytes"));
240         if (!this.range.equals(Range.ENTIRE)) {
241             headers.add(
242                 DefaultResource.header(
243                     "Content-Range",
244                     String.format(
245                         "bytes %d-%d/%d",
246                         this.range.first(),
247                         this.range.last(),
248                         this.size()
249                     )
250                 )
251             );
252         }
253         return headers;
254     }
255 
256     @Override
257     @NotNull
258     public String etag() {
259         return this.object.getObjectMetadata().getETag();
260     }
261 
262     @Override
263     public Date lastModified() {
264         return new Date(
265             this.object.getObjectMetadata().getLastModified().getTime()
266         );
267     }
268 
269     @Override
270     public String contentType() {
271         return this.object.getObjectMetadata().getContentType();
272     }
273 
274     @Override
275     public void close() throws IOException {
276         this.object.close();
277     }
278 
279     /**
280      * Create a HTTP header from name and value.
281      * @param name Name of the header
282      * @param value The value
283      * @return Full HTTP header string
284      */
285     @NotNull
286     private static String header(@NotNull final String name,
287         @NotNull final String value) {
288         return String.format("%s: %s", name, value);
289     }
290 
291     /**
292      * Make S3 request with a specified range.
293      * @param rng Range to request
294      * @param ver Version of object to fetch
295      * @return Request
296      */
297     private GetObjectRequest request(final Range rng, final Version ver) {
298         final GetObjectRequest request =
299             new GetObjectRequest(this.bucket, this.key);
300         if (!rng.equals(Range.ENTIRE)) {
301             request.withRange(rng.first(), rng.last());
302         }
303         if (!ver.latest()) {
304             request.withVersionId(ver.version());
305         }
306         return request;
307     }
308 
309     /**
310      * Get total size of an S3 object.
311      * @return Size of it in bytes
312      */
313     private long size() {
314         final long size;
315         if (this.range.equals(Range.ENTIRE)) {
316             size = this.object.getObjectMetadata().getContentLength();
317         } else {
318             S3Object obj = null;
319             try {
320                 obj = this.client.getObject(
321                     this.request(Range.ENTIRE, this.version)
322                 );
323                 size = obj.getObjectMetadata().getContentLength();
324             } finally {
325                 IOUtils.closeQuietly(obj);
326             }
327         }
328         return size;
329     }
330 
331     /**
332      * Custom IO exception.
333      */
334     private static final class StreamingException extends IOException {
335         /**
336          * Serialization marker.
337          */
338         private static final long serialVersionUID = 0x7529FA781E111179L;
339         /**
340          * Public ctor.
341          * @param cause The cause of it
342          * @param thr The cause of it
343          */
344         StreamingException(final String cause, final Throwable thr) {
345             super(
346                 String.format("%s: '%s'", cause, thr.getMessage()),
347                 thr
348             );
349         }
350     }
351 
352 }