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