View Javadoc

1   /**
2    * Copyright 2004-2012 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.maven.mojo.s3;
17  
18  import java.io.ByteArrayInputStream;
19  import java.io.IOException;
20  import java.io.InputStream;
21  import java.text.SimpleDateFormat;
22  import java.util.ArrayList;
23  import java.util.Collection;
24  import java.util.Collections;
25  import java.util.Date;
26  import java.util.List;
27  import java.util.Map;
28  import java.util.TimeZone;
29  
30  import org.apache.commons.beanutils.BeanUtils;
31  import org.apache.commons.io.IOUtils;
32  import org.apache.commons.lang.StringUtils;
33  import org.apache.maven.plugin.MojoExecutionException;
34  import org.apache.maven.plugin.MojoFailureException;
35  import org.apache.maven.plugin.descriptor.PluginDescriptor;
36  import org.apache.maven.project.MavenProject;
37  import org.kuali.common.threads.ElementHandler;
38  import org.kuali.common.threads.ExecutionStatistics;
39  import org.kuali.common.threads.ThreadInvoker;
40  import org.kuali.maven.common.UrlBuilder;
41  
42  import com.amazonaws.AmazonServiceException;
43  import com.amazonaws.auth.AWSCredentials;
44  import com.amazonaws.services.s3.AmazonS3Client;
45  import com.amazonaws.services.s3.model.CannedAccessControlList;
46  import com.amazonaws.services.s3.model.CopyObjectRequest;
47  import com.amazonaws.services.s3.model.ListObjectsRequest;
48  import com.amazonaws.services.s3.model.ObjectListing;
49  import com.amazonaws.services.s3.model.ObjectMetadata;
50  import com.amazonaws.services.s3.model.PutObjectRequest;
51  import com.amazonaws.services.s3.model.S3Object;
52  import com.amazonaws.services.s3.model.S3ObjectSummary;
53  
54  /**
55   * <p>
56   * This mojo updates a bucket serving as an origin for a Cloud Front distribution. It generates an html directory
57   * listing for each "directory" in the bucket and stores the html under a key in the bucket such that a regular http
58   * request for a directory returns html. What would happen otherwise is XML for "object does not exist" would be
59   * returned by Amazon.
60   * </p>
61   * <p>
62   * For example: The url "http://www.mybucket.com/foo/bar" will return an html page containing a listing of all the files
63   * and directories under "foo/bar" in the bucket.
64   * </p>
65   * <p>
66   * If a directory contains an object with a key that is the same as the default object, the plugin copies the object to
67   * a key representing the directory structure.
68   * </p>
69   *
70   * <p>
71   * For example, the url "http://www.mybucket.com/foo/bar/index.html" represents an object in an S3 bucket under the key
72   * "foo/bar/index.html". This plugin will copy the object from the key "foo/bar/index.html" to the key "foo/bar/". This
73   * causes the url "http://www.mybucket.com/foo/bar/" to return the same content as the url
74   * "http://www.mybucket.com/foo/bar/index.html"
75   * </p>
76   * <p>
77   * It also generates an html directory listing at the root of the bucket hierarchy and places that html into the bucket
78   * as the default object, unless a default object already exists.
79   * </p>
80   *
81   * @goal updateoriginbucket
82   */
83  public class UpdateOriginBucketMojo extends S3Mojo {
84      UrlBuilder builder = new UrlBuilder();
85      SimpleFormatter formatter = new SimpleFormatter();
86      ThreadInvoker invoker = new ThreadInvoker();
87  
88      private static final String S3_INDEX_METADATA_KEY = "maven-cloudfront-plugin-index";
89      private static final String S3_INDEX_CONTENT_TYPE = "text/html";
90      CloudFrontHtmlGenerator generator;
91      S3DataConverter converter;
92  
93      /**
94       * GAV strings representing organizational poms eg "org.kuali.pom:kuali" or "org.kuali.pom:kuali-common". Version is
95       * ignored, only groupId and artifactId are relevant.
96       *
97       * @parameter
98       */
99      private List<String> orgPomGavs;
100 
101     /**
102      * The max number of threads to use when making calls to S3
103      *
104      * @parameter expression="${cloudfront.threads}" default-value="20"
105      */
106     private int threads;
107 
108     /**
109      * This parameter should represent the portion of the groupId that is implied by the hostname where content is
110      * published.
111      *
112      * For example, the Kuali Rice project has the groupId "org.kuali.rice" and content for the Kuali Rice project is
113      * published under "site.kuali.org/rice". The "org.kuali" portion of the groupId is implied from the hostname
114      * "site.kuali.org" and is thus removed when calculating the url in order to keep things a little more compact.
115      *
116      * If this parameter is not supplied, the complete groupId is used.
117      *
118      * @parameter expression="${cloudfront.organizationGroupId}"
119      */
120     private String organizationGroupId;
121 
122     /**
123      * This controls the caching behavior for CloudFront. By default, CloudFront edge locations cache content from an S3
124      * bucket for 24 hours. That interval is shortened to 1 hour for the html indexes generated by this plugin.
125      *
126      * @parameter expression="${cloudfront.cacheControl}" default-value="max-age=3600, must-revalidate"
127      */
128     private String cacheControl;
129 
130     /**
131      * If true, "foo/bar/index.html" will get copied to "foo/bar/"
132      *
133      * @parameter expression="${cloudfront.copyDefaultObjectWithDelimiter}" default-value="true"
134      */
135     private boolean copyDefaultObjectWithDelimiter;
136 
137     /**
138      * If true, "foo/bar/index.html" will get copied to "foo/bar". This is defaulted to false because the relative
139      * pathing in the html generated by the maven-site-plugin does not render correctly from a url without the trailing
140      * slash.
141      *
142      * @parameter expression="${cloudfront.copyDefaultObjectWithoutDelimiter}" default-value="false"
143      */
144     private boolean copyDefaultObjectWithoutDelimiter;
145 
146     /**
147      * The stylesheet to use for the directory listing
148      *
149      * @parameter expression="${cloudfront.css}" default-value="http://s3browse.ks.kuali.org/css/style.css"
150      */
151     private String css;
152 
153     /**
154      * Image representing a file
155      *
156      * @parameter expression="${cloudfront.fileImage}"
157      *            default-value="http://s3browse.ks.kuali.org/images/page_white.png"
158      */
159     private String fileImage;
160 
161     /**
162      * Image representing a directory
163      *
164      * @parameter expression="${cloudfront.directoryImage}"
165      *            default-value="http://s3browse.ks.kuali.org/images/folder.png"
166      */
167     private String directoryImage;
168 
169     /**
170      * When displaying the last modified timestamp, use this timezone
171      *
172      * @parameter expression="${cloudfront.timezone}" default-value="UTC"
173      */
174     private String timezone;
175 
176     /**
177      * When displaying the last modified timestamp use this format
178      *
179      * @parameter expression="${cloudfront.dateFormat}" default-value="EEE, dd MMM yyyy HH:mm:ss z"
180      */
181     private String dateFormat;
182 
183     /**
184      * The key containing the default object for the Cloud Front distribution. If this object already exists, the plugin
185      * will not modify it. If it does not exist, this plugin will generate an html directory listing and place it into
186      * the bucket under this key.
187      *
188      * @parameter expression="${cloudfront.defaultObjectKey}" default-value="index.html"
189      */
190     private String defaultObjectKey;
191 
192     /**
193      * The html for browsing a directory will be created under this key
194      *
195      * @parameter expression="${cloudfront.browseKey}" default-value="browse.html"
196      */
197     private String browseKey;
198 
199     /**
200      * The default permissions for S3 objects created by this plugin
201      *
202      * @parameter expression="${cloudfront.acl}" default-value="PublicRead"
203      * @required
204      */
205     private CannedAccessControlList acl;
206 
207     /**
208      * Return a listing of all the prefixes in this directory plus all the prefixes leading back to (and including) the
209      * root directory
210      *
211      * @param listing
212      * @param prefix
213      * @param delimiter
214      * @return
215      */
216     protected List<String> getPrefixes(ObjectListing listing, String prefix, String delimiter) {
217         List<String> commonPrefixes = listing.getCommonPrefixes();
218         List<String> pathPrefixes = getPathsToRoot(delimiter, prefix);
219         List<String> prefixes = new ArrayList<String>();
220         prefixes.addAll(commonPrefixes);
221         prefixes.addAll(pathPrefixes);
222         Collections.sort(prefixes);
223         return prefixes;
224     }
225 
226     /**
227      * Get an object listing for the current directory
228      *
229      * @param context
230      * @param request
231      * @return
232      */
233     protected ObjectListing getObjectListing(S3BucketContext context, ListObjectsRequest request) {
234         String prefix = getPrefix();
235         String bucket = context.getBucket();
236         String delimiter = context.getDelimiter();
237         Integer maxKeys = context.getMaxKeys();
238 
239         // This is the file system equivalent of typing "ls" in a directory
240         // No objects are retrieved, just metadata about the objects
241         long start = System.currentTimeMillis();
242         ObjectListing listing = context.getClient().listObjects(request);
243         long millis = System.currentTimeMillis() - start;
244         getLog().info(getListMsg(listing.getCommonPrefixes().size(), millis));
245 
246         // If we have more than 1000 objects in the current directory we have an issue
247         if (listing.isTruncated()) {
248             throw new AmazonServiceException("The listing for " + bucket + delimiter + prefix + " exceeded " + maxKeys);
249         }
250         return listing;
251     }
252 
253     /**
254      * Convert prefixes intl ListObjectContext objects
255      *
256      * @param bucketContext
257      * @param prefixes
258      * @return
259      */
260     protected List<ListObjectsContext> getListObjectsContexts(S3BucketContext bucketContext, List<String> prefixes) {
261         List<ListObjectsContext> contexts = new ArrayList<ListObjectsContext>();
262         for (String prefix : prefixes) {
263             ListObjectsRequest request = getListObjectsRequest(bucketContext, prefix);
264             ListObjectsContext context = new ListObjectsContext();
265             context.setRequest(request);
266             context.setBucketContext(bucketContext);
267             contexts.add(context);
268         }
269         return contexts;
270     }
271 
272     /**
273      * Given a prefix and a bucket context return a ListObjectsRequest object
274      *
275      * @param context
276      * @param prefix
277      * @return
278      */
279     protected ListObjectsRequest getListObjectsRequest(S3BucketContext context, String prefix) {
280         String bucket = context.getBucket();
281         String delimiter = context.getDelimiter();
282         Integer maxKeys = context.getMaxKeys();
283         if (prefix.equals(delimiter)) {
284             prefix = null;
285         }
286         ListObjectsRequest request = new ListObjectsRequest(bucket, prefix, null, delimiter, maxKeys);
287         return request;
288     }
289 
290     /**
291      * Show some information about the current directory
292      *
293      * @param subDirectoryCount
294      * @param millis
295      * @return
296      */
297     protected String getListMsg(int subDirectoryCount, long millis) {
298         StringBuilder sb = new StringBuilder();
299         sb.append("S3 Directories: " + subDirectoryCount);
300         sb.append("  Listing Request: " + formatter.getTime(millis));
301         return sb.toString();
302     }
303 
304     @Override
305     public void executeMojo() throws MojoExecutionException, MojoFailureException {
306         try {
307             // Track what time we started
308             long start = System.currentTimeMillis();
309             getLog().info("Updating S3 bucket - " + getBucket());
310 
311             // Update properties on the mojo
312             updateMojoState();
313 
314             // Save some context info about the bucket we are updating
315             S3BucketContext context = getS3BucketContext();
316 
317             // Utilities for generating html
318             generator = new CloudFrontHtmlGenerator(context);
319             converter = new S3DataConverter(context);
320             converter.setBrowseKey(getBrowseKey());
321 
322             // Show what we are up to
323             String prefix = getPrefix();
324             getLog().info("Re-indexing content for - " + prefix);
325 
326             // Get the object listing for this projects directory
327             ListObjectsRequest request = getListObjectsRequest(context, prefix);
328 
329             // This object listing is a reflection of the projects directory out on S3
330             ObjectListing projectDirectory = getObjectListing(context, request);
331 
332             // Get a list of prefixes representing other directories in this directory and the directories
333             // leading back to (and including) the root directory
334             List<String> prefixes = getPrefixes(projectDirectory, prefix, context.getDelimiter());
335 
336             // Make a multi-threaded call out to S3 to obtain object summaries for all of the prefixes
337             List<ObjectListing> listings = getObjectListings(context, prefixes, threads);
338 
339             // Add the ObjectListing for the projects directory
340             listings.add(projectDirectory);
341 
342             // Convert ObjectListings into S3PrefixContext objects (this generates the html)
343             List<S3PrefixContext> contexts = getS3PrefixContexts(context, listings);
344 
345             // Convert S3PrefixContext objects into UpdateDirectoryContext objects
346             List<UpdateDirectoryContext> udcs = getUpdateDirContexts(contexts);
347 
348             // Create a thread safe handler for dealing with elements in the list
349             ElementHandler<UpdateDirectoryContext> handler = new UpdateDirectoryContextHandler(this);
350 
351             // Make a multi-threaded call out to S3. This creates and/or updates html indexes
352             ExecutionStatistics stats = invoker.invokeThreads(threads, handler, udcs);
353 
354             // Print some diagnostics
355             long millis = stats.getExecutionTime();
356             long count = stats.getIterationCount();
357             getLog().info("Updated " + count + " bucket keys.  Time: " + formatter.getTime(millis));
358             getLog().info("S3 Bucket update complete - " + getBucket());
359             getLog().info("Total time: " + formatter.getTime(System.currentTimeMillis() - start));
360         } catch (Exception e) {
361             throw new MojoExecutionException("Unexpected error: ", e);
362         }
363     }
364 
365     /**
366      * Make a multi-threaded call to S3 to acquire ObjectListing objects for each of the prefixes
367      *
368      * @param context
369      * @param prefixes
370      * @param maxThreads
371      * @return
372      */
373     protected List<ObjectListing> getObjectListings(S3BucketContext context, List<String> prefixes, int maxThreads) {
374         List<ListObjectsContext> contexts = getListObjectsContexts(context, prefixes);
375         ListObjectsContextHandler elementHandler = new ListObjectsContextHandler();
376         ExecutionStatistics stats = invoker.invokeThreads(maxThreads, elementHandler, contexts);
377         long millis = stats.getExecutionTime();
378         long count = stats.getIterationCount();
379         getLog().info("Acquired listings for " + count + " prefixes.  Time: " + formatter.getTime(millis));
380         return elementHandler.getObjectListings();
381     }
382 
383     /**
384      * Convert S3PrefixContext objects into UpdateDirectoryContext objects. Each S3PrefixContext object generates two S3
385      * calls. One for the prefix with the delimiter and one for the prefix without the delimiter.
386      *
387      * @param contexts
388      * @return
389      */
390     protected List<UpdateDirectoryContext> getUpdateDirContexts(List<S3PrefixContext> contexts) {
391         List<UpdateDirectoryContext> list = new ArrayList<UpdateDirectoryContext>();
392         for (S3PrefixContext context : contexts) {
393 
394             // Root object requires special handling
395             if (context.isRoot()) {
396                 UpdateDirectoryContext udc = new UpdateDirectoryContext();
397                 udc.setContext(context);
398                 list.add(udc);
399                 continue;
400             }
401 
402             // Create context info for prefixes with and without the delimiter
403             String delimiter = context.getBucketContext().getDelimiter();
404             String trimmedPrefix = converter.getTrimmedPrefix(context.getPrefix(), delimiter);
405 
406             UpdateDirectoryContext udc1 = new UpdateDirectoryContext();
407             udc1.setContext(context);
408             udc1.setCopyIfExists(isCopyDefaultObjectWithDelimiter());
409             udc1.setCopyToKey(context.getPrefix());
410 
411             UpdateDirectoryContext udc2 = new UpdateDirectoryContext();
412             udc2.setContext(context);
413             udc2.setCopyIfExists(isCopyDefaultObjectWithoutDelimiter());
414             udc2.setCopyToKey(trimmedPrefix);
415 
416             list.add(udc1);
417             list.add(udc2);
418 
419         }
420         return list;
421     }
422 
423     /**
424      * Split the string up and return a list of Strings representing the path from the starting prefix back to (and
425      * including) the root of the bucket.
426      *
427      * @param delimiter
428      * @param startingPrefix
429      * @return
430      */
431     protected List<String> getPathsToRoot(String delimiter, String startingPrefix) {
432         List<String> list = new ArrayList<String>();
433 
434         list.add(delimiter);
435 
436         String[] prefixes = StringUtils.splitByWholeSeparator(startingPrefix, delimiter);
437         String newPrefix = "";
438         for (int i = 0; i < prefixes.length - 2; i++) {
439             newPrefix += prefixes[i] + delimiter;
440             list.add(newPrefix);
441         }
442         return list;
443     }
444 
445     /**
446      * Get a default prefix into the bucket from the project + groupId
447      *
448      * @param project
449      * @param groupId
450      * @return
451      */
452     protected String getDefaultPrefix(MavenProject project, String orgGroupId) {
453         List<MavenProject> orgPoms = builder.getMavenProjects(orgPomGavs);
454         return builder.getSitePath(project, orgPoms, orgGroupId);
455     }
456 
457     /**
458      * If they supplied a prefix use it. Otherwise generate one from the project metadata
459      */
460     protected void updatePrefix() {
461         String s = getPrefix();
462         if (StringUtils.isEmpty(s)) {
463             s = getDefaultPrefix(getProject(), getOrganizationGroupId());
464         }
465         if (!s.endsWith(getDelimiter())) {
466             s = s + getDelimiter();
467         }
468         setPrefix(s);
469     }
470 
471     /**
472      * Update the state of this mojo from the project metadata
473      */
474     protected void updateMojoState() throws MojoExecutionException {
475         updateCredentials();
476         validateCredentials();
477         updatePrefix();
478     }
479 
480     /**
481      * Get context information about the bucket we are operating on
482      */
483     protected S3BucketContext getS3BucketContext() throws MojoExecutionException {
484         AWSCredentials credentials = getCredentials();
485         AmazonS3Client client = new AmazonS3Client(credentials);
486         S3BucketContext context = new S3BucketContext();
487         try {
488             BeanUtils.copyProperties(context, this);
489         } catch (Exception e) {
490             throw new MojoExecutionException("Error copying properties", e);
491         }
492         context.setClient(client);
493         context.setLastModifiedDateFormatter(getLastModifiedDateFormatter());
494         context.setAbout(getAbout());
495         return context;
496     }
497 
498     /**
499      * Create a PutObjectRequest for some html generated by this mojo. The PutObjectRequest sets the content type to
500      * S3_INDEX_CONTENT_TYPE, sets the ACL to PublicRead, and adds some custom metadata so we can positively identify it
501      * as an object created by this plugin
502      */
503     protected PutObjectRequest getPutIndexObjectRequest(String html, String key) {
504         InputStream in = new ByteArrayInputStream(html.getBytes());
505         ObjectMetadata om = new ObjectMetadata();
506         om.setCacheControl(getCacheControl());
507         String contentType = S3_INDEX_CONTENT_TYPE;
508         om.setContentType(contentType);
509         om.setContentLength(html.length());
510         om.addUserMetadata(S3_INDEX_METADATA_KEY, "true");
511         PutObjectRequest request = new PutObjectRequest(getBucket(), key, in, om);
512         request.setCannedAcl(getAcl());
513         return request;
514     }
515 
516     /**
517      * Return a SimpleDateFormat object initialized with the date format and timezone supplied to the mojo
518      */
519     protected SimpleDateFormat getLastModifiedDateFormatter() {
520         SimpleDateFormat sdf = new SimpleDateFormat(getDateFormat());
521         sdf.setTimeZone(TimeZone.getTimeZone(getTimezone()));
522         return sdf;
523     }
524 
525     /**
526      * Return true if the Collection is null or contains no entries, false otherwise
527      */
528     protected boolean isEmpty(Collection<?> c) {
529         return c == null || c.size() == 0;
530     }
531 
532     /**
533      * Show some text about this plugin
534      */
535     protected String getAbout() {
536         String date = getLastModifiedDateFormatter().format(new Date());
537         PluginDescriptor descriptor = (PluginDescriptor) this.getPluginContext().get("pluginDescriptor");
538         if (descriptor == null) {
539             // Maven 2.2.1 is returning a null descriptor
540             return "Listing generated by the maven-cloudfront-plugin on " + date;
541         } else {
542             String name = descriptor.getArtifactId();
543             String version = descriptor.getVersion();
544             return "Listing generated by the " + name + " v" + version + " on " + date;
545         }
546     }
547 
548     public void updateDirectory(UpdateDirectoryContext context) throws IOException {
549         updateDirectory(context.getContext(), context.isCopyIfExists(), context.getCopyToKey());
550     }
551 
552     /**
553      * Create an object in the bucket under a key that lets a normal http request function correctly with CloudFront /
554      * S3.<br>
555      * Either use the client's object or upload some html created by this plugin<br>
556      */
557     protected void updateDirectory(S3PrefixContext context, boolean isCopyIfExists, String copyToKey)
558             throws IOException {
559         S3BucketContext bucketContext = context.getBucketContext();
560         AmazonS3Client client = context.getBucketContext().getClient();
561         String bucket = bucketContext.getBucket();
562 
563         boolean containsDefaultObject = isExistingObject(context.getObjectListing(), context.getDefaultObjectKey());
564         if (containsDefaultObject && isCopyIfExists) {
565             // Copy the contents of the clients default object
566             String sourceKey = context.getDefaultObjectKey();
567             String destKey = copyToKey;
568             CopyObjectRequest request = getCopyObjectRequest(bucket, sourceKey, destKey);
569             getLog().debug("Copy: " + sourceKey + " to " + destKey);
570             client.copyObject(request);
571         } else {
572             // Upload our custom content
573             PutObjectRequest request = getPutIndexObjectRequest(context.getHtml(), copyToKey);
574             getLog().debug("Put: " + copyToKey);
575             client.putObject(request);
576         }
577     }
578 
579     /**
580      * Update this S3 "directory".
581      */
582     protected void updateDirectory(final S3PrefixContext context) throws IOException {
583         String trimmedPrefix = converter.getTrimmedPrefix(context.getPrefix(), context.getBucketContext()
584                 .getDelimiter());
585 
586         // Handle "http://www.mybucket.com/foo/bar/"
587         updateDirectory(context, isCopyDefaultObjectWithDelimiter(), context.getPrefix());
588 
589         // Handle "http://www.mybucket.com/foo/bar"
590         updateDirectory(context, isCopyDefaultObjectWithoutDelimiter(), trimmedPrefix);
591 
592         // Handle "http://www.mybucket.com/foo/bar/browse.html"
593         // context.getBucketContext().getClient().putObject(getPutIndexObjectRequest(context.getHtml(),
594         // context.getBrowseHtmlKey()));
595     }
596 
597     /**
598      * If this is the root of the bucket and the default object either does not exist or was created by this plugin,
599      * overwrite the default object with newly generated html. Otherwise, do nothing.
600      */
601     public void updateRoot(UpdateDirectoryContext udc) throws IOException {
602         updateRoot(udc.getContext());
603     }
604 
605     /**
606      * If this is the root of the bucket and the default object either does not exist or was created by this plugin,
607      * overwrite the default object with newly generated html. Otherwise, do nothing.
608      */
609     protected void updateRoot(S3PrefixContext context) throws IOException {
610         AmazonS3Client client = context.getBucketContext().getClient();
611 
612         // Handle "http://www.mybucket.com/browse.html"
613         PutObjectRequest request1 = getPutIndexObjectRequest(context.getHtml(), context.getBrowseHtmlKey());
614         StringBuilder sb = new StringBuilder();
615         sb.append(context.getBrowseHtmlKey());
616         client.putObject(request1);
617 
618         boolean isCreateOrUpdateDefaultObject = isCreateOrUpdateDefaultObject(context);
619         if (!isCreateOrUpdateDefaultObject) {
620             getLog().debug("Put: " + sb.toString());
621             return;
622         }
623 
624         // Update the default object
625         PutObjectRequest request2 = getPutIndexObjectRequest(context.getHtml(), context.getDefaultObjectKey());
626         getLog().debug("Put: " + sb.toString() + ", " + context.getDefaultObjectKey());
627         client.putObject(request2);
628     }
629 
630     /**
631      * Convert ObjectListing objects into S3PrefixContext objects
632      */
633     protected List<S3PrefixContext> getS3PrefixContexts(S3BucketContext context, List<ObjectListing> listings) {
634         List<S3PrefixContext> contexts = new ArrayList<S3PrefixContext>();
635         for (ObjectListing listing : listings) {
636             S3PrefixContext prefixContext = getS3PrefixContext(context, listing);
637             contexts.add(prefixContext);
638         }
639         return contexts;
640     }
641 
642     /**
643      * Convert an ObjectListing into an S3PrefixContext
644      */
645     protected S3PrefixContext getS3PrefixContext(S3BucketContext context, ObjectListing objectListing) {
646         String prefix = objectListing.getPrefix();
647         String delimiter = context.getDelimiter();
648         List<String[]> data = converter.getData(objectListing, prefix, delimiter);
649         String html = generator.getHtml(data, prefix, delimiter);
650         String defaultObjectKey = StringUtils.isEmpty(prefix) ? getDefaultObjectKey() : prefix + getDefaultObjectKey();
651         String browseHtmlKey = StringUtils.isEmpty(prefix) ? getBrowseKey() : prefix + getBrowseKey();
652         // Is this the root of the bucket?
653         boolean isRoot = StringUtils.isEmpty(prefix);
654 
655         S3PrefixContext prefixContext = new S3PrefixContext();
656         prefixContext.setObjectListing(objectListing);
657         prefixContext.setHtml(html);
658         prefixContext.setRoot(isRoot);
659         prefixContext.setDefaultObjectKey(defaultObjectKey);
660         prefixContext.setPrefix(prefix);
661         prefixContext.setBucketContext(context);
662         prefixContext.setBrowseHtmlKey(browseHtmlKey);
663         return prefixContext;
664     }
665 
666     /**
667      * Return true if the ObjectListing contains an object under "key"
668      */
669     protected boolean isExistingObject(final ObjectListing objectListing, final String key) {
670         List<S3ObjectSummary> summaries = objectListing.getObjectSummaries();
671         for (S3ObjectSummary summary : summaries) {
672             if (key.equals(summary.getKey())) {
673                 return true;
674             }
675         }
676         return false;
677     }
678 
679     /**
680      * Return true if there is no object in the ObjectListing under defaultObjectKey.<br>
681      * Return true if the object in the ObjectListing was created by this plugin.<br>
682      * Return false otherwise.<br>
683      */
684     protected boolean isCreateOrUpdateDefaultObject(final S3PrefixContext context) {
685         if (!isExistingObject(context.getObjectListing(), context.getDefaultObjectKey())) {
686             // There is no default object, we are free to create one
687             return true;
688         }
689         S3BucketContext s3Context = context.getBucketContext();
690         // There is a default object, but if it was created by this plugin, we
691         // still need to update it
692         S3Object s3Object = s3Context.getClient().getObject(s3Context.getBucket(), context.getDefaultObjectKey());
693         boolean isOurDefaultObject = isOurObject(s3Object);
694         IOUtils.closeQuietly(s3Object.getObjectContent());
695         if (isOurDefaultObject) {
696             return true;
697         } else {
698             return false;
699         }
700     }
701 
702     /**
703      * Return true if this S3Object was created by this plugin. This is is done by checking the metadata attached to
704      * this object for the presence of a custom value.
705      */
706     protected boolean isOurObject(final S3Object s3Object) {
707         ObjectMetadata metadata = s3Object.getObjectMetadata();
708         Map<String, String> userMetadata = metadata.getUserMetadata();
709         String value = userMetadata.get(S3_INDEX_METADATA_KEY);
710         boolean isOurObject = "true".equals(value);
711         return isOurObject;
712     }
713 
714     /**
715      * Create a CopyObjectRequest with an ACL set to PublicRead
716      */
717     protected CopyObjectRequest getCopyObjectRequest(final String bucket, final String sourceKey, final String destKey) {
718         CopyObjectRequest request = new CopyObjectRequest(bucket, sourceKey, bucket, destKey);
719         request.setCannedAccessControlList(getAcl());
720         return request;
721     }
722 
723     public String getTimezone() {
724         return timezone;
725     }
726 
727     public void setTimezone(final String timezone) {
728         this.timezone = timezone;
729     }
730 
731     public String getDateFormat() {
732         return dateFormat;
733     }
734 
735     public void setDateFormat(final String dateFormat) {
736         this.dateFormat = dateFormat;
737     }
738 
739     public String getDefaultObjectKey() {
740         return defaultObjectKey;
741     }
742 
743     public void setDefaultObjectKey(final String defaultCloudFrontObject) {
744         this.defaultObjectKey = defaultCloudFrontObject;
745     }
746 
747     public String getFileImage() {
748         return fileImage;
749     }
750 
751     public void setFileImage(final String fileImage) {
752         this.fileImage = fileImage;
753     }
754 
755     public String getDirectoryImage() {
756         return directoryImage;
757     }
758 
759     public void setDirectoryImage(final String directoryImage) {
760         this.directoryImage = directoryImage;
761     }
762 
763     public String getCss() {
764         return css;
765     }
766 
767     public void setCss(final String css) {
768         this.css = css;
769     }
770 
771     public boolean isCopyDefaultObjectWithDelimiter() {
772         return copyDefaultObjectWithDelimiter;
773     }
774 
775     public void setCopyDefaultObjectWithDelimiter(final boolean copyDefaultObjectWithDelimiter) {
776         this.copyDefaultObjectWithDelimiter = copyDefaultObjectWithDelimiter;
777     }
778 
779     public boolean isCopyDefaultObjectWithoutDelimiter() {
780         return copyDefaultObjectWithoutDelimiter;
781     }
782 
783     public void setCopyDefaultObjectWithoutDelimiter(final boolean copyDefaultObjectWithoutDelimiter) {
784         this.copyDefaultObjectWithoutDelimiter = copyDefaultObjectWithoutDelimiter;
785     }
786 
787     public String getCacheControl() {
788         return cacheControl;
789     }
790 
791     public void setCacheControl(final String cacheControl) {
792         this.cacheControl = cacheControl;
793     }
794 
795     public String getBrowseKey() {
796         return browseKey;
797     }
798 
799     public void setBrowseKey(final String browseHtml) {
800         this.browseKey = browseHtml;
801     }
802 
803     /**
804      * @return the organizationGroupId
805      */
806     public String getOrganizationGroupId() {
807         return organizationGroupId;
808     }
809 
810     /**
811      * @param organizationGroupId
812      *            the organizationGroupId to set
813      */
814     public void setOrganizationGroupId(final String organizationGroupId) {
815         this.organizationGroupId = organizationGroupId;
816     }
817 
818     public int getThreads() {
819         return threads;
820     }
821 
822     public void setThreads(int threadCount) {
823         this.threads = threadCount;
824     }
825 
826     public CannedAccessControlList getAcl() {
827         return acl;
828     }
829 
830     public void setAcl(CannedAccessControlList acl) {
831         this.acl = acl;
832     }
833 
834     public List<String> getOrgPomGavs() {
835         return orgPomGavs;
836     }
837 
838     public void setOrgPomGavs(List<String> orgPomGavs) {
839         this.orgPomGavs = orgPomGavs;
840     }
841 }