View Javadoc
1   /**
2    * Copyright 2004-2014 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.common.aws.s3.old;
17  
18  import java.util.ArrayList;
19  import java.util.List;
20  
21  import org.apache.commons.beanutils.BeanUtils;
22  import org.apache.commons.lang.StringUtils;
23  import org.kuali.common.util.Assert;
24  import org.kuali.common.util.CollectionUtils;
25  import org.kuali.common.util.Counter;
26  import org.slf4j.Logger;
27  import org.slf4j.LoggerFactory;
28  
29  import com.amazonaws.services.s3.AmazonS3Client;
30  import com.amazonaws.services.s3.model.ListObjectsRequest;
31  import com.amazonaws.services.s3.model.ObjectListing;
32  
33  public class DefaultBucketService implements BucketService {
34  
35  	private static final Logger logger = LoggerFactory.getLogger(DefaultBucketService.class);
36  
37  	@Override
38  	public ListingResult getObjectListings(ObjectListingsContext context) {
39  
40  		// Make sure we are configured correctly
41  		Assert.notNull(context, "context is null");
42  		Assert.notNull(context.getClient(), "client is null");
43  		Assert.notNull(context.getRequest(), "request is null");
44  		Assert.notNull(context.getBucketContext(), "bucket context is null");
45  
46  		// Extract the pojo's for convenience
47  		AmazonS3Client client = context.getClient();
48  		BucketContext bucketContext = context.getBucketContext();
49  		ListingRequest request = context.getRequest();
50  
51  		// Complete some more configuration checks
52  		Assert.hasText(bucketContext.getDelimiter(), "delimiter has no text");
53  		Assert.hasText(bucketContext.getName(), "name has no text");
54  		boolean exists = client.doesBucketExist(bucketContext.getName());
55  		Assert.isTrue(exists, "bucket [" + context.getBucketContext().getName() + "] does not exist");
56  
57  		// Start the informer, if they supplied one
58  		if (request.getInformer() != null) {
59  			logger.debug("starting informer");
60  			request.getInformer().start();
61  		}
62  
63  		// Preserve the start time
64  		long start = System.currentTimeMillis();
65  
66  		// Initialize a new counter
67  		Counter counter = new Counter();
68  
69  		// Connect to Amazon's S3 service and collect summary information about objects in the S3 bucket
70  		// This can be recursive and take a while
71  		List<ObjectListing> listings = accumulateObjectListings(context, context.getRequest(), start, counter);
72  
73  		// Preserve the stop time
74  		long stop = System.currentTimeMillis();
75  
76  		// Stop the informer, if they supplied one
77  		if (request.getInformer() != null) {
78  			request.getInformer().stop();
79  		}
80  
81  		// Aggregate information about this request into a result object
82  		ListingResult result = new ListingResult();
83  		result.setListings(listings);
84  		result.setStartTime(start);
85  		result.setStopTime(stop);
86  		result.setElapsed(stop - start);
87  		return result;
88  	}
89  
90  	/**
91  	 * Examine an S3 bucket (potentially recursively) for information about the "directories" and objects it contains.
92  	 */
93  	protected List<ObjectListing> accumulateObjectListings(ObjectListingsContext context, ListingRequest request, long startTime, Counter counter) {
94  
95  		// Make sure we haven't exceeded any of our limits
96  		validateState(request, startTime, counter);
97  
98  		// Append delimiter to prefix if needed
99  		String prefix = getPrefix(request.getPrefix(), context.getBucketContext().getDelimiter());
100 
101 		// Setup some storage for our Object listings
102 		List<ObjectListing> listings = new ArrayList<ObjectListing>();
103 
104 		// Connect to S3 and obtain an ObjectListing for this prefix
105 		ObjectListing listing = getObjectListing(context, prefix, counter);
106 
107 		// Add the current ObjectListing to the list
108 		listings.add(listing);
109 
110 		// Recurse into the "sub-directories"
111 		for (String subDirectory : listing.getCommonPrefixes()) {
112 			doSubDirectory(context, subDirectory, listings, startTime, counter);
113 		}
114 
115 		// Return the aggregated list of ObjectListings
116 		return listings;
117 	}
118 
119 	/**
120 	 * Make sure none of the configured limits have been exceeded.
121 	 */
122 	protected void validateState(ListingRequest request, long startTime, Counter counter) {
123 
124 		// Make sure we have not exceeded our overall timeout limit
125 		if (System.currentTimeMillis() > (startTime + request.getTimeoutMillis())) {
126 			throw new IllegalStateException("timeout exceeded");
127 		}
128 
129 		// Make sure we have not exceeded our overall object listing limit
130 		if (counter.getValue() > request.getMaxListings()) {
131 			throw new IllegalStateException("max listings exceeded");
132 		}
133 	}
134 
135 	protected ObjectListing getObjectListing(ObjectListingsContext context, String prefix, Counter counter) {
136 
137 		// Create an Amazon request
138 		ListObjectsRequest lor = getListObjectsRequest(context, prefix);
139 
140 		// Connect to S3 and extract the object listing
141 		ObjectListing listing = context.getClient().listObjects(lor);
142 
143 		// Make sure it isn't truncated (< 1000 objects total)
144 		Assert.isFalse(listing.isTruncated(), "listing is truncated");
145 
146 		// Increment progress on the informer, if they supplied one
147 		if (context.getRequest().getInformer() != null) {
148 			context.getRequest().getInformer().incrementProgress();
149 		}
150 
151 		// Increment the counter
152 		counter.increment();
153 
154 		// Return the listing
155 		return listing;
156 	}
157 
158 	protected void doSubDirectory(ObjectListingsContext context, String subDirectory, List<ObjectListing> listings, long startTime, Counter counter) {
159 
160 		// Determine if we are recursing into this "sub-directory"
161 		if (isRecurse(context, subDirectory)) {
162 
163 			// If so, clone the existing request, but with an new prefix
164 			ListingRequest clone = clone(context.getRequest(), subDirectory);
165 
166 			// Recurse in order to accumulate all ObjectListing's under this one
167 			List<ObjectListing> children = accumulateObjectListings(context, clone, startTime, counter);
168 
169 			// Add the aggregated child list to our overall list
170 			listings.addAll(children);
171 
172 		} else {
173 
174 			// We are not recursing into the "sub-directory" but we still list the contents of the "sub-directory" itself
175 			ObjectListing subDirectoryListing = getObjectListing(context, subDirectory, counter);
176 
177 			// Add the "sub-directory" listing to the overall list
178 			listings.add(subDirectoryListing);
179 		}
180 	}
181 
182 	/**
183 	 * If <code>prefix</code> does not end with <code>delimiter</code>, append it. If <code>prefix</code> is blank or <code>prefix==delimiter</code> return <code>null</code>
184 	 */
185 	protected String getPrefix(String prefix, String delimiter) {
186 		if (StringUtils.isBlank(prefix) || StringUtils.equals(prefix, delimiter)) {
187 			return null;
188 		} else {
189 			return StringUtils.endsWith(prefix, delimiter) ? prefix : prefix + delimiter;
190 		}
191 	}
192 
193 	/**
194 	 * Make sure <code>pattern</code> is bracketed by <code>delimiter</code>.
195 	 * 
196 	 * <pre>
197 	 *   apidocs  -> /apidocs/
198 	 *   apidocs/ -> /apidocs/
199 	 *  /apidocs  -> /apidocs/
200 	 *  /apidocs/ -> /apidocs/
201 	 * </pre>
202 	 */
203 	protected String getSuffixPattern(String pattern, String delimiter) {
204 
205 		Assert.hasText(pattern, "pattern has no text");
206 		Assert.hasText(delimiter, "delimiter has no text");
207 
208 		StringBuilder sb = new StringBuilder();
209 		if (!StringUtils.startsWith(pattern, delimiter)) {
210 			sb.append(delimiter);
211 		}
212 		sb.append(pattern);
213 		if (!StringUtils.endsWith(pattern, delimiter)) {
214 			sb.append(delimiter);
215 		}
216 		return sb.toString();
217 	}
218 
219 	protected boolean isEndsWithMatch(String prefix, String pattern, String delimiter) {
220 		String suffix = getSuffixPattern(pattern, delimiter);
221 		return StringUtils.endsWith(prefix, suffix);
222 	}
223 
224 	protected boolean isExclude(ObjectListingsContext context, String prefix) {
225 		for (String exclude : CollectionUtils.toEmptyList(context.getRequest().getExcludes())) {
226 			if (isEndsWithMatch(prefix, exclude, context.getBucketContext().getDelimiter())) {
227 				return true;
228 			}
229 		}
230 		return false;
231 	}
232 
233 	protected boolean isInclude(ObjectListingsContext context, String prefix) {
234 		if (CollectionUtils.isEmpty(context.getRequest().getIncludes())) {
235 			return true;
236 		}
237 		for (String include : context.getRequest().getIncludes()) {
238 			if (isEndsWithMatch(prefix, include, context.getBucketContext().getDelimiter())) {
239 				return true;
240 			}
241 		}
242 		return false;
243 	}
244 
245 	protected boolean isRecurse(ObjectListingsContext context, String prefix) {
246 		return context.getRequest().isRecursive() && !isExclude(context, prefix) && isInclude(context, prefix);
247 	}
248 
249 	protected ListingRequest clone(ListingRequest request, String prefix) {
250 		ListingRequest clone = new ListingRequest();
251 		try {
252 			BeanUtils.copyProperties(clone, request);
253 		} catch (Exception e) {
254 			throw new IllegalStateException(e);
255 		}
256 		clone.setPrefix(prefix);
257 		return clone;
258 	}
259 
260 	protected ListObjectsRequest getListObjectsRequest(ObjectListingsContext context, String prefix) {
261 		String name = context.getBucketContext().getName();
262 		String delimiter = context.getBucketContext().getDelimiter();
263 		Integer maxKeys = context.getBucketContext().getMaxKeys();
264 		return getListObjectsRequest(name, prefix, delimiter, maxKeys);
265 	}
266 
267 	protected ListObjectsRequest getListObjectsRequest(String bucket, String prefix, String delimiter, Integer maxKeys) {
268 		ListObjectsRequest request = new ListObjectsRequest();
269 		request.setBucketName(bucket);
270 		request.setDelimiter(delimiter);
271 		request.setPrefix(prefix);
272 		request.setMaxKeys(maxKeys);
273 		return request;
274 	}
275 
276 }