Coverage Report - org.kuali.maven.wagon.S3Wagon
 
Classes in this File Line Coverage Branch Coverage Complexity
S3Wagon
0%
0/161
0%
0/32
2.333
 
 1  
 /*
 2  
  * Copyright 2004-2007 the original author or authors. Licensed under the Apache License, Version 2.0 (the "License");
 3  
  * you may not use this file except in compliance with the License. You may obtain a copy of the License at
 4  
  * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software
 5  
  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 6  
  * either express or implied. See the License for the specific language governing permissions and limitations under the
 7  
  * License.
 8  
  */
 9  
 package org.kuali.maven.wagon;
 10  
 
 11  
 import java.io.File;
 12  
 import java.io.FileNotFoundException;
 13  
 import java.io.IOException;
 14  
 import java.io.InputStream;
 15  
 import java.io.OutputStream;
 16  
 import java.net.URI;
 17  
 import java.util.ArrayList;
 18  
 import java.util.Date;
 19  
 import java.util.List;
 20  
 
 21  
 import org.apache.commons.io.IOUtils;
 22  
 import org.apache.commons.lang.StringUtils;
 23  
 import org.apache.maven.wagon.ResourceDoesNotExistException;
 24  
 import org.apache.maven.wagon.TransferFailedException;
 25  
 import org.apache.maven.wagon.authentication.AuthenticationException;
 26  
 import org.apache.maven.wagon.authentication.AuthenticationInfo;
 27  
 import org.apache.maven.wagon.proxy.ProxyInfo;
 28  
 import org.apache.maven.wagon.repository.Repository;
 29  
 import org.slf4j.Logger;
 30  
 import org.slf4j.LoggerFactory;
 31  
 
 32  
 import com.amazonaws.AmazonClientException;
 33  
 import com.amazonaws.AmazonServiceException;
 34  
 import com.amazonaws.auth.AWSCredentials;
 35  
 import com.amazonaws.auth.BasicAWSCredentials;
 36  
 import com.amazonaws.services.s3.AmazonS3Client;
 37  
 import com.amazonaws.services.s3.internal.Mimetypes;
 38  
 import com.amazonaws.services.s3.model.Bucket;
 39  
 import com.amazonaws.services.s3.model.CannedAccessControlList;
 40  
 import com.amazonaws.services.s3.model.ObjectListing;
 41  
 import com.amazonaws.services.s3.model.ObjectMetadata;
 42  
 import com.amazonaws.services.s3.model.PutObjectRequest;
 43  
 import com.amazonaws.services.s3.model.S3Object;
 44  
 import com.amazonaws.services.s3.model.S3ObjectSummary;
 45  
 
 46  
 /**
 47  
  * An implementation of the Maven Wagon interface that is integrated with the Amazon S3 service. URLs that reference the
 48  
  * S3 service should be in the form of <code>s3://bucket.name</code>. As an example <code>s3://maven.kuali.org</code>
 49  
  * puts files into the <code>maven.kuali.org</code> bucket on the S3 service.
 50  
  * <p/>
 51  
  * This implementation uses the <code>username</code> and <code>password</code> portions of the server authentication
 52  
  * metadata for credentials. <code>
 53  
  *
 54  
  * pom.xml
 55  
  * <snapshotRepository>
 56  
  *   <id>kuali.snapshot</id>
 57  
  *   <name>Kuali Snapshot Repository</name>
 58  
  *   <url>s3://maven.kuali.org/snapshot</url>
 59  
  * </snapshotRepository>
 60  
  *
 61  
  * settings.xml
 62  
  * <server>
 63  
  *   <id>kuali.snapshot</id>
 64  
  *   <username>[AWS Access Key ID]</username>
 65  
  *   <password>[AWS Secret Access Key]</password>
 66  
  * </server>
 67  
  *
 68  
  * </code> Kuali Updates -------------<br>
 69  
  * 1) Use username/password instead of passphrase/privatekey for AWS credentials (Maven 3.0 is ignoring passphrase)<br>
 70  
  * 2) Fixed a bug in getBaseDir() if it was passed a one character string<br>
 71  
  * 3) Removed directory creation. The concept of a "directory" inside an AWS bucket is not needed for tools like S3Fox,
 72  
  * Bucket Explorer and https://s3browse.springsource.com/browse/maven.kuali.org/snapshot to correctly display the
 73  
  * contents of the bucket
 74  
  *
 75  
  * @author Ben Hale
 76  
  * @author Jeff Caddel
 77  
  */
 78  
 public class S3Wagon extends AbstractWagon implements RequestFactory {
 79  
         public static final String THREADS_KEY = "maven.wagon.threads";
 80  
         public static final int DEFAULT_THREAD_COUNT = 10;
 81  
 
 82  
         public static final String TIMEOUT_KEY = "maven.wagon.thread.timeout";
 83  
         public static final int FOUR_HOURS = 60 * 60 * 4;
 84  
         public static final int DEFAULT_THREAD_TIMEOUT_SECONDS = FOUR_HOURS;
 85  
 
 86  0
         SimpleFormatter formatter = new SimpleFormatter();
 87  0
         int threadCount = getThreadCount();
 88  0
         int threadTimeout = getThreadTimeout();
 89  
 
 90  0
         final Logger log = LoggerFactory.getLogger(S3Listener.class);
 91  
 
 92  
         private AmazonS3Client client;
 93  
 
 94  
         private Bucket bucket;
 95  
 
 96  
         private String basedir;
 97  
 
 98  0
         private final Mimetypes mimeTypes = Mimetypes.getInstance();
 99  
 
 100  
         public S3Wagon() {
 101  0
                 super(true);
 102  0
                 S3Listener listener = new S3Listener();
 103  0
                 super.addSessionListener(listener);
 104  0
                 super.addTransferListener(listener);
 105  0
         }
 106  
 
 107  
         protected Bucket getOrCreateBucket(final AmazonS3Client client, final String bucketName) {
 108  0
                 List<Bucket> buckets = client.listBuckets();
 109  0
                 for (Bucket bucket : buckets) {
 110  0
                         if (bucket.getName().equals(bucketName)) {
 111  0
                                 return bucket;
 112  
                         }
 113  
                 }
 114  0
                 return client.createBucket(bucketName);
 115  
         }
 116  
 
 117  
         @Override
 118  
         protected void connectToRepository(final Repository source, final AuthenticationInfo authenticationInfo,
 119  
                         final ProxyInfo proxyInfo) throws AuthenticationException {
 120  
 
 121  0
                 AWSCredentials credentials = getCredentials(authenticationInfo);
 122  0
                 client = new AmazonS3Client(credentials);
 123  0
                 bucket = getOrCreateBucket(client, source.getHost());
 124  0
                 basedir = getBaseDir(source);
 125  0
         }
 126  
 
 127  
         @Override
 128  
         protected boolean doesRemoteResourceExist(final String resourceName) {
 129  
                 try {
 130  0
                         client.getObjectMetadata(bucket.getName(), basedir + resourceName);
 131  0
                 } catch (AmazonClientException e1) {
 132  0
                         return false;
 133  0
                 }
 134  0
                 return true;
 135  
         }
 136  
 
 137  
         @Override
 138  
         protected void disconnectFromRepository() {
 139  
                 // Nothing to do for S3
 140  0
         }
 141  
 
 142  
         /**
 143  
          * Pull an object out of an S3 bucket and write it to a file
 144  
          */
 145  
         @Override
 146  
         protected void getResource(final String resourceName, final File destination, final TransferProgress progress)
 147  
                         throws ResourceDoesNotExistException, IOException {
 148  
                 // Obtain the object from S3
 149  0
                 S3Object object = null;
 150  
                 try {
 151  0
                         String key = basedir + resourceName;
 152  0
                         object = client.getObject(bucket.getName(), key);
 153  0
                 } catch (Exception e) {
 154  0
                         throw new ResourceDoesNotExistException("Resource " + resourceName + " does not exist in the repository", e);
 155  0
                 }
 156  
 
 157  
                 //
 158  0
                 InputStream in = null;
 159  0
                 OutputStream out = null;
 160  
                 try {
 161  0
                         in = object.getObjectContent();
 162  0
                         out = new TransferProgressFileOutputStream(destination, progress);
 163  0
                         byte[] buffer = new byte[1024];
 164  
                         int length;
 165  0
                         while ((length = in.read(buffer)) != -1) {
 166  0
                                 out.write(buffer, 0, length);
 167  
                         }
 168  
                 } finally {
 169  0
                         IOUtils.closeQuietly(in);
 170  0
                         IOUtils.closeQuietly(out);
 171  0
                 }
 172  0
         }
 173  
 
 174  
         /**
 175  
          * Is the S3 object newer than the timestamp passed in?
 176  
          */
 177  
         @Override
 178  
         protected boolean isRemoteResourceNewer(final String resourceName, final long timestamp) {
 179  0
                 ObjectMetadata metadata = client.getObjectMetadata(bucket.getName(), basedir + resourceName);
 180  0
                 return metadata.getLastModified().compareTo(new Date(timestamp)) < 0;
 181  
         }
 182  
 
 183  
         /**
 184  
          * List all of the objects in a given directory
 185  
          */
 186  
         @Override
 187  
         protected List<String> listDirectory(final String directory) throws Exception {
 188  0
                 ObjectListing objectListing = client.listObjects(bucket.getName(), basedir + directory);
 189  0
                 List<String> fileNames = new ArrayList<String>();
 190  0
                 for (S3ObjectSummary summary : objectListing.getObjectSummaries()) {
 191  0
                         fileNames.add(summary.getKey());
 192  
                 }
 193  0
                 return fileNames;
 194  
         }
 195  
 
 196  
         /**
 197  
          * Normalize the key to our S3 object<br>
 198  
          * 1. Convert "./css/style.css" into "/css/style.css"<br>
 199  
          * 2. Convert "/foo/bar/../../css/style.css" into "/css/style.css"
 200  
          *
 201  
          * @see java.net.URI.normalize()
 202  
          */
 203  
         protected String getNormalizedKey(final File source, final String destination) {
 204  
                 // Generate our bucket key for this file
 205  0
                 String key = basedir + destination;
 206  
                 try {
 207  0
                         String prefix = "http://s3.amazonaws.com/" + bucket.getName() + "/";
 208  0
                         String urlString = prefix + key;
 209  0
                         URI rawURI = new URI(urlString);
 210  0
                         URI normalizedURI = rawURI.normalize();
 211  0
                         String normalized = normalizedURI.toString();
 212  0
                         int pos = normalized.indexOf(prefix) + prefix.length();
 213  0
                         String normalizedKey = normalized.substring(pos);
 214  0
                         return normalizedKey;
 215  0
                 } catch (Exception e) {
 216  0
                         throw new RuntimeException(e);
 217  
                 }
 218  
         }
 219  
 
 220  
         protected ObjectMetadata getObjectMetadata(final File source, final String destination) {
 221  
                 // Set the mime type according to the extension of the destination file
 222  0
                 String contentType = mimeTypes.getMimetype(destination);
 223  0
                 long contentLength = source.length();
 224  
 
 225  0
                 ObjectMetadata omd = new ObjectMetadata();
 226  0
                 omd.setContentLength(contentLength);
 227  0
                 omd.setContentType(contentType);
 228  0
                 return omd;
 229  
         }
 230  
 
 231  
         /**
 232  
          * Create a PutObjectRequest based on the PutContext
 233  
          */
 234  
         public PutObjectRequest getPutObjectRequest(PutContext context) {
 235  0
                 File source = context.getSource();
 236  0
                 String destination = context.getDestination();
 237  0
                 TransferProgress progress = context.getProgress();
 238  0
                 return getPutObjectRequest(source, destination, progress);
 239  
         }
 240  
 
 241  
         /**
 242  
          * Create a PutObjectRequest based on the source file and destination passed in
 243  
          */
 244  
         protected PutObjectRequest getPutObjectRequest(File source, String destination, TransferProgress progress) {
 245  
                 try {
 246  0
                         String key = getNormalizedKey(source, destination);
 247  0
                         String bucketName = bucket.getName();
 248  0
                         InputStream input = new TransferProgressFileInputStream(source, progress);
 249  0
                         ObjectMetadata metadata = getObjectMetadata(source, destination);
 250  0
                         PutObjectRequest request = new PutObjectRequest(bucketName, key, input, metadata);
 251  0
                         request.setCannedAcl(CannedAccessControlList.PublicRead);
 252  0
                         return request;
 253  0
                 } catch (FileNotFoundException e) {
 254  0
                         throw new AmazonServiceException("File not found", e);
 255  
                 }
 256  
         }
 257  
 
 258  
         /**
 259  
          * On S3 there are no true "directories". An S3 bucket is essentially a Hashtable of files stored by key. The
 260  
          * integration between a traditional file system and an S3 bucket is to use the path of the file on the local file
 261  
          * system as the key to the file in the bucket. The S3 bucket does not contain a separate key for the directory
 262  
          * itself.
 263  
          */
 264  
         public final void putDirectory(File sourceDir, String destinationDir) throws TransferFailedException {
 265  0
                 log.info("Uploading: '" + sourceDir.getAbsolutePath() + "'");
 266  0
                 List<PutContext> contexts = getPutContexts(sourceDir, destinationDir);
 267  0
                 long bytes = sum(contexts);
 268  0
                 log.info("Files: " + contexts.size());
 269  0
                 log.info("Size: " + formatter.getSize(bytes));
 270  0
                 ThreadHandler handler = getThreadHandler(contexts);
 271  0
                 handler.executeThreads();
 272  0
                 if (handler.getException() != null) {
 273  0
                         throw new TransferFailedException("Unexpected error", handler.getException());
 274  
                 }
 275  0
         }
 276  
 
 277  
         protected int getRequestsPerThread(int threads, int requests) {
 278  0
                 int requestsPerThread = requests / threads;
 279  0
                 while (requestsPerThread * threads < requests) {
 280  0
                         requestsPerThread++;
 281  
                 }
 282  0
                 return requestsPerThread;
 283  
         }
 284  
 
 285  
         protected ThreadHandler getThreadHandler(List<PutContext> contexts) {
 286  0
                 int requestsPerThread = getRequestsPerThread(threadCount, contexts.size());
 287  0
                 log.info("Thread Count: " + threadCount);
 288  0
                 log.info("Requests Per Thread: " + requestsPerThread);
 289  0
                 ThreadHandler handler = new ThreadHandler();
 290  0
                 ThreadGroup group = new ThreadGroup("S3 Uploaders");
 291  0
                 group.setDaemon(true);
 292  0
                 Thread[] threads = new Thread[threadCount];
 293  0
                 for (int i = 0; i < threads.length; i++) {
 294  0
                         int offset = i * requestsPerThread;
 295  0
                         int length = requestsPerThread;
 296  0
                         PutThreadContext context = getPutThreadContext(handler, length, offset);
 297  0
                         context.setContexts(contexts);
 298  0
                         PutThread runnable = new PutThread(context);
 299  0
                         threads[i] = new Thread(group, runnable, "S3 Uploader " + i);
 300  0
                         threads[i].setUncaughtExceptionHandler(handler);
 301  0
                         threads[i].setDaemon(true);
 302  
                 }
 303  0
                 handler.setGroup(group);
 304  0
                 handler.setThreads(threads);
 305  0
                 return handler;
 306  
         }
 307  
 
 308  
         protected PutThreadContext getPutThreadContext(ThreadHandler handler, int length, int offset) {
 309  0
                 PutThreadContext context = new PutThreadContext();
 310  0
                 context.setClient(client);
 311  0
                 context.setFactory(this);
 312  0
                 context.setHandler(handler);
 313  0
                 context.setLength(length);
 314  0
                 context.setOffset(offset);
 315  0
                 return context;
 316  
         }
 317  
 
 318  
         protected long sum(List<PutContext> contexts) {
 319  0
                 long sum = 0;
 320  0
                 for (PutContext context : contexts) {
 321  0
                         File file = context.getSource();
 322  0
                         long length = file.length();
 323  0
                         sum += length;
 324  0
                 }
 325  0
                 return sum;
 326  
         }
 327  
 
 328  
         /**
 329  
          * Store a resource into S3
 330  
          */
 331  
         @Override
 332  
         protected void putResource(final File source, final String destination, final TransferProgress progress)
 333  
                         throws IOException {
 334  
 
 335  
                 // Create a new S3Object
 336  0
                 PutObjectRequest request = getPutObjectRequest(source, destination, progress);
 337  
 
 338  
                 // Store the file on S3
 339  0
                 client.putObject(request);
 340  0
         }
 341  
 
 342  
         protected String getDestinationPath(final String destination) {
 343  0
                 return destination.substring(0, destination.lastIndexOf('/'));
 344  
         }
 345  
 
 346  
         /**
 347  
          * Convert "/" -> ""<br>
 348  
          * Convert "/snapshot/" -> "snapshot/"<br>
 349  
          * Convert "/snapshot" -> "snapshot/"<br>
 350  
          */
 351  
         protected String getBaseDir(final Repository source) {
 352  0
                 StringBuilder sb = new StringBuilder(source.getBasedir());
 353  0
                 sb.deleteCharAt(0);
 354  0
                 if (sb.length() == 0) {
 355  0
                         return "";
 356  
                 }
 357  0
                 if (sb.charAt(sb.length() - 1) != '/') {
 358  0
                         sb.append('/');
 359  
                 }
 360  0
                 return sb.toString();
 361  
         }
 362  
 
 363  
         protected String getAuthenticationErrorMessage() {
 364  0
                 StringBuffer sb = new StringBuffer();
 365  0
                 sb.append("The S3 wagon needs AWS Access Key set as the username and AWS Secret Key set as the password. eg:\n");
 366  0
                 sb.append("<server>\n");
 367  0
                 sb.append("  <id>my.server</id>\n");
 368  0
                 sb.append("  <username>[AWS Access Key ID]</username>\n");
 369  0
                 sb.append("  <password>[AWS Secret Access Key]</password>\n");
 370  0
                 sb.append("</server>\n");
 371  0
                 return sb.toString();
 372  
         }
 373  
 
 374  
         /**
 375  
          * Create AWSCredentionals from the information in settings.xml
 376  
          */
 377  
         protected AWSCredentials getCredentials(final AuthenticationInfo authenticationInfo) throws AuthenticationException {
 378  0
                 if (authenticationInfo == null) {
 379  0
                         throw new AuthenticationException(getAuthenticationErrorMessage());
 380  
                 }
 381  0
                 String accessKey = authenticationInfo.getUserName();
 382  0
                 String secretKey = authenticationInfo.getPassword();
 383  0
                 if (accessKey == null || secretKey == null) {
 384  0
                         throw new AuthenticationException(getAuthenticationErrorMessage());
 385  
                 }
 386  0
                 return new BasicAWSCredentials(accessKey, secretKey);
 387  
         }
 388  
 
 389  
         protected int getThreadTimeout() {
 390  0
                 String threadTimeout = System.getProperty(TIMEOUT_KEY);
 391  0
                 if (StringUtils.isEmpty(threadTimeout)) {
 392  0
                         return DEFAULT_THREAD_TIMEOUT_SECONDS;
 393  
                 } else {
 394  0
                         return new Integer(threadTimeout);
 395  
                 }
 396  
         }
 397  
 
 398  
         protected int getThreadCount() {
 399  0
                 String threadCount = System.getProperty(THREADS_KEY);
 400  0
                 if (StringUtils.isEmpty(threadCount)) {
 401  0
                         return DEFAULT_THREAD_COUNT;
 402  
                 } else {
 403  0
                         return new Integer(threadCount);
 404  
                 }
 405  
         }
 406  
 
 407  
 }