INTEXT-5: Initial push for AWS core and S3 adapter
For reference see: https://jira.springsource.org/browse/INTEXT-5
This commit is contained in:
committed by
Gunnar Hillert
parent
1d6caa21ad
commit
f35e755ff0
@@ -15,11 +15,13 @@
|
||||
*/
|
||||
package org.springframework.integration.aws.config.xml;
|
||||
|
||||
import org.springframework.integration.aws.s3.config.xml.AmazonS3InboundChannelAdapterParser;
|
||||
import org.springframework.integration.aws.s3.config.xml.AmazonS3OutboundChannelAdapterParser;
|
||||
import org.springframework.integration.aws.ses.config.xml.AmazonSESOutboundAdapterParser;
|
||||
import org.springframework.integration.config.xml.AbstractIntegrationNamespaceHandler;
|
||||
|
||||
/**
|
||||
* The namepsace handler for "int-aws" namespace
|
||||
* The namespace handler for "int-aws" namespace
|
||||
*
|
||||
* @author Amol Nayak
|
||||
*
|
||||
@@ -32,6 +34,8 @@ public class AWSNamespaceHandler extends
|
||||
|
||||
public void init() {
|
||||
this.registerBeanDefinitionParser("ses-outbound-channel-adapter", new AmazonSESOutboundAdapterParser());
|
||||
this.registerBeanDefinitionParser("s3-outbound-channel-adapter",new AmazonS3OutboundChannelAdapterParser());
|
||||
this.registerBeanDefinitionParser("s3-inbound-channel-adapter", new AmazonS3InboundChannelAdapterParser());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2012 the original author or authors.
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2012 the original author or authors.
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.integration.aws.core;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.security.DigestInputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import org.apache.commons.codec.binary.Base64;
|
||||
import org.apache.commons.codec.binary.Hex;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
/**
|
||||
* The common utility methods for the
|
||||
*
|
||||
* @author Amol Nayak
|
||||
*
|
||||
* @since 0.5
|
||||
*
|
||||
*/
|
||||
public class AWSCommonUtils {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(AWSCommonUtils.class);
|
||||
|
||||
/**
|
||||
* Generates the MD5 hash of the file provided
|
||||
* @param file
|
||||
* @return
|
||||
*/
|
||||
public static byte[] getContentsMD5AsBytes(File file) {
|
||||
|
||||
DigestInputStream din = null;
|
||||
final byte[] digestToReturn;
|
||||
|
||||
try {
|
||||
BufferedInputStream bin = new BufferedInputStream(new FileInputStream(file),32768);
|
||||
din = new DigestInputStream(bin, MessageDigest.getInstance("MD5"));
|
||||
//Just to update the digest
|
||||
byte[] dummy = new byte[4096];
|
||||
for (int i = 1; i > 0; i = din.read(dummy));
|
||||
digestToReturn = din.getMessageDigest().digest();
|
||||
}
|
||||
catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalStateException("Caught Exception while generating a MessageDigest instance", e);
|
||||
}
|
||||
catch (FileNotFoundException e) {
|
||||
throw new IllegalStateException("File " + file.getName() + " not found", e);
|
||||
}
|
||||
catch(IOException e) {
|
||||
throw new IllegalStateException("Caught exception while reading from file", e);
|
||||
}
|
||||
finally {
|
||||
IOUtils.closeQuietly(din);
|
||||
}
|
||||
return digestToReturn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the MD5 hash of the provided String
|
||||
* @param the String whose MD5 sun is to be computed
|
||||
* @return
|
||||
*/
|
||||
public static byte[] getContentsMD5AsBytes(String contents) {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("MD5");
|
||||
return digest.digest(contents.getBytes());
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
logger.error("Unable to digest the input String", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes the given raw bytes into hex
|
||||
* @param rawBytes
|
||||
* @return
|
||||
*/
|
||||
public static String encodeHex(byte[] rawBytes) throws UnsupportedEncodingException {
|
||||
return new String(Hex.encodeHex(rawBytes));
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes the given base 64 raw bytes
|
||||
*
|
||||
* @param rawBytes
|
||||
* @return
|
||||
*/
|
||||
public static byte[] decodeBase64(byte[] rawBytes) throws UnsupportedEncodingException {
|
||||
return Base64.decodeBase64(rawBytes);
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,6 @@ import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import com.amazonaws.AmazonWebServiceClient;
|
||||
@@ -67,6 +66,8 @@ public abstract class AbstractAWSClientFactory<T extends AmazonWebServiceClient>
|
||||
*/
|
||||
protected final static String DEFAULT_PROTOCOL = HTTPS;
|
||||
|
||||
private T defaultEndpointInstance;
|
||||
|
||||
|
||||
/**
|
||||
* Returns the cached implementation of the {@link AmazonWebServiceClient} based on the URL provided.
|
||||
@@ -79,6 +80,12 @@ public abstract class AbstractAWSClientFactory<T extends AmazonWebServiceClient>
|
||||
*/
|
||||
public final T getClient(String url) {
|
||||
String endpoint = getEndpointFromURL(url);
|
||||
if(endpoint == null) {
|
||||
if(defaultEndpointInstance == null) {
|
||||
defaultEndpointInstance = getClientImplementation();
|
||||
}
|
||||
return defaultEndpointInstance;
|
||||
}
|
||||
if(!clientMap.containsKey(endpoint)) {
|
||||
T client = getClientImplementation();
|
||||
client.setEndpoint(endpoint);
|
||||
@@ -89,8 +96,10 @@ public abstract class AbstractAWSClientFactory<T extends AmazonWebServiceClient>
|
||||
client = existingClient;
|
||||
}
|
||||
return client;
|
||||
} else
|
||||
}
|
||||
else {
|
||||
return clientMap.get(endpoint);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -115,7 +124,9 @@ public abstract class AbstractAWSClientFactory<T extends AmazonWebServiceClient>
|
||||
* @return
|
||||
*/
|
||||
private String getEndpointFromURL(String stringUrl) {
|
||||
Assert.notNull(stringUrl,"Provided String URL is null");
|
||||
if(!StringUtils.hasText(stringUrl)) {
|
||||
return null;
|
||||
}
|
||||
String endpoint;
|
||||
try {
|
||||
if(!(stringUrl.startsWith(HTTP)
|
||||
@@ -145,4 +156,4 @@ public abstract class AbstractAWSClientFactory<T extends AmazonWebServiceClient>
|
||||
protected abstract T getClientImplementation();
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.integration.aws.s3;
|
||||
|
||||
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* The abstract file name filter that first filters out the file if it is
|
||||
* not eligible for filtering based on the name.
|
||||
* For e.g, if a particular folder on S3 is to be synchronized with the
|
||||
* local file system, then the name of the key is initially accepted
|
||||
* only if it corresponds to an object under that folder or sub folder on S3.
|
||||
* All other keys are ignored
|
||||
*
|
||||
* @author Amol Nayak
|
||||
*
|
||||
* @since 0.5
|
||||
*
|
||||
*/
|
||||
public abstract class AbstractFileNameFilter implements FileNameFilter {
|
||||
|
||||
private volatile String folderName;
|
||||
|
||||
private volatile boolean acceptSubFolders;
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.integration.aws.s3.FileNameFilter#accept(java.lang.String)
|
||||
*/
|
||||
public boolean accept(String fileName) {
|
||||
if(!StringUtils.hasText(fileName))
|
||||
return false;
|
||||
|
||||
if(StringUtils.hasText(folderName)) {
|
||||
if(fileName.startsWith(folderName)) {
|
||||
//This file is in the folder or in a child folder or the given folder
|
||||
String relativePath = fileName.substring(folderName.length());
|
||||
if(relativePath.length() == 0 || (!acceptSubFolders && relativePath.indexOf("/") != -1)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
//Its the folder entry within the bucket
|
||||
if(!acceptSubFolders && fileName.indexOf("/") != -1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if(fileName.contains("/")) {
|
||||
return isFileNameAccepted(fileName.substring(fileName.lastIndexOf("/") + 1));
|
||||
}
|
||||
else {
|
||||
return isFileNameAccepted(fileName);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the folder whose file are to be accepted, this path is relative to the
|
||||
* bucket.
|
||||
* @return
|
||||
*/
|
||||
public String getFolderName() {
|
||||
return folderName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the base folder name under which which the files will be accepted.
|
||||
*
|
||||
* @param folderName
|
||||
*/
|
||||
public void setFolderName(String folderName) {
|
||||
if(StringUtils.hasText(folderName)) {
|
||||
folderName = folderName.trim();
|
||||
if(folderName.equals("/")) {
|
||||
folderName = null;
|
||||
}
|
||||
else {
|
||||
if(!folderName.endsWith("/")) {
|
||||
folderName = folderName + "/";
|
||||
}
|
||||
|
||||
if(folderName.startsWith("/")) {
|
||||
folderName = folderName.substring(1);
|
||||
}
|
||||
}
|
||||
this.folderName = folderName;
|
||||
}
|
||||
else {
|
||||
this.folderName = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks the flag if the sub folders are to be accepted or not.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public boolean isAcceptSubFolders() {
|
||||
return acceptSubFolders;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets if the sub folders of the folder set in {@link #folderName}
|
||||
* are to be accepted or not.
|
||||
*
|
||||
* @param acceptSubFolders
|
||||
*/
|
||||
public void setAcceptSubFolders(boolean acceptSubFolders) {
|
||||
this.acceptSubFolders = acceptSubFolders;
|
||||
}
|
||||
|
||||
|
||||
public abstract boolean isFileNameAccepted(String fileName);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.integration.aws.s3;
|
||||
|
||||
/**
|
||||
* Simple {@link FileNameFilter} implementation that accepts all the names.
|
||||
*
|
||||
* @author Amol Nayak
|
||||
*
|
||||
* @since 0.5
|
||||
*
|
||||
*/
|
||||
public class AlwaysTrueFileNamefilter extends AbstractFileNameFilter {
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.integration.aws.s3.FileNameFilter#accept(java.lang.String)
|
||||
*/
|
||||
|
||||
@Override
|
||||
public boolean isFileNameAccepted(String fileName) {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.integration.aws.s3;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.context.expression.BeanFactoryResolver;
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.expression.spel.support.StandardEvaluationContext;
|
||||
import org.springframework.integration.Message;
|
||||
import org.springframework.integration.aws.core.AWSCredentials;
|
||||
import org.springframework.integration.aws.s3.core.AbstractAmazonS3Operations;
|
||||
import org.springframework.integration.aws.s3.core.AmazonS3Operations;
|
||||
import org.springframework.integration.aws.s3.core.DefaultAmazonS3Operations;
|
||||
import org.springframework.integration.context.IntegrationObjectSupport;
|
||||
import org.springframework.integration.core.MessageSource;
|
||||
import org.springframework.integration.support.MessageBuilder;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* The message source used to receive the File instances stored on the local file system
|
||||
* synchronized from the S3
|
||||
*
|
||||
* @author Amol Nayak
|
||||
*
|
||||
* @since 0.5
|
||||
*
|
||||
*/
|
||||
public class AmazonS3InboundSynchronizationMessageSource extends
|
||||
IntegrationObjectSupport implements MessageSource<File>,FileEventHandler {
|
||||
|
||||
|
||||
private volatile InboundFileSynchronizer synchronizer;
|
||||
private volatile String bucket;
|
||||
private volatile String remoteDirectory;
|
||||
private volatile File directory;
|
||||
private volatile AWSCredentials credentials;
|
||||
private volatile String temporarySuffix = ".writing";
|
||||
private volatile int maxObjectsPerBatch;
|
||||
private volatile String fileNameWildcard;
|
||||
private volatile String fileNameRegex;
|
||||
private volatile BlockingQueue<File> filesQueue;
|
||||
private volatile AmazonS3Operations s3Operations;
|
||||
private volatile boolean acceptSubFolders;
|
||||
private volatile String awsEndpoint;
|
||||
//We will hard code the queue capacity here
|
||||
private final int QUEUE_CAPACITY = 1024;
|
||||
private volatile StandardEvaluationContext ctx;
|
||||
private volatile Expression directoryExpression;
|
||||
|
||||
|
||||
public Message<File> receive() {
|
||||
File headElement = filesQueue.poll();
|
||||
if(headElement == null) {
|
||||
synchronizer.synchronizeToLocalDirectory(directory, bucket, remoteDirectory);
|
||||
//Now check the queue again
|
||||
headElement = filesQueue.poll();
|
||||
}
|
||||
if(headElement != null) {
|
||||
return MessageBuilder.withPayload(headElement).build();
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void onInit() throws Exception {
|
||||
Assert.notNull(directoryExpression, "Local directory to synchronize to is not set");
|
||||
|
||||
ctx = new StandardEvaluationContext();
|
||||
BeanFactory factory = getBeanFactory();
|
||||
if(factory != null) {
|
||||
ctx.setBeanResolver(new BeanFactoryResolver(factory));
|
||||
}
|
||||
String directoryPath = directoryExpression.getValue(ctx,String.class);
|
||||
directory = new File(directoryPath);
|
||||
|
||||
Assert.notNull(directory, "Please provide a valid local directory to synchronize the remote files");
|
||||
// TODO: Uncomment this once we start supporting auto-create-local-directory
|
||||
// Assert.isTrue(directory.exists(),
|
||||
// String.format("Provided directory %s does not exist", directoryPath));
|
||||
Assert.isTrue(directory.isDirectory(),
|
||||
String.format("Provided path %s is not a directory", directoryPath));
|
||||
|
||||
//instantiate the S3Operations instance
|
||||
if(s3Operations == null) {
|
||||
s3Operations = new DefaultAmazonS3Operations(credentials);
|
||||
}
|
||||
|
||||
if(AbstractAmazonS3Operations.class.isAssignableFrom(s3Operations.getClass())) {
|
||||
AbstractAmazonS3Operations abstractOperation = (AbstractAmazonS3Operations)s3Operations;
|
||||
abstractOperation.setTemporaryFileSuffix(temporarySuffix);
|
||||
if(StringUtils.hasText(awsEndpoint)) {
|
||||
abstractOperation.setAwsEndpoint(awsEndpoint);
|
||||
}
|
||||
abstractOperation.afterPropertiesSet();
|
||||
}
|
||||
|
||||
//Now the file operations class
|
||||
InboundLocalFileOperationsImpl fileOperations = new InboundLocalFileOperationsImpl();
|
||||
fileOperations.setTemporaryFileSuffix(temporarySuffix);
|
||||
fileOperations.addEventListener(this);
|
||||
|
||||
InboundFileSynchronizationImpl synchronizationImpl =
|
||||
new InboundFileSynchronizationImpl(s3Operations, fileOperations);
|
||||
synchronizationImpl.setSynchronizingBatchSize(maxObjectsPerBatch);
|
||||
if(StringUtils.hasText(fileNameWildcard)) {
|
||||
synchronizationImpl.setFileWildcard(fileNameWildcard);
|
||||
}
|
||||
if(StringUtils.hasText(fileNameRegex)) {
|
||||
synchronizationImpl.setFileNamePattern(fileNameRegex);
|
||||
}
|
||||
synchronizationImpl.setAcceptSubFolders(acceptSubFolders);
|
||||
synchronizationImpl.afterPropertiesSet();
|
||||
this.synchronizer = synchronizationImpl;
|
||||
|
||||
filesQueue = new ArrayBlockingQueue<File>(QUEUE_CAPACITY);
|
||||
}
|
||||
|
||||
//-- For Spring DI
|
||||
|
||||
/**
|
||||
* Sets the AWSCredential instance to be used
|
||||
*/
|
||||
public void setCredentials(AWSCredentials credentials) {
|
||||
Assert.notNull(credentials, "null 'credentials' provided");
|
||||
this.credentials = credentials;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The temporary suffix that would be used to indicate that the file is being writtem and the operation
|
||||
* is not yet complete
|
||||
*
|
||||
* @param temporarySuffix
|
||||
*/
|
||||
public void setTemporarySuffix(String temporarySuffix) {
|
||||
Assert.hasText(temporarySuffix,"Provide a non null non empty string as temporary suffix");
|
||||
this.temporarySuffix = temporarySuffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* The maximum number of objects those will be retrieved in one batch from Amazon S3 bucket
|
||||
* as part of the listOperation
|
||||
*
|
||||
* @param maxObjectsPerBatch
|
||||
*/
|
||||
public void setMaxObjectsPerBatch(int maxObjectsPerBatch) {
|
||||
Assert.isTrue(maxObjectsPerBatch > 0, "Provide a non sero, non negative number for max objects per batch");
|
||||
this.maxObjectsPerBatch = maxObjectsPerBatch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the file's wildcard pattern that would be used to match the objects in S3 bucket
|
||||
* This attribute is mutually exclusive to fileName regex.
|
||||
*
|
||||
* @param fileWildcard
|
||||
*/
|
||||
public void setFileNameWildcard(String fileNameWildcard) {
|
||||
Assert.hasText(fileNameWildcard, "Provided file wildcard is null or empty string");
|
||||
Assert.isTrue(!StringUtils.hasText(fileNameRegex), "File name regex and wildcard are mutually exclusive");
|
||||
this.fileNameWildcard = fileNameWildcard;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the regex to be used to match the objects in S3 bucket. This attribute is mutually exclusive
|
||||
* to fileName regex.
|
||||
*
|
||||
* @param fileNameRegex
|
||||
*/
|
||||
public void setFileNameRegex(String fileNameRegex) {
|
||||
Assert.hasText(fileNameRegex, "Provided file regex is null or empty string");
|
||||
Assert.isTrue(!StringUtils.hasText(fileNameWildcard), "File name regex and wildcard are mutually exclusive");
|
||||
this.fileNameRegex = fileNameRegex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the bucket with which the data in local directory is synchronized with.
|
||||
*
|
||||
* @param bucket
|
||||
*/
|
||||
public void setBucket(String bucket) {
|
||||
Assert.hasText(bucket, "Provided 'bucket' is null or empty string");
|
||||
this.bucket = bucket;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the remote directory, this is the directory relative to the provided bucket
|
||||
* in S3.
|
||||
*
|
||||
* @param remoteDirectory
|
||||
*/
|
||||
public void setRemoteDirectory(String remoteDirectory) {
|
||||
Assert.hasText(remoteDirectory, "Provided 'remoteDirectory' is null or empty string");
|
||||
this.remoteDirectory = remoteDirectory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the expression to find the local directory where the remote files are synchronized with.
|
||||
*
|
||||
* @param directory
|
||||
*/
|
||||
public void setDirectory(Expression directoryExpression) {
|
||||
Assert.notNull(directoryExpression, "provided 'directoryExpression' is null");
|
||||
this.directoryExpression = directoryExpression;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link AmazonS3Operations} instance that would be used for the receiving
|
||||
* the objects and listing the objects in the bucket.
|
||||
*
|
||||
* @param s3Operations
|
||||
*/
|
||||
public void setS3Operations(AmazonS3Operations s3Operations) {
|
||||
Assert.notNull(s3Operations, "null 's3Operations' instance provided");
|
||||
this.s3Operations = s3Operations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set to true if you want the subfolders of the given remote folder to be synchronized to the
|
||||
* local directory.
|
||||
*
|
||||
* @param acceptSubFolders
|
||||
*/
|
||||
public void setAcceptSubFolders(boolean acceptSubFolders) {
|
||||
this.acceptSubFolders = acceptSubFolders;
|
||||
}
|
||||
|
||||
/**
|
||||
* The AWS region's endpoint whose bucket(and the subfolder if any) will be synchronized
|
||||
* by this adapter
|
||||
*
|
||||
* @param awsEndpoint
|
||||
*/
|
||||
public void setAwsEndpoint(String awsEndpoint) {
|
||||
Assert.hasText(awsEndpoint, "Given AWS Endpoint has to be non null and non empty string");
|
||||
this.awsEndpoint = awsEndpoint;
|
||||
}
|
||||
|
||||
|
||||
//----
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public void onEvent(FileEvent event) {
|
||||
//We are interested in Create new file events only
|
||||
if(FileOperationType.CREATE.equals(event.getFileOperation())) {
|
||||
try {
|
||||
filesQueue.put(event.getFile());
|
||||
//The call hierarchy is
|
||||
//if, no file found in queue, then
|
||||
// receive()
|
||||
// -> InboundFileSynchronizer.synchronizeToLocalDirectory()
|
||||
// ->InboundLocalFileOperations.writeToFile()
|
||||
// ->onEvent()
|
||||
//If the Queue is full and the thread blocks, the lock in synchronizeToLocalDirectory
|
||||
//stays and hence preventing further concurrent synchronization
|
||||
} catch (InterruptedException e) {
|
||||
logger.error("Interrupted while waiting to put the event on the filesQueue", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.integration.aws.s3;
|
||||
|
||||
import static org.springframework.integration.aws.s3.AmazonS3MessageHeaders.METADATA;
|
||||
import static org.springframework.integration.aws.s3.AmazonS3MessageHeaders.OBJECT_ACLS;
|
||||
import static org.springframework.integration.aws.s3.AmazonS3MessageHeaders.USER_METADATA;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.integration.Message;
|
||||
import org.springframework.integration.aws.core.AWSCredentials;
|
||||
import org.springframework.integration.aws.s3.core.AmazonS3Object;
|
||||
import org.springframework.integration.aws.s3.core.AmazonS3OperationException;
|
||||
import org.springframework.integration.aws.s3.core.AmazonS3Operations;
|
||||
import org.springframework.integration.handler.AbstractMessageHandler;
|
||||
import org.springframework.integration.handler.ExpressionEvaluatingMessageProcessor;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
|
||||
/**
|
||||
* The Message handler for the S3 outbound channel adapter
|
||||
*
|
||||
* @author Amol Nayak
|
||||
*
|
||||
* @since 0.5
|
||||
*
|
||||
*/
|
||||
public class AmazonS3MessageHandler extends AbstractMessageHandler {
|
||||
|
||||
|
||||
private final AWSCredentials credentials;
|
||||
|
||||
private final AmazonS3Operations operations;
|
||||
|
||||
private volatile String charset = "UTF-8";
|
||||
|
||||
private volatile String bucket;
|
||||
|
||||
private volatile ExpressionEvaluatingMessageProcessor<String> remoteDirectoryProcessor;
|
||||
|
||||
private volatile FileNameGenerationStrategy fileNameGenerator = new DefaultFileNameGenerationStrategy();
|
||||
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
protected void onInit() throws Exception {
|
||||
super.onInit();
|
||||
Assert.hasText(bucket,"Bucket not set'");
|
||||
Assert.notNull(remoteDirectoryProcessor, "Remote Directory processor should be present, set the remore directory expression");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The constructor that initializes {@link AmazonS3MessageHandler} with the provided
|
||||
* implementation of {@link AmazonS3Operations} and using the provided {@link AmazonWSCredentials}
|
||||
*
|
||||
* @param credentials
|
||||
* @param operations
|
||||
*/
|
||||
public AmazonS3MessageHandler(AWSCredentials credentials,AmazonS3Operations operations) {
|
||||
Assert.notNull(operations,"s3 operations is null");
|
||||
Assert.notNull(credentials,"AWS Credentials are null");
|
||||
this.credentials = credentials;
|
||||
this.operations = operations;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The handler implementation for the Amazon S3 used to put objects in the remote AWS S3 bucket
|
||||
* the message should contain a valid payload of type {@link File}, {@link InputStream},
|
||||
* byte[] or {@link String}. Various predetermined headers as defined in {@link AmazonS3MessageHeaders}
|
||||
* are extracted from the message and an {@link AmazonS3Object} is constructed that is provided to
|
||||
* the {@link AmazonS3Operations} implementation to be uploaded in S3.
|
||||
*
|
||||
* @param message
|
||||
*/
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
protected void handleMessageInternal(Message<?> message) throws Exception {
|
||||
|
||||
Object payload = message.getPayload();
|
||||
|
||||
//The payload can be only of type java.io.File, java.io.InputStream, byte[] or String
|
||||
File file = null;
|
||||
InputStream in = null;
|
||||
|
||||
//potentially unsafe operation if the types are not as those expected
|
||||
Map<String, String> userMetaData = getHeaderValue(message,USER_METADATA,Map.class);
|
||||
Map<String, Object> metaData = getHeaderValue(message,METADATA,Map.class);
|
||||
Map<String, Collection<String>> objectAcls = getHeaderValue(message,OBJECT_ACLS,Map.class);
|
||||
|
||||
AmazonS3ObjectBuilder builder = AmazonS3ObjectBuilder
|
||||
.getInstance()
|
||||
.withMetaData(metaData)
|
||||
.withUserMetaData(userMetaData)
|
||||
.withObjectACL(objectAcls);
|
||||
|
||||
|
||||
String folder = this.remoteDirectoryProcessor.processMessage(message);
|
||||
|
||||
String objectName = this.fileNameGenerator.generateFileName(message);
|
||||
|
||||
if(payload instanceof File) {
|
||||
file = (File)payload;
|
||||
}
|
||||
else if (payload instanceof InputStream) {
|
||||
in = (InputStream)payload;
|
||||
}
|
||||
else if(payload instanceof byte[]) {
|
||||
in = new ByteArrayInputStream((byte[])payload);
|
||||
}
|
||||
else if(payload instanceof String) {
|
||||
in = new ByteArrayInputStream(((String)payload).getBytes(charset));
|
||||
}
|
||||
else {
|
||||
throw new AmazonS3OperationException
|
||||
(credentials.getAccessKey(),
|
||||
bucket, objectName, "The Message payload is of unexpected type "
|
||||
+ payload.getClass().getCanonicalName() + ", only supported types are"
|
||||
+" java.io.File, java.io.InputStream, byte[] and java.lang.String");
|
||||
}
|
||||
if(file != null) {
|
||||
builder.fromFile(file);
|
||||
}
|
||||
else {
|
||||
builder.fromInputStream(in);
|
||||
}
|
||||
|
||||
AmazonS3Object object = builder.build();
|
||||
|
||||
if(logger.isDebugEnabled()) {
|
||||
logger.debug("Uploading Object to bucket " + bucket + ", to folder " + folder + ", with object name " + objectName);
|
||||
}
|
||||
|
||||
operations.putObject(bucket, folder, objectName, object);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The common helper method that would read the message header and checks if it is of a particular type or not
|
||||
* @param <T>
|
||||
* @param message
|
||||
* @param headerName
|
||||
* @param expectedType
|
||||
* @return
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T> T getHeaderValue(Message<?> message, String headerName, Class<T> expectedType) {
|
||||
T header = null;
|
||||
Object genericHeader = message.getHeaders().get(headerName);
|
||||
if(genericHeader == null) {
|
||||
return null;
|
||||
}
|
||||
if(expectedType.isAssignableFrom(genericHeader.getClass())) {
|
||||
header = (T)genericHeader;
|
||||
}
|
||||
else {
|
||||
logger.warn("Found header " + USER_METADATA + " in the message but was not of required type");
|
||||
}
|
||||
return header;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets the charset for the String payload received
|
||||
* @param charset
|
||||
*/
|
||||
public void setCharset(String charset) {
|
||||
Assert.hasText(charset,"'charset' should be non null, non empty string");
|
||||
this.charset = charset;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets the S3 Bucket to which the files are to be uploaded
|
||||
* @param bucket
|
||||
*/
|
||||
public void setBucket(String bucket) {
|
||||
Assert.hasText(bucket, "'bucket' should be non null, non empty string");
|
||||
this.bucket = bucket;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the directory evaluating expression for finding the remote directory in S3
|
||||
* @param expression
|
||||
*/
|
||||
public void setRemoteDirectoryExpression(Expression expression) {
|
||||
Assert.notNull(expression, "Remote directory expression is null");
|
||||
remoteDirectoryProcessor = new ExpressionEvaluatingMessageProcessor<String>(expression);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the file name generation strategy
|
||||
* @param fileNameGenerator
|
||||
*/
|
||||
public void setFileNameGenerator(FileNameGenerationStrategy fileNameGenerator) {
|
||||
Assert.notNull(fileNameGenerator,"File name generator is null");
|
||||
this.fileNameGenerator = fileNameGenerator;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.integration.aws.s3;
|
||||
|
||||
/**
|
||||
* Constants defining the headers containing attributes of the S3 object like
|
||||
* File Name, User's metadata, Object metadata, Object ACL etc
|
||||
*
|
||||
* @author Amol Nayak
|
||||
*
|
||||
* @since 0.5
|
||||
*
|
||||
*/
|
||||
public interface AmazonS3MessageHeaders {
|
||||
|
||||
//TODO: Get rid of the interface for constants
|
||||
public static final String FILE_NAME = "file_name";
|
||||
public static final String USER_METADATA = "user_meta_data";
|
||||
public static final String METADATA = "meta_data";
|
||||
public static final String OBJECT_ACLS = "object_acls";
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.integration.aws.s3;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.integration.aws.s3.core.AmazonS3Object;
|
||||
import org.springframework.integration.aws.s3.core.AmazonS3ObjectACL;
|
||||
import org.springframework.integration.aws.s3.core.Grantee;
|
||||
import org.springframework.integration.aws.s3.core.GranteeType;
|
||||
import org.springframework.integration.aws.s3.core.ObjectGrant;
|
||||
import org.springframework.integration.aws.s3.core.ObjectPermissions;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* The convenience builder class for building {@link AmazonS3Object}
|
||||
*
|
||||
* @author Amol Nayak
|
||||
*
|
||||
* @since 0.5
|
||||
*
|
||||
*/
|
||||
public class AmazonS3ObjectBuilder {
|
||||
|
||||
private final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
private File file;
|
||||
private InputStream in;
|
||||
private Map<String, Object> metaData;
|
||||
private Map<String, String> userMetaData;
|
||||
private AmazonS3ObjectACL objectACL;
|
||||
|
||||
/**
|
||||
* Gets a new instance of the builder
|
||||
* @return
|
||||
*/
|
||||
public static AmazonS3ObjectBuilder getInstance() {
|
||||
return new AmazonS3ObjectBuilder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the file which is to be read for uploading into S3
|
||||
* @param file
|
||||
* @return
|
||||
*/
|
||||
public AmazonS3ObjectBuilder fromFile(File file) {
|
||||
Assert.notNull(file,"null 'file' object provided");
|
||||
Assert.isNull(in, "Cannot instantiate using both the InputStream and File");
|
||||
Assert.isTrue(file.exists(), "Provided File location \""+ file.getAbsolutePath()
|
||||
+ "\" is invalid");
|
||||
Assert.isTrue(!file.isDirectory(), "Provided File location \""+ file.getAbsolutePath()
|
||||
+ "\" is a directory path, please provide a valid file location");
|
||||
this.file = file;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method for setting the File object from the String path
|
||||
* @param fileLocation
|
||||
* @return
|
||||
*/
|
||||
public AmazonS3ObjectBuilder fromLocation(String fileLocation) {
|
||||
Assert.hasText(fileLocation, "'fileLocation should be non null, non empty string");
|
||||
return fromFile(new File(fileLocation));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an InputStream from which the data to be uploaded to S3 will be read
|
||||
* @param in
|
||||
* @return
|
||||
*/
|
||||
public AmazonS3ObjectBuilder fromInputStream(InputStream in) {
|
||||
Assert.notNull(in, "The Stream object provided is null");
|
||||
Assert.isNull(file, "Cannot instantiate using both the InputStream and File");
|
||||
this.in = in;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the given user meta data for the file to be uploaded
|
||||
* @param userMetaData
|
||||
* @return
|
||||
*/
|
||||
public AmazonS3ObjectBuilder withUserMetaData(Map<String, String> userMetaData) {
|
||||
this.userMetaData = userMetaData;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* uses the given metadata for the S3 object to be uploaded
|
||||
* @param metaData
|
||||
* @return
|
||||
*/
|
||||
public AmazonS3ObjectBuilder withMetaData(Map<String, Object> metaData) {
|
||||
this.metaData = metaData;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the S3 Object ACL
|
||||
* @param objectACL
|
||||
* @return
|
||||
*/
|
||||
public AmazonS3ObjectBuilder withObjectACL(Map<String, Collection<String>> objectACL) {
|
||||
//The key can be of three types, the email id, the canonical id of the user or the Group identifier
|
||||
if(objectACL != null && !objectACL.isEmpty()) {
|
||||
this.objectACL = new AmazonS3ObjectACL();
|
||||
for(String key:objectACL.keySet()) {
|
||||
if(isCanonicalId(key)) {
|
||||
addPermissions(key, GranteeType.CANONICAL_GRANTEE_TYPE, objectACL.get(key));
|
||||
}
|
||||
else if(isGroupIdentifier(key)) {
|
||||
addPermissions(key, GranteeType.GROUP_GRANTEE_TYPE, objectACL.get(key));
|
||||
}
|
||||
else {
|
||||
//assuming thats an email identifier, not using regex to validate the email
|
||||
//id. We can add email validation if needed and throw an eexception
|
||||
//if some unexpected value comes up.
|
||||
addPermissions(key, GranteeType.EMAIL_GRANTEE_TYPE, objectACL.get(key));
|
||||
}
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal helper method for adding the Grants to the object ACL
|
||||
* @param grantee
|
||||
* @param type
|
||||
* @param permissions
|
||||
*/
|
||||
private void addPermissions(String grantee,GranteeType type,Collection<String> permissions) {
|
||||
for(String permission:permissions) {
|
||||
ObjectPermissions objectPermission = getObjectPermission(permission);
|
||||
this.objectACL.addGrant(new ObjectGrant
|
||||
(new Grantee(grantee,type), objectPermission));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Appropriate {@link ObjectPermissions} based on the String passed
|
||||
* @param permission
|
||||
* @return
|
||||
*/
|
||||
private ObjectPermissions getObjectPermission(String permission) {
|
||||
//Types of permission are READ, READ_ACP, WRITE_ACP
|
||||
if(StringUtils.hasText(permission)) {
|
||||
String permissionString = permission.trim().replaceAll("[ ]+", "_").toUpperCase();
|
||||
try {
|
||||
return ObjectPermissions.valueOf(permissionString);
|
||||
} catch (IllegalArgumentException e) {
|
||||
logger.error("Requested Enum not found, see underlying exception for more details", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw new IllegalArgumentException("Empty or null string found for object permission");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method checks if the provided String is a canonical id of the AWS account
|
||||
* @param identifier
|
||||
* @return
|
||||
*/
|
||||
private boolean isCanonicalId(String identifier) {
|
||||
//Note: we do not check if the given id is a valid canonical id with AWS, we just check if it is in the right format
|
||||
if(StringUtils.hasText(identifier)) {
|
||||
if(identifier.length() == 64) {
|
||||
String replacedString = identifier.trim().replaceAll("[a-fA-F0-9]+","");
|
||||
return replacedString.length() == 0;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given identifier corresponds to a group id
|
||||
* @param identifier
|
||||
* @return
|
||||
*/
|
||||
private boolean isGroupIdentifier(String identifier){
|
||||
if(StringUtils.hasText(identifier)) {
|
||||
String trimmedIdentifier = identifier.trim();
|
||||
return trimmedIdentifier.equals("http://acs.amazonaws.com/groups/global/AllUsers")
|
||||
|| trimmedIdentifier.equals("http://acs.amazonaws.com/groups/global/AuthenticatedUsers");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the {@link AmazonS3Object} from the provided meta data, file/input stream and object ACLs
|
||||
* @return
|
||||
*/
|
||||
public AmazonS3Object build() {
|
||||
Assert.isTrue(!(in == null && file == null),"One of File object or InputStream is required");
|
||||
return new AmazonS3Object(userMetaData, metaData, in, file,objectACL);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.integration.aws.s3;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.integration.Message;
|
||||
import org.springframework.integration.util.AbstractExpressionEvaluator;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* The Default file name generation strategy. The strategy does the below steps for file name
|
||||
* generation
|
||||
* 1. The expression provided for generation of the file name, it is evaluated and
|
||||
* if a value is obtained, it is used. By default it used the value present in the
|
||||
* file_name header.
|
||||
* 2. Else, if the provided payload is of type {@link File}, then the name of the file is used.
|
||||
* if the file name ends with the temporary file suffix, the suffix is removed and the
|
||||
* remainder of the file is used as the name.
|
||||
* 3. If none of the above two are provided, the file name is the <Message Id>.ext
|
||||
*
|
||||
* @author Amol Nayak
|
||||
*
|
||||
* @since 0.5
|
||||
*
|
||||
*/
|
||||
public class DefaultFileNameGenerationStrategy extends AbstractExpressionEvaluator implements
|
||||
FileNameGenerationStrategy {
|
||||
|
||||
private final Log logger = LogFactory.getLog(DefaultFileNameGenerationStrategy.class);
|
||||
|
||||
private volatile String temporarySuffix = ".writing";
|
||||
|
||||
private volatile String fileNameExpression = "headers['" + AmazonS3MessageHeaders.FILE_NAME + "']" ;
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.integration.aws.s3.FileNameGenerationStrategy#generateFileName(org.springframework.integration.Message)
|
||||
*/
|
||||
public String generateFileName(Message<?> message) {
|
||||
String generatedFileName;
|
||||
try {
|
||||
String fileName = evaluateExpression(fileNameExpression, message,String.class);
|
||||
if(StringUtils.hasText(fileName)) {
|
||||
return fileName;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
//Some exception while evaluating using expression, continue to the file Name
|
||||
//Ignore
|
||||
logger.warn("Exception while evaluating the expression '"
|
||||
+ fileNameExpression + "' on the message", e);
|
||||
}
|
||||
Object payload = message.getPayload();
|
||||
if(payload instanceof File) {
|
||||
String fileName = ((File)payload).getName();
|
||||
if(fileName.endsWith(temporarySuffix)) {
|
||||
//chop off the temp suffix
|
||||
generatedFileName = fileName.substring(0, fileName.indexOf(temporarySuffix));
|
||||
}
|
||||
else {
|
||||
generatedFileName = fileName;
|
||||
}
|
||||
}
|
||||
else {
|
||||
//use the default name generated
|
||||
generatedFileName = message.getHeaders().getId() + ".ext";
|
||||
}
|
||||
if(logger.isInfoEnabled()) {
|
||||
logger.info("Generated file name is " + generatedFileName);
|
||||
}
|
||||
|
||||
return generatedFileName;
|
||||
|
||||
}
|
||||
|
||||
public void setTemporarySuffix(String temporarySuffix) {
|
||||
Assert.hasText(temporarySuffix, "Temporary directory suffix should be non null, non empty");
|
||||
this.temporarySuffix = temporarySuffix;
|
||||
}
|
||||
|
||||
public void setFileNameExpression(String fileNameExpression) {
|
||||
Assert.hasText(fileNameExpression, "Remote name generation expression should be non null, non empty string");
|
||||
this.fileNameExpression = fileNameExpression;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.integration.aws.s3;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
|
||||
/**
|
||||
* The interface denoting the file event type and the {@link File} on which the event occurred
|
||||
*
|
||||
* @author Amol Nayak
|
||||
*
|
||||
* @since 0.5
|
||||
*
|
||||
*/
|
||||
public interface FileEvent {
|
||||
|
||||
/**
|
||||
* The Type of the operation on the {@link File} that occurred
|
||||
* @return The {@link FileOperationType}
|
||||
*/
|
||||
FileOperationType getFileOperation();
|
||||
|
||||
/**
|
||||
* The File on which the given occurred
|
||||
* @return
|
||||
*/
|
||||
File getFile();
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.integration.aws.s3;
|
||||
|
||||
/**
|
||||
* Callback interface that gets notified upon a file event on local file system which are
|
||||
* performed from {@link InboundLocalFileOperations}
|
||||
*
|
||||
* @author Amol Nayak
|
||||
*
|
||||
* @since 0.5
|
||||
*
|
||||
*/
|
||||
public interface FileEventHandler {
|
||||
|
||||
/**
|
||||
* The Method that would be invoked with the specified event that occurred on the file
|
||||
* @param event the {@link FileEvent}
|
||||
*/
|
||||
void onEvent(FileEvent event);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.integration.aws.s3;
|
||||
|
||||
/**
|
||||
* The strategy interface used to filter out file names based on some predetermined criteria
|
||||
*
|
||||
* @author Amol Nayak
|
||||
*
|
||||
* @since 0.5
|
||||
*
|
||||
*/
|
||||
public interface FileNameFilter {
|
||||
|
||||
/**
|
||||
* Determines whether to accept the file with the given name or not
|
||||
* @param fileName
|
||||
* @return true if the file with the given name can be accepted, else false
|
||||
*/
|
||||
boolean accept(String fileName);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.integration.aws.s3;
|
||||
|
||||
import org.springframework.integration.Message;
|
||||
|
||||
/**
|
||||
* The Common interface used to generate the file name for the file to be uploaded to S3
|
||||
*
|
||||
* @author Amol Nayak
|
||||
*
|
||||
* @since 0.5
|
||||
*
|
||||
*/
|
||||
public interface FileNameGenerationStrategy {
|
||||
|
||||
/**
|
||||
* Generates the file name from the given message
|
||||
* @return
|
||||
*/
|
||||
String generateFileName(Message<?> message);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.integration.aws.s3;
|
||||
|
||||
/**
|
||||
* The enum for various file operations
|
||||
*
|
||||
* @author Amol Nayak
|
||||
*
|
||||
* @since 0.5
|
||||
*
|
||||
*/
|
||||
public enum FileOperationType {
|
||||
CREATE,UPDATE,RENAME,DELETE;
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.integration.aws.s3;
|
||||
|
||||
import static org.springframework.integration.aws.core.AWSCommonUtils.decodeBase64;
|
||||
import static org.springframework.integration.aws.core.AWSCommonUtils.encodeHex;
|
||||
import static org.springframework.integration.aws.core.AWSCommonUtils.getContentsMD5AsBytes;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.integration.aws.s3.core.AmazonS3Object;
|
||||
import org.springframework.integration.aws.s3.core.AmazonS3Operations;
|
||||
import org.springframework.integration.aws.s3.core.PaginatedObjectsView;
|
||||
import org.springframework.integration.aws.s3.core.S3ObjectSummary;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
|
||||
/**
|
||||
* The implementation for {@link InboundFileSynchronizer}, this implementation will use
|
||||
* the {@link AmazonS3Operations} to list the objects in the remote bucket on invocation of
|
||||
* the {@link #synchronizeToLocalDirectory(File, String, String)}. The listed objects will then
|
||||
* be checked against the
|
||||
*
|
||||
* @author Amol Nayak
|
||||
*
|
||||
* @since 0.5
|
||||
*
|
||||
*/
|
||||
public class InboundFileSynchronizationImpl implements InboundFileSynchronizer,InitializingBean {
|
||||
|
||||
private final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
public static final String CONTENT_MD5 = "Content-MD5";
|
||||
private final AmazonS3Operations client;
|
||||
private volatile int maxObjectsPerBatch = 100; //default
|
||||
private final InboundLocalFileOperations fileOperations;
|
||||
private volatile FileNameFilter filter;
|
||||
private volatile String fileWildcard;
|
||||
private volatile String fileNameRegex;
|
||||
private final Lock lock = new ReentrantLock();
|
||||
private volatile boolean acceptSubFolders;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param client
|
||||
*/
|
||||
public InboundFileSynchronizationImpl(AmazonS3Operations client,
|
||||
InboundLocalFileOperations fileOperations) {
|
||||
Assert.notNull(client,"AmazonS3Client should be non null");
|
||||
Assert.notNull(fileOperations,"fileOperations should be non null");
|
||||
this.client = client;
|
||||
this.fileOperations = fileOperations;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
Assert.isTrue(!(StringUtils.hasText(fileWildcard) && StringUtils.hasText(fileNameRegex)),
|
||||
"Only one of the file name wildcard string or file name regex can be specified");
|
||||
|
||||
if(StringUtils.hasText(fileWildcard)) {
|
||||
filter = new WildcardFileNameFilter(fileWildcard);
|
||||
}
|
||||
else if(StringUtils.hasText(fileNameRegex)) {
|
||||
filter = new RegexFileNameFilter(fileNameRegex);
|
||||
}
|
||||
else {
|
||||
filter = new AlwaysTrueFileNamefilter(); //Match all
|
||||
}
|
||||
|
||||
if(acceptSubFolders) {
|
||||
((AbstractFileNameFilter)filter).setAcceptSubFolders(true);
|
||||
fileOperations.setCreateDirectoriesIfRequired(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.integration.aws.s3.InboundFileSynchronizer#synchronizeToLocalDirectory(java.io.File, java.lang.String, java.lang.String)
|
||||
*/
|
||||
|
||||
public void synchronizeToLocalDirectory(File localDirectory, String bucketName, String remoteFolder) {
|
||||
if(!lock.tryLock()) {
|
||||
if(logger.isInfoEnabled()) {
|
||||
logger.info("Sync already in progess");
|
||||
}
|
||||
//Prevent concurrent synchronization requests
|
||||
return;
|
||||
}
|
||||
|
||||
if(logger.isInfoEnabled()) {
|
||||
logger.info("Starting sync with local directory");
|
||||
}
|
||||
//Below sync can take long, above lock ensures only one thread is synchronizing
|
||||
try {
|
||||
if(remoteFolder != null && "/".equals(remoteFolder)) {
|
||||
remoteFolder = null;
|
||||
}
|
||||
|
||||
//Set the remote folder for the filter
|
||||
if(filter instanceof AbstractFileNameFilter) {
|
||||
((AbstractFileNameFilter)filter).setFolderName(remoteFolder);
|
||||
}
|
||||
|
||||
String nextMarker = null;
|
||||
do {
|
||||
PaginatedObjectsView paginatedView = client.listObjects(bucketName, remoteFolder,nextMarker,maxObjectsPerBatch);
|
||||
if(paginatedView == null)
|
||||
break; //No files to sync
|
||||
nextMarker = paginatedView.getNextMarker();
|
||||
List<S3ObjectSummary> summaries = paginatedView.getObjectSummary();
|
||||
for(S3ObjectSummary summary:summaries) {
|
||||
String key = summary.getKey();
|
||||
if(key.endsWith("/")) {
|
||||
continue;
|
||||
}
|
||||
if(!filter.accept(key))
|
||||
continue;
|
||||
//The folder is the root as the key is relative to bucket
|
||||
AmazonS3Object s3Object = client.getObject(bucketName, "/", key);
|
||||
synchronizeObjectWithFile(localDirectory,summary,s3Object);
|
||||
}
|
||||
} while(nextMarker != null);
|
||||
|
||||
} finally {
|
||||
lock.unlock();
|
||||
if(logger.isInfoEnabled()) {
|
||||
logger.info("Sync completed");
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Synchronizes the Object with the File on the local file system
|
||||
* @param localDirectory
|
||||
* @param summary
|
||||
*/
|
||||
private void synchronizeObjectWithFile(File localDirectory,S3ObjectSummary summary,
|
||||
AmazonS3Object s3Object) {
|
||||
//Get the complete object data
|
||||
|
||||
String key = summary.getKey();
|
||||
if(key.endsWith("/")) {
|
||||
return;
|
||||
}
|
||||
int lastIndex = key.lastIndexOf("/");
|
||||
|
||||
String fileName = key.substring(lastIndex + 1);
|
||||
String filePath = localDirectory.getAbsolutePath();
|
||||
if(!filePath.endsWith(File.separator)) {
|
||||
filePath += File.separator;
|
||||
}
|
||||
|
||||
File baseDirectory;
|
||||
if(lastIndex > 0) {
|
||||
//there could very well be previous '/' and thus nested sub folders
|
||||
String prefixKey = key.substring(0, lastIndex);
|
||||
String[] folders = prefixKey.split("/");
|
||||
if(folders.length > 0) {
|
||||
for(String folder:folders) {
|
||||
filePath = filePath + folder + File.separator;
|
||||
}
|
||||
//create the directory structure
|
||||
baseDirectory = new File(filePath);
|
||||
baseDirectory.mkdirs();
|
||||
}
|
||||
else {
|
||||
baseDirectory = localDirectory;
|
||||
}
|
||||
}
|
||||
else {
|
||||
baseDirectory = localDirectory;
|
||||
}
|
||||
|
||||
File file = new File(filePath + fileName);
|
||||
if(!file.exists()) {
|
||||
//File doesnt exist, write the contents to it
|
||||
try {
|
||||
fileOperations.writeToFile(baseDirectory, fileName,s3Object.getInputStream());
|
||||
} catch (IOException e) {
|
||||
logger.error("Caught Exception while writing to file " + file.getAbsolutePath());
|
||||
//continue with next file.
|
||||
}
|
||||
}
|
||||
else {
|
||||
//Synchronize a file that exists
|
||||
if(!file.isFile()) {
|
||||
if(logger.isWarnEnabled()) {
|
||||
logger.warn("The file " + file.getAbsolutePath() + " is not a regular file, probably a directory, ");
|
||||
}
|
||||
return;
|
||||
}
|
||||
String eTag = summary.getETag();
|
||||
String md5Hex = null;
|
||||
if(isEtagMD5Hash(eTag)) {
|
||||
//Single thread upload
|
||||
try {
|
||||
md5Hex = encodeHex(getContentsMD5AsBytes(file));
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
logger.error("Exception encountered while generating the MD5 hash for the file " + file.getAbsolutePath(), e);
|
||||
}
|
||||
if(!eTag.equals(md5Hex)) {
|
||||
//The local file is different than the one on S3, could be latest but we will still
|
||||
//sync this with the copy on S3
|
||||
try {
|
||||
fileOperations.writeToFile(baseDirectory, fileName, s3Object.getInputStream());
|
||||
} catch (IOException e) {
|
||||
logger.error("Caught Exception while writing to file " + file.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
//Multi part upload
|
||||
//Get the MD5 hash from the headers
|
||||
Map<String, String> userMetaData = s3Object.getUserMetaData();
|
||||
String b64MD5 = userMetaData.get(CONTENT_MD5);
|
||||
if(b64MD5 != null) {
|
||||
//Need to convert to Hex from Base64
|
||||
try {
|
||||
md5Hex = encodeHex(getContentsMD5AsBytes(file));
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
logger.error("Exception encountered while generating the MD5 hash for the file " + file.getAbsolutePath(), e);
|
||||
}
|
||||
try {
|
||||
String remoteHexMD5 = new String(
|
||||
encodeHex(
|
||||
decodeBase64(b64MD5.getBytes("UTF-8"))));
|
||||
if(!md5Hex.equals(remoteHexMD5)) {
|
||||
//Update only if the local file is not same as remote file
|
||||
try {
|
||||
fileOperations.writeToFile(baseDirectory, fileName, s3Object.getInputStream());
|
||||
} catch (IOException e) {
|
||||
logger.error("Caught Exception while writing to file " + file.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
//Should never get this, suppress
|
||||
}
|
||||
}
|
||||
else {
|
||||
//Forcefully update the file
|
||||
try {
|
||||
fileOperations.writeToFile(baseDirectory, fileName, s3Object.getInputStream());
|
||||
} catch (IOException e) {
|
||||
logger.error("Caught Exception while writing to file " + file.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given eTag is a MD5 hash as hex, the hash is 128 bit and hence
|
||||
* has to be 32 characters in length, also it should contain only hex characters
|
||||
* In case of multi uploads, it is observed that the eTag contains a "-",
|
||||
* and hence this method will return false.
|
||||
*
|
||||
* @param eTag
|
||||
* @return
|
||||
*/
|
||||
private boolean isEtagMD5Hash(String eTag) {
|
||||
if (eTag == null || eTag.length() != 32) {
|
||||
return false;
|
||||
}
|
||||
return eTag.replaceAll("[a-f0-9A-F]", "").length() == 0;
|
||||
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.integration.aws.s3.InboundFileSynchronizer#setSynchronizingBatchSize(int)
|
||||
*/
|
||||
|
||||
public void setSynchronizingBatchSize(int batchSize) {
|
||||
if(batchSize > 0)
|
||||
this.maxObjectsPerBatch = batchSize;
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.integration.aws.s3.InboundFileSynchronizer#setFileNamePattern(java.lang.String)
|
||||
*/
|
||||
|
||||
public void setFileNamePattern(String fileNameRegex) {
|
||||
this.fileNameRegex = fileNameRegex;
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.integration.aws.s3.InboundFileSynchronizer#setFileWildcard(java.lang.String)
|
||||
*/
|
||||
|
||||
public void setFileWildcard(String fileWildcard) {
|
||||
this.fileWildcard = fileWildcard;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAcceptSubFolders(boolean acceptSubFolders) {
|
||||
this.acceptSubFolders = acceptSubFolders;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.integration.aws.s3;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* The strategy interface for synchronizion the remote file system with local directory
|
||||
*
|
||||
* @author Amol Nayak
|
||||
*
|
||||
* @since 0.5
|
||||
*
|
||||
*/
|
||||
public interface InboundFileSynchronizer {
|
||||
|
||||
/**
|
||||
* The operation synchronizes the remote S3 file system with the local directory
|
||||
* It retrieved new files and updates existing ones with the latest content from S3
|
||||
* Please note that this method will NOT delete any additional files present on the
|
||||
* local filesystem
|
||||
* @param localDirectory: The local directory that needs to be synchronized with the
|
||||
* Remote S3 bucket
|
||||
* @param bucketName: The name of the bucket whose contents are to be synchronized
|
||||
* @param remoteFolder: The folder name in S3 whose contents are to be synchronized
|
||||
* use / if the contents of the bucket starting from the root
|
||||
* are to be synchronized. This operation will only synchronize
|
||||
* the files resent in the given remote folder and will ignore
|
||||
* all the folders and sub folder in it.
|
||||
*/
|
||||
void synchronizeToLocalDirectory(File localDirectory,String bucketName,String remoteFolder);
|
||||
|
||||
/**
|
||||
* Sets the max number of Files to synchronize at a time. This is the max number of
|
||||
* Object information retrieved at a time from S3 and synchronized with the Local file system
|
||||
* before the next set of information is retrieved from S3. Please note if the file name
|
||||
* pattern is set the total number of files from a batch fetched can be anywhere between
|
||||
* 0 to the max number of files fetched per batch
|
||||
* @param batchSize
|
||||
*/
|
||||
void setSynchronizingBatchSize(int batchSize);
|
||||
|
||||
/**
|
||||
* Sets the file name regex that will be used to match the key value from S3
|
||||
* to find a match.
|
||||
* @param fileNameRegex
|
||||
*/
|
||||
void setFileNamePattern(String fileNameRegex);
|
||||
|
||||
/**
|
||||
* Sets the simple file name wildcard to match to match the file e.g., it can be
|
||||
* set to *.txt to accept all .txt files
|
||||
*
|
||||
* @param suffix of the file name to match
|
||||
*/
|
||||
void setFileWildcard(String wildcardString);
|
||||
|
||||
/**
|
||||
* Set the value to true to enable the objects to be accepted even if they
|
||||
* are present in the sub folder of the remote folder passed to
|
||||
* {@link #synchronizeToLocalDirectory(File, String, String)}
|
||||
*
|
||||
* @param acceptSubFolders
|
||||
*/
|
||||
void setAcceptSubFolders(boolean acceptSubFolders);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.integration.aws.s3;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Interface for performing File Operations on local file system.
|
||||
* It supports registering an {@link FileEventHandler} instances that
|
||||
* notifies the operations on the file those were performed
|
||||
*
|
||||
* @author Amol Nayak
|
||||
*
|
||||
* @since 0.5
|
||||
*
|
||||
*/
|
||||
public interface InboundLocalFileOperations {
|
||||
|
||||
/**
|
||||
* Registers an individual event handler.
|
||||
* @param handler
|
||||
*/
|
||||
void addEventListener(FileEventHandler handler);
|
||||
|
||||
/**
|
||||
* Registers a {@link List} of {@link FileEventHandler} instances
|
||||
* @param handlers
|
||||
*/
|
||||
void setEventListeners(List<FileEventHandler> handlers);
|
||||
|
||||
/**
|
||||
* The temporary file suffix that will be used when the file is being written to the filesystem
|
||||
* @param prefix
|
||||
*/
|
||||
void setTemporaryFileSuffix(String prefix);
|
||||
|
||||
|
||||
/**
|
||||
* Sets the flag to true if directories given are to be created if not present
|
||||
*
|
||||
* @param createDirectoriesIfRequired
|
||||
*/
|
||||
void setCreateDirectoriesIfRequired(boolean createDirectoriesIfRequired);
|
||||
|
||||
/**
|
||||
* The method will write to the file with the specified name in the specified directory
|
||||
* from the given {@link InputStream}. Upon completion of the writing the appropriate
|
||||
* {@link FileEventHandler} instance(s) will be notified with the {@link FileOperationType}
|
||||
* <i>WRITE<i> and {@link File} instance for the created file.
|
||||
* @param directory
|
||||
* @param fileName
|
||||
* @param in
|
||||
*/
|
||||
void writeToFile(File directory,String fileName,InputStream in) throws IOException ;
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.integration.aws.s3;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* The Implementation class for the {@link InboundLocalFileOperations}
|
||||
*
|
||||
* @author Amol Nayak
|
||||
*
|
||||
* @since 0.5
|
||||
*
|
||||
*/
|
||||
public class InboundLocalFileOperationsImpl implements
|
||||
InboundLocalFileOperations {
|
||||
|
||||
private final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
private final List<FileEventHandler> handlers = new ArrayList<FileEventHandler>();
|
||||
|
||||
private volatile String tempFileSuffix = ".writing";
|
||||
|
||||
private volatile boolean createDirectoriesIfRequired;
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.integration.aws.s3.InboundLocalFileOperations#addEventListener(org.springframework.integration.aws.s3.FileEventHandler)
|
||||
*/
|
||||
@Override
|
||||
public void addEventListener(FileEventHandler handler) {
|
||||
Assert.notNull(handler, "Handler instance must non null");
|
||||
handlers.add(handler);
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.integration.aws.s3.InboundLocalFileOperations#setEventListeners(java.util.List)
|
||||
*/
|
||||
@Override
|
||||
public void setEventListeners(List<FileEventHandler> handlers) {
|
||||
Assert.notNull(handlers, "Handlers must be non null and non empty");
|
||||
Assert.notEmpty(handlers, "Handlers must be non null and non empty");
|
||||
this.handlers.clear();
|
||||
this.handlers.addAll(handlers);
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.integration.aws.s3.InboundLocalFileOperations#setTemporaryFileSuffix(java.lang.String)
|
||||
*/
|
||||
@Override
|
||||
public void setTemporaryFileSuffix(String tempFileSuffix) {
|
||||
if(!StringUtils.hasText(tempFileSuffix)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!tempFileSuffix.startsWith(".")) {
|
||||
this.tempFileSuffix = "." + tempFileSuffix;
|
||||
}
|
||||
else {
|
||||
this.tempFileSuffix = tempFileSuffix;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if create directories if required flag is set to true
|
||||
* @return
|
||||
*/
|
||||
public boolean isCreateDirectoriesIfRequired() {
|
||||
return createDirectoriesIfRequired;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the flag to true if directories given are to be created if not present
|
||||
*
|
||||
* @param createDirectoriesIfRequired
|
||||
*/
|
||||
@Override
|
||||
public void setCreateDirectoriesIfRequired(boolean createDirectoriesIfRequired) {
|
||||
this.createDirectoriesIfRequired = createDirectoriesIfRequired;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.integration.aws.s3.InboundLocalFileOperations#writeToFile(java.io.File, java.lang.String, java.io.InputStream)
|
||||
*/
|
||||
public void writeToFile(File directory, String fileName, InputStream in)
|
||||
throws IOException {
|
||||
Assert.notNull(directory, "Provide a non null directory");
|
||||
Assert.hasText(fileName, "Provide a non null non empty file name");
|
||||
Assert.notNull(in,"Provide a non null instance of InputStream");
|
||||
Assert.isTrue(!directory.exists() || directory.isDirectory(),"Provided directory is not a directory");
|
||||
Assert.isTrue(createDirectoriesIfRequired || directory.exists(),"Provided directories does not exist and create directory flag is false");
|
||||
|
||||
if(!directory.exists() && createDirectoriesIfRequired) {
|
||||
if(!directory.mkdirs()) {
|
||||
throw new IOException(String.format("Unable to create the directory '%s'", directory.getAbsolutePath()));
|
||||
}
|
||||
}
|
||||
|
||||
if(!(in instanceof ByteArrayInputStream)
|
||||
&& !(in instanceof BufferedInputStream)) {
|
||||
in = new BufferedInputStream(in);
|
||||
}
|
||||
String tempFileName = fileName + tempFileSuffix;
|
||||
byte[] bytes = new byte[4096]; //4K
|
||||
|
||||
String absoluteDirectoryPath = directory.getAbsolutePath();
|
||||
String filePath;
|
||||
if(absoluteDirectoryPath.endsWith(File.separator)) {
|
||||
filePath = absoluteDirectoryPath + tempFileName;
|
||||
}
|
||||
else {
|
||||
filePath = absoluteDirectoryPath + File.separator + tempFileName;
|
||||
}
|
||||
|
||||
final File fileToWrite = new File(filePath);
|
||||
if(!fileToWrite.exists()) {
|
||||
fileToWrite.createNewFile();
|
||||
}
|
||||
FileOutputStream fos = new FileOutputStream(fileToWrite);
|
||||
BufferedOutputStream bos = new BufferedOutputStream(fos);
|
||||
for(int read = 0;(read = in.read(bytes)) != -1;) {
|
||||
bos.write(bytes, 0, read);
|
||||
}
|
||||
bos.close();
|
||||
//Now rename the file
|
||||
final File dest = new File(filePath.substring(0, filePath.indexOf(tempFileSuffix)));
|
||||
//ifDestination file exists, delete it
|
||||
final boolean isSuccessful;
|
||||
if(dest.exists()) {
|
||||
boolean isDeleteSuccessful = dest.delete();
|
||||
if(isDeleteSuccessful) {
|
||||
if(logger.isDebugEnabled()) {
|
||||
logger.debug("Delete of file " + dest.getName() + " successful");
|
||||
}
|
||||
//now rename the temp file to perm destination file
|
||||
isSuccessful = renameFile(fileToWrite, dest);
|
||||
}
|
||||
else {
|
||||
if(logger.isWarnEnabled()) {
|
||||
logger.warn("Deletion of file " + dest.getName() + " not successful, falling back to overwriting the contents");
|
||||
}
|
||||
FileCopyUtils.copy(fileToWrite, dest);
|
||||
boolean deleteTemp = fileToWrite.delete();
|
||||
if(!deleteTemp && logger.isWarnEnabled()) {
|
||||
logger.warn("Deletion of " + fileToWrite.getName() + " unsuccessful");
|
||||
}
|
||||
isSuccessful = true; //as copy has occurred successfully
|
||||
|
||||
}
|
||||
}
|
||||
else {
|
||||
isSuccessful = renameFile(fileToWrite, dest);
|
||||
}
|
||||
//notify the listeners
|
||||
if(!handlers.isEmpty()) {
|
||||
FileEvent event = new FileEvent() {
|
||||
|
||||
|
||||
public FileOperationType getFileOperation() {
|
||||
return FileOperationType.CREATE;
|
||||
}
|
||||
|
||||
|
||||
public File getFile() {
|
||||
if(isSuccessful) {
|
||||
return dest;
|
||||
}
|
||||
else {
|
||||
return fileToWrite;
|
||||
}
|
||||
}
|
||||
};
|
||||
for(FileEventHandler handler:handlers) {
|
||||
try {
|
||||
handler.onEvent(event);
|
||||
} catch (Exception e) {
|
||||
if(logger.isInfoEnabled())
|
||||
logger.info("Exception occurred while notifying the handler class "
|
||||
+ handler.getClass().getName(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Private helper method that is used to rename the source to destination file
|
||||
*
|
||||
* @param fileToWrite
|
||||
* @param dest
|
||||
* @return
|
||||
*/
|
||||
private boolean renameFile(final File from, final File to) {
|
||||
final boolean isSuccessful;
|
||||
final boolean isRenameSuccessful = from.renameTo(to);
|
||||
if(isRenameSuccessful) {
|
||||
if(logger.isDebugEnabled()) {
|
||||
logger.debug("Renaming of file " + from.getName() + " to "
|
||||
+ to.getName() + " successful");
|
||||
}
|
||||
isSuccessful = isRenameSuccessful;
|
||||
}
|
||||
else {
|
||||
if(logger.isWarnEnabled()) {
|
||||
logger.warn("Renaming of file " + from.getName() + " to "
|
||||
+ to.getName() + " unsuccessful");
|
||||
}
|
||||
isSuccessful = false;
|
||||
}
|
||||
return isSuccessful;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.integration.aws.s3;
|
||||
|
||||
import org.apache.commons.io.filefilter.IOFileFilter;
|
||||
import org.apache.commons.io.filefilter.RegexFileFilter;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Filters out the files by matching the given File name against the given regex
|
||||
* Uses Apache Commons IO {@link RegexFileFilter} internally
|
||||
*
|
||||
* @author Amol Nayak
|
||||
*
|
||||
* @since 0.5
|
||||
*
|
||||
*/
|
||||
public class RegexFileNameFilter extends AbstractFileNameFilter {
|
||||
private final IOFileFilter filter;
|
||||
|
||||
/**
|
||||
* Default constructor accepting the regex
|
||||
* @param regex
|
||||
*/
|
||||
public RegexFileNameFilter(String regex) {
|
||||
Assert.hasText(regex, "Regex should be non null, non empty String");
|
||||
filter = new RegexFileFilter(regex);
|
||||
}
|
||||
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.integration.aws.s3.FileNameFilter#accept(java.lang.String)
|
||||
*/
|
||||
|
||||
@Override
|
||||
public boolean isFileNameAccepted(String fileName) {
|
||||
return filter.accept(null, fileName);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.integration.aws.s3;
|
||||
|
||||
import org.apache.commons.io.IOCase;
|
||||
import org.apache.commons.io.filefilter.IOFileFilter;
|
||||
import org.apache.commons.io.filefilter.WildcardFileFilter;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Performs wildcard filename filtering based on the wildcard String passed.
|
||||
* Used Apache Commons IO {@link WildcardFileFilter} to perform the filtering
|
||||
*
|
||||
* @author Amol Nayak
|
||||
*
|
||||
* @since 0.5
|
||||
*
|
||||
*/
|
||||
public class WildcardFileNameFilter extends AbstractFileNameFilter {
|
||||
|
||||
private final IOFileFilter filter;
|
||||
|
||||
/**
|
||||
* Default construtor accepting the wildcard string
|
||||
*
|
||||
* @param wildcardString
|
||||
*/
|
||||
public WildcardFileNameFilter(String wildcardString) {
|
||||
Assert.hasText(wildcardString, "Wildcard string should be non null, non empty String");
|
||||
filter = new WildcardFileFilter(wildcardString,IOCase.INSENSITIVE);
|
||||
//Our checks will be case insensitive
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.integration.aws.s3.FileNameFilter#accept(java.lang.String)
|
||||
*/
|
||||
|
||||
@Override
|
||||
public boolean isFileNameAccepted(String fileName) {
|
||||
return filter.accept(null, fileName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.integration.aws.s3.config.xml;
|
||||
import static org.springframework.integration.aws.config.xml.AmazonWSParserUtils.getAmazonWSCredentials;
|
||||
|
||||
import org.springframework.beans.BeanMetadataElement;
|
||||
import org.springframework.beans.factory.BeanDefinitionStoreException;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
|
||||
import org.springframework.beans.factory.xml.ParserContext;
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.expression.common.LiteralExpression;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.integration.aws.s3.AmazonS3InboundSynchronizationMessageSource;
|
||||
import org.springframework.integration.config.xml.AbstractPollingInboundChannelAdapterParser;
|
||||
import org.springframework.integration.config.xml.IntegrationNamespaceUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
|
||||
/**
|
||||
* The channel adapter parser for the S3 inbound parser
|
||||
*
|
||||
* @author Amol Nayak
|
||||
*
|
||||
* @since 0.5
|
||||
*
|
||||
*/
|
||||
public class AmazonS3InboundChannelAdapterParser extends
|
||||
AbstractPollingInboundChannelAdapterParser {
|
||||
|
||||
private static final String S3_BUCKET = "bucket";
|
||||
private static final String TEMPORARY_SUFFIX = "temporary-suffix";
|
||||
private static final String S3_OPERATIONS = "s3-operations";
|
||||
private static final String AWS_ENDPOINT = "aws-endpoint";
|
||||
private static final String REMOTE_DIRECTORY = "remote-directory";
|
||||
private static final String LOCAL_DIRECTORY = "local-directory";
|
||||
private static final String LOCAL_DIRECTORY_EXPRESSION = "local-directory-expression";
|
||||
private static final String AWS_CREDENTIAL = "credentials";
|
||||
private static final String MAX_OBJECTS_PER_BATCH = "max-objects-per-batch";
|
||||
private static final String FILE_NAME_WILDCARD = "file-name-wildcard";
|
||||
private static final String FILE_NAME_REGEX = "file-name-regex";
|
||||
private static final String ACCEPT_SUB_FOLDERS = "accept-sub-folders";
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.integration.config.xml.AbstractPollingInboundChannelAdapterParser#parseSource(org.w3c.dom.Element, org.springframework.beans.factory.xml.ParserContext)
|
||||
*/
|
||||
@Override
|
||||
protected BeanMetadataElement parseSource(Element element,
|
||||
ParserContext parserContext) {
|
||||
String awsCredentials = getAmazonWSCredentials(element, parserContext);
|
||||
BeanDefinitionBuilder builder = BeanDefinitionBuilder
|
||||
.genericBeanDefinition(AmazonS3InboundSynchronizationMessageSource.class);
|
||||
builder.addPropertyReference(AWS_CREDENTIAL, awsCredentials);
|
||||
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, S3_OPERATIONS);
|
||||
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, S3_BUCKET);
|
||||
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, TEMPORARY_SUFFIX);
|
||||
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, REMOTE_DIRECTORY);
|
||||
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, AWS_ENDPOINT);
|
||||
String directory = element.getAttribute(LOCAL_DIRECTORY);
|
||||
String directoryExpression = element.getAttribute(LOCAL_DIRECTORY_EXPRESSION);
|
||||
boolean hasDirectory = StringUtils.hasText(directory);
|
||||
boolean hasDirectoryExpression = StringUtils.hasText(directoryExpression);
|
||||
if(!hasDirectory && !hasDirectoryExpression) {
|
||||
String message =
|
||||
String.format("One of attributes '%s' and '%s' is required", LOCAL_DIRECTORY, LOCAL_DIRECTORY_EXPRESSION);
|
||||
throw new BeanDefinitionStoreException(message);
|
||||
}
|
||||
if(hasDirectory && hasDirectoryExpression) {
|
||||
String message =
|
||||
String.format("Attributes '%s' and '%s' are mutually exclusive to each other", LOCAL_DIRECTORY, LOCAL_DIRECTORY_EXPRESSION);
|
||||
throw new BeanDefinitionStoreException(message);
|
||||
}
|
||||
else {
|
||||
Expression expr;
|
||||
if(hasDirectory) {
|
||||
expr = new LiteralExpression(directory);
|
||||
}
|
||||
else {
|
||||
expr = new SpelExpressionParser().parseExpression(directoryExpression);
|
||||
}
|
||||
builder.addPropertyValue("directory", expr);
|
||||
}
|
||||
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, MAX_OBJECTS_PER_BATCH);
|
||||
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, ACCEPT_SUB_FOLDERS);
|
||||
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, FILE_NAME_WILDCARD);
|
||||
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, FILE_NAME_REGEX);
|
||||
return builder.getBeanDefinition();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.integration.aws.s3.config.xml;
|
||||
|
||||
import org.springframework.beans.factory.BeanDefinitionStoreException;
|
||||
import org.springframework.beans.factory.support.AbstractBeanDefinition;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
|
||||
import org.springframework.beans.factory.xml.ParserContext;
|
||||
import org.springframework.expression.common.LiteralExpression;
|
||||
import org.springframework.integration.aws.config.xml.AbstractAWSOutboundChannelAdapterParser;
|
||||
import org.springframework.integration.aws.s3.AmazonS3MessageHandler;
|
||||
import org.springframework.integration.aws.s3.DefaultFileNameGenerationStrategy;
|
||||
import org.springframework.integration.aws.s3.core.DefaultAmazonS3Operations;
|
||||
import org.springframework.integration.config.ExpressionFactoryBean;
|
||||
import org.springframework.integration.config.xml.IntegrationNamespaceUtils;
|
||||
import org.springframework.integration.core.MessageHandler;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
/**
|
||||
* The namespace parser for outbound-channel-parser for the aws-s3 namespace
|
||||
*
|
||||
* @author Amol Nayak
|
||||
*
|
||||
* @since 0.5
|
||||
*
|
||||
*/
|
||||
public class AmazonS3OutboundChannelAdapterParser extends
|
||||
AbstractAWSOutboundChannelAdapterParser {
|
||||
|
||||
private static final String S3_OPERATIONS = "s3-operations";
|
||||
private static final String AWS_ENDPOINT = "aws-endpoint";
|
||||
private static final String S3_BUCKET = "bucket";
|
||||
private static final String CHARSET = "charset";
|
||||
private static final String MULTIPART_THRESHOLD = "multipart-upload-threshold";
|
||||
private static final String TEMPORARY_DIRECTORY = "temporary-directory";
|
||||
private static final String TEMPORARY_SUFFIX = "temporary-suffix";
|
||||
private static final String THREADPOOL_EXECUTOR = "thread-pool-executor";
|
||||
private static final String REMOTE_DIRECTORY = "remote-directory";
|
||||
private static final String REMOTE_DIRECTORY_EXPRESSION = "remote-directory-expression";
|
||||
private static final String FILE_NAME_GENERATOR = "file-name-generator";
|
||||
private static final String FILE_NAME_GENERATION_EXPRESSION = "file-name-generation-expression";
|
||||
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.integration.aws.core.config.AbstractAWSOutboundChannelAdapterParser#getMessageHandlerImplementation()
|
||||
*/
|
||||
|
||||
@Override
|
||||
protected Class<? extends MessageHandler> getMessageHandlerImplementation() {
|
||||
return AmazonS3MessageHandler.class;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is where we will be instantiating the AmazonS3Operations instance and
|
||||
* passing it to the MessageHandler
|
||||
*/
|
||||
@Override
|
||||
protected void processBeanDefinition(BeanDefinitionBuilder builder,
|
||||
String awsCredentialsGeneratedName,Element element, ParserContext context) {
|
||||
|
||||
//TODO: When we will have more than one implementations, also provision with an enum
|
||||
//for the operation
|
||||
|
||||
String s3Operations = element.getAttribute(S3_OPERATIONS);
|
||||
String operationsService;
|
||||
if(StringUtils.hasText(s3Operations)) {
|
||||
//custom implementation provided
|
||||
if(element.hasAttribute(MULTIPART_THRESHOLD)
|
||||
|| element.hasAttribute(TEMPORARY_DIRECTORY)
|
||||
|| element.hasAttribute(TEMPORARY_SUFFIX)
|
||||
|| element.hasAttribute(THREADPOOL_EXECUTOR)) {
|
||||
throw new BeanDefinitionStoreException("Attributes '" + MULTIPART_THRESHOLD + "', '"
|
||||
+ TEMPORARY_DIRECTORY + "', '" + TEMPORARY_SUFFIX + "' and '" + THREADPOOL_EXECUTOR
|
||||
+ " are mutually exclusive to the '" + S3_OPERATIONS + "' attribute");
|
||||
}
|
||||
operationsService = s3Operations;
|
||||
}
|
||||
else {
|
||||
BeanDefinitionBuilder s3OpBuilder = BeanDefinitionBuilder.genericBeanDefinition(DefaultAmazonS3Operations.class);
|
||||
s3OpBuilder.addConstructorArgReference(awsCredentialsGeneratedName);
|
||||
IntegrationNamespaceUtils.setValueIfAttributeDefined(s3OpBuilder, element, MULTIPART_THRESHOLD);
|
||||
IntegrationNamespaceUtils.setValueIfAttributeDefined(s3OpBuilder, element, TEMPORARY_DIRECTORY);
|
||||
IntegrationNamespaceUtils.setValueIfAttributeDefined(s3OpBuilder, element, TEMPORARY_SUFFIX,"temporaryFileSuffix");
|
||||
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(s3OpBuilder, element, THREADPOOL_EXECUTOR);
|
||||
IntegrationNamespaceUtils.setValueIfAttributeDefined(s3OpBuilder, element, AWS_ENDPOINT);
|
||||
operationsService = BeanDefinitionReaderUtils.registerWithGeneratedName(s3OpBuilder.getBeanDefinition(), context.getRegistry());
|
||||
}
|
||||
|
||||
//Set the bucket and charset
|
||||
builder.addConstructorArgReference(operationsService);
|
||||
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, CHARSET);
|
||||
builder.addPropertyValue(S3_BUCKET, element.getAttribute(S3_BUCKET)); //Mandatory
|
||||
|
||||
//Get the remote directory expression or remote directory literal string
|
||||
String remoteDirectoryLiteral = element.getAttribute(REMOTE_DIRECTORY);
|
||||
String remoteDirectoryExpression = element.getAttribute(REMOTE_DIRECTORY_EXPRESSION);
|
||||
boolean hasRemoteDirectoryExpression = StringUtils.hasText(remoteDirectoryExpression);
|
||||
boolean hasRemoteDirectoryLiteral = StringUtils.hasText(remoteDirectoryLiteral);
|
||||
if(!(hasRemoteDirectoryExpression ^ hasRemoteDirectoryLiteral)) {
|
||||
throw new BeanDefinitionStoreException("Exactly one of " + REMOTE_DIRECTORY + " or "
|
||||
+ REMOTE_DIRECTORY_EXPRESSION + " is required");
|
||||
}
|
||||
AbstractBeanDefinition expression;
|
||||
if(hasRemoteDirectoryLiteral) {
|
||||
expression = BeanDefinitionBuilder.genericBeanDefinition(LiteralExpression.class)
|
||||
.addConstructorArgValue(remoteDirectoryLiteral)
|
||||
.getBeanDefinition();
|
||||
}
|
||||
else {
|
||||
expression = BeanDefinitionBuilder.genericBeanDefinition(ExpressionFactoryBean.class)
|
||||
.addConstructorArgValue(remoteDirectoryExpression)
|
||||
.getBeanDefinition();
|
||||
}
|
||||
builder.addPropertyValue("remoteDirectoryExpression", expression);
|
||||
|
||||
//Get the File generation strategy
|
||||
String fileNameGenerator = element.getAttribute(FILE_NAME_GENERATOR);
|
||||
String fileNameGenerationExpression = element.getAttribute(FILE_NAME_GENERATION_EXPRESSION);
|
||||
boolean hasFileGenerator = StringUtils.hasText(fileNameGenerator);
|
||||
boolean hasFileGenerationExpression = StringUtils.hasText(fileNameGenerationExpression);
|
||||
|
||||
if(hasFileGenerationExpression && hasFileGenerator) {
|
||||
throw new BeanDefinitionStoreException("Attributes '" + FILE_NAME_GENERATION_EXPRESSION + "' and '"
|
||||
+ FILE_NAME_GENERATOR + "' are mutually exclusive, at most one might be specified");
|
||||
}
|
||||
|
||||
if(hasFileGenerator) {
|
||||
builder.addPropertyReference("fileNameGenerator", fileNameGenerator);
|
||||
}
|
||||
else {
|
||||
BeanDefinitionBuilder fileNameGeneratorBuilder =
|
||||
BeanDefinitionBuilder.genericBeanDefinition(DefaultFileNameGenerationStrategy.class);
|
||||
String tempDirectorySuffix = element.getAttribute(TEMPORARY_SUFFIX);
|
||||
if(StringUtils.hasText(tempDirectorySuffix)) {
|
||||
fileNameGeneratorBuilder.addPropertyValue("temporarySuffix",tempDirectorySuffix);
|
||||
}
|
||||
if(hasFileGenerationExpression) {
|
||||
fileNameGeneratorBuilder.addPropertyValue("fileNameExpression", fileNameGenerationExpression);
|
||||
}
|
||||
builder.addPropertyValue("fileNameGenerator", fileNameGeneratorBuilder.getBeanDefinition());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,510 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.integration.aws.s3.core;
|
||||
|
||||
import static org.springframework.integration.aws.core.AWSCommonUtils.encodeHex;
|
||||
import static org.springframework.integration.aws.core.AWSCommonUtils.getContentsMD5AsBytes;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.integration.aws.core.AWSCredentials;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* The common super class for any implementation of {@link AmazonS3Operations}. The sub class
|
||||
* has to implement all the functionality for performing the actual work to add, remove, update
|
||||
* delete, list the objects in an S3 bucket
|
||||
*
|
||||
* @author Amol Nayak
|
||||
*
|
||||
* @since 0.5
|
||||
*
|
||||
*/
|
||||
public abstract class AbstractAmazonS3Operations implements AmazonS3Operations,InitializingBean {
|
||||
|
||||
protected final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
private volatile long multipartUploadThreshold;
|
||||
|
||||
private volatile File temporaryDirectory = new File(System.getProperty("java.io.tmpdir"));
|
||||
|
||||
private volatile String temporaryFileSuffix = ".writing";
|
||||
|
||||
public final String PATH_SEPARATOR = "/";
|
||||
|
||||
private final AWSCredentials credentials;
|
||||
|
||||
private String awsEndpoint;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* The constructor that accepts the {@link AmazonWSCredentials}
|
||||
* @param credentials
|
||||
*/
|
||||
protected AbstractAmazonS3Operations(AWSCredentials credentials) {
|
||||
Assert.notNull(credentials,"null 'credentials' provided");
|
||||
this.credentials = credentials;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the threshold value in bytes above which multi part upload will be used
|
||||
* @return
|
||||
*/
|
||||
public long getMultipartUploadThreshold() {
|
||||
return multipartUploadThreshold;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The threshold value in bytes above which the service will use multi part upload.
|
||||
* All the uploads below this value will be uploaded in a single thread
|
||||
* Minimum value for the threshold is 5120 Bytes (5 KB).
|
||||
* It is recommended by Amazon to use Multi part uploads for all the uploads
|
||||
* above 100 MB
|
||||
* If the value is set to a number above Integer.MAX_VALUE, the value will be
|
||||
* set to Integer.MAX_VALUE.
|
||||
*
|
||||
* @param multipartUploadThreshold
|
||||
*/
|
||||
public void setMultipartUploadThreshold(long multipartUploadThreshold) {
|
||||
Assert.isTrue(multipartUploadThreshold >= 5120,
|
||||
"Minimum threshold for multipart upload is 5120 bytes");
|
||||
this.multipartUploadThreshold = multipartUploadThreshold;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the temporary directory
|
||||
* @return
|
||||
*/
|
||||
public File getTemporaryDirectory() {
|
||||
return temporaryDirectory;
|
||||
}
|
||||
|
||||
/**
|
||||
* The temporary directory that will be used to write the files received over stream
|
||||
* @param temporaryDirectory
|
||||
*/
|
||||
public void setTemporaryDirectory(File temporaryDirectory) {
|
||||
Assert.notNull(temporaryDirectory, "Provided temporaryDirectory is null");
|
||||
Assert.isTrue(temporaryDirectory.exists(),"The given temporary directory does not exist");
|
||||
Assert.isTrue(temporaryDirectory.isDirectory(), "The given temporary directory path has to be a directory");
|
||||
this.temporaryDirectory = temporaryDirectory;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The temporary directory that will be used to write the files received over stream
|
||||
* @param temporaryDirectory
|
||||
*/
|
||||
public void setTemporaryDirectory(String temporaryDirectory) {
|
||||
Assert.hasText(temporaryDirectory,"Provided temporary directory string is null or empty string");
|
||||
setTemporaryDirectory(new File(temporaryDirectory));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the temporary file suffix that is appended to the file while writing to
|
||||
* the temporary directory
|
||||
* @return
|
||||
*/
|
||||
public String getTemporaryFileSuffix() {
|
||||
return temporaryFileSuffix;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the temporary file suffix
|
||||
* @param temporaryFileSuffix
|
||||
*/
|
||||
public void setTemporaryFileSuffix(String temporaryFileSuffix) {
|
||||
Assert.hasText(temporaryFileSuffix, "The temporary file suffix must not be null or empty");
|
||||
if(!temporaryFileSuffix.startsWith(".")) {
|
||||
temporaryFileSuffix = "." + temporaryFileSuffix;
|
||||
}
|
||||
|
||||
this.temporaryFileSuffix = temporaryFileSuffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the AWS endpoint to use for all the operations, by default if none is set then us-east-1 is
|
||||
* assumed.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public String getAwsEndpoint() {
|
||||
return awsEndpoint;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets the valid AWS endpoint to be used by the client to connect to appropriate
|
||||
* region to perform the operations
|
||||
*
|
||||
* @param awsEndpoint
|
||||
*/
|
||||
public void setAwsEndpoint(String awsEndpoint) {
|
||||
Assert.hasText(awsEndpoint, "Given AWS Endpoint has to be non null and non empty string");
|
||||
this.awsEndpoint = awsEndpoint;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The implemented afterPropertiesSet method
|
||||
*/
|
||||
public final void afterPropertiesSet() throws Exception {
|
||||
//TODO: protect by lock?
|
||||
init();
|
||||
}
|
||||
|
||||
/**
|
||||
* The subclass needs to override this method if it desires to perform any initializing
|
||||
* of the class
|
||||
*/
|
||||
protected void init() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the stream provided and writes the file to the temp location
|
||||
* @param in the Stream from which the data of the Object is to be read
|
||||
* @param objectName the name of the object that would be used to upload the file
|
||||
*/
|
||||
private File getTempFile(InputStream in,String bucketName,String objectName) {
|
||||
InputStream inStream;
|
||||
if(!(in instanceof BufferedInputStream) && !(in instanceof ByteArrayInputStream)) {
|
||||
inStream = new BufferedInputStream(in);
|
||||
}
|
||||
else {
|
||||
inStream = in;
|
||||
}
|
||||
String fileName;
|
||||
if(objectName.contains(PATH_SEPARATOR)) {
|
||||
String[] splits = objectName.split(PATH_SEPARATOR);
|
||||
fileName = splits[splits.length - 1];
|
||||
}
|
||||
else {
|
||||
fileName = objectName;
|
||||
}
|
||||
File temporaryDirectory = getTemporaryDirectory();
|
||||
String temporaryFileSuffix = getTemporaryFileSuffix();
|
||||
|
||||
String filePath = temporaryDirectory.getAbsoluteFile() + File.separator + fileName + temporaryFileSuffix;
|
||||
|
||||
if(logger.isDebugEnabled()) {
|
||||
logger.debug("Temporary file path is " + filePath);
|
||||
}
|
||||
|
||||
//Write data to temporary file
|
||||
File tempFile = new File(filePath);
|
||||
FileOutputStream fos = null;
|
||||
try {
|
||||
fos = new FileOutputStream(tempFile);
|
||||
byte[] bytes = new byte[1024];
|
||||
int read = 0;
|
||||
while(true) {
|
||||
read = inStream.read(bytes);
|
||||
if(read == -1) {
|
||||
break;
|
||||
}
|
||||
fos.write(bytes, 0, read);
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
throw new AmazonS3OperationException(credentials.getAccessKey(),
|
||||
bucketName,
|
||||
objectName,
|
||||
"Exception caught while writing the temporary file from input stream", e);
|
||||
} catch(IOException ioe) {
|
||||
throw new AmazonS3OperationException(credentials.getAccessKey(),
|
||||
bucketName,
|
||||
objectName,
|
||||
"Exception caught while reading from the provided input stream", ioe);
|
||||
} finally {
|
||||
if(fos != null) {
|
||||
try {
|
||||
fos.flush();
|
||||
fos.close();
|
||||
} catch (IOException e) {
|
||||
//just log
|
||||
logger.error("Unable to close the stream to the temp file being created", e);
|
||||
}
|
||||
try {
|
||||
in.close();
|
||||
} catch (IOException e) {
|
||||
//just log
|
||||
logger.error("Unable to close the input stream source", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return tempFile;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The implementation of the core common operations that would be performed
|
||||
* before and after the subclass does the actual work of putting the object to
|
||||
* the given bucket
|
||||
*
|
||||
* @param bucketName The bucket to which the object is to be put
|
||||
* @param folder The folder to which the object is to be uploaded
|
||||
* @param objectName The name of the object in the given bucket
|
||||
* @param s3Object The {@link AmazonS3Object} instance that represents the object
|
||||
* to be uploaded.
|
||||
*/
|
||||
@Override
|
||||
public final void putObject(String bucketName, String folder, String objectName,
|
||||
AmazonS3Object s3Object) {
|
||||
Assert.hasText(bucketName,"null or empty bucketName provided");
|
||||
Assert.hasText(objectName,"null or empty object name provided");
|
||||
Assert.notNull(s3Object, "null s3 object provided for upload");
|
||||
|
||||
|
||||
File file = s3Object.getFileSource();
|
||||
InputStream in = s3Object.getInputStream();
|
||||
Assert.isTrue(file != null ^ in != null,
|
||||
"Exactly one of file or inpuut stream needed in the provided s3 object");
|
||||
|
||||
boolean isTempFile = false;
|
||||
if(in != null) {
|
||||
//We don't know the source of the stream and hence we read the content
|
||||
//and write to the temporary file.
|
||||
file = getTempFile(in,bucketName,objectName);
|
||||
isTempFile = true;
|
||||
}
|
||||
|
||||
String key = getKeyFromFolder(folder, objectName);
|
||||
|
||||
//if the size of the file is greater than the threshold for multipart upload,
|
||||
//set the Content-MD5 header for this upload. This header will also come handy
|
||||
//later in inbound-channel-adapter where we cant find the MD5 sum of the
|
||||
//multipart upload file from its ETag
|
||||
|
||||
String stringContentMD5 = null;
|
||||
try {
|
||||
stringContentMD5 =
|
||||
encodeHex(getContentsMD5AsBytes(file));
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
logger.error("Exception while generating the content's MD5 of the file " + file.getAbsolutePath(), e);
|
||||
}
|
||||
|
||||
try {
|
||||
doPut(bucketName, key, file, s3Object.getObjectACL(),
|
||||
s3Object.getUserMetaData(), stringContentMD5);
|
||||
} catch (Exception e) {
|
||||
throw new AmazonS3OperationException(
|
||||
credentials.getAccessKey(), bucketName,
|
||||
key,
|
||||
"Encountered exception while putting an object, see root cause for more details",
|
||||
e);
|
||||
|
||||
}
|
||||
|
||||
if(isTempFile) {
|
||||
//Delete the temp file
|
||||
if(logger.isDebugEnabled()) {
|
||||
logger.debug("Deleting temp file: " + file.getName());
|
||||
}
|
||||
boolean deleteSuccessful = file.delete();
|
||||
if(!deleteSuccessful) {
|
||||
logger.warn("Unable to delete file '" + file.getName() + "'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The private method that takes the folder and the object name as the parameters and
|
||||
* constructs the key that would be for the item to be retrieved or put in the bucket
|
||||
*
|
||||
* @param folder
|
||||
* @param objectName
|
||||
* @return
|
||||
*/
|
||||
private String getKeyFromFolder(String folder, String objectName) {
|
||||
if(objectName.startsWith(PATH_SEPARATOR)) {
|
||||
//remove the leading / of the object name
|
||||
objectName = objectName.substring(1);
|
||||
}
|
||||
String key;
|
||||
if(folder != null) {
|
||||
key = folder.endsWith(PATH_SEPARATOR)?
|
||||
folder + objectName:folder + PATH_SEPARATOR + objectName;
|
||||
}
|
||||
else {
|
||||
key = objectName;
|
||||
}
|
||||
|
||||
//check if the foldername begins with a /, if yes, remove it as well as it created
|
||||
//one directory with blank name
|
||||
|
||||
if(key.startsWith(PATH_SEPARATOR)) {
|
||||
key = key.substring(1);
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the object with the given name from the provided bucket and folder.
|
||||
*
|
||||
*/
|
||||
public boolean removeObject(String bucketName, String folder,
|
||||
String objectName) {
|
||||
throw new UnsupportedOperationException("Operation not et supported");
|
||||
}
|
||||
|
||||
/**
|
||||
* List the objects in a given bucket in the given folder.
|
||||
*
|
||||
* @param bucketName The bucket in which we want to list the objects in
|
||||
* @param nextMarker The number of objects can be very large and this serves as the marker
|
||||
* for remembering the last record fetch in the last retrieve operation.
|
||||
* @param pageSize The max number of records to be retrieved in one list object operation.
|
||||
* @param prefix The prefix for the list operation, this can serve as the folder whose contents
|
||||
* are to be listed.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public final PaginatedObjectsView listObjects(String bucketName, String folder, String nextMarker,int pageSize) {
|
||||
Assert.hasText(bucketName, "Bucket name should be non null and non empty");
|
||||
Assert.isTrue(pageSize >= 0, "Page size should be a non negative number");
|
||||
|
||||
if(logger.isDebugEnabled()) {
|
||||
logger.debug("Listing objects from bucket " + bucketName + " and folder " + folder);
|
||||
logger.debug("Next marker is " + nextMarker + " and pageSize is " + pageSize);
|
||||
}
|
||||
|
||||
|
||||
String prefix = null;
|
||||
if(folder != null && !PATH_SEPARATOR.equals(folder)) {
|
||||
prefix = folder;
|
||||
}
|
||||
//check if the prefix begins with /
|
||||
if(prefix != null && prefix.startsWith(PATH_SEPARATOR)) {
|
||||
prefix = prefix.substring(1);
|
||||
}
|
||||
|
||||
return doListObjects(bucketName, nextMarker, pageSize, prefix);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the object from the given bucket, folder and the name.
|
||||
*
|
||||
* @param bucketName The bucket from which to retrieve the object
|
||||
* @param folder The folder name
|
||||
* @param objectName The name of the object to retrieve
|
||||
*/
|
||||
public final AmazonS3Object getObject(String bucketName, String folder,
|
||||
String objectName) {
|
||||
Assert.hasText(bucketName, "Bucket name should be non null and non empty");
|
||||
if(logger.isDebugEnabled()) {
|
||||
logger.debug("Getting from bucket " + bucketName +
|
||||
", from folder " + folder + " the object name " + objectName);
|
||||
}
|
||||
String key = getKeyFromFolder(folder, objectName);
|
||||
try {
|
||||
return doGetObject(bucketName, key);
|
||||
} catch (Exception e) {
|
||||
throw new AmazonS3OperationException(
|
||||
credentials.getAccessKey(), bucketName,
|
||||
key,
|
||||
"Encountered exception while putting an object, see root cause for more details",
|
||||
e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the object from the given bucket with the given key using the AWS SDK implementation
|
||||
*
|
||||
* @param bucketName
|
||||
* @param key
|
||||
* @return
|
||||
*/
|
||||
protected abstract AmazonS3Object doGetObject(String bucketName, String key);
|
||||
|
||||
|
||||
/**
|
||||
* The implementation should use the appropriate API to list objects from the given bucket
|
||||
*
|
||||
* @param bucketName The bucket in which we want to list the objects in
|
||||
* @param nextMarker The number of objects can be very large and this serves as the marker
|
||||
* for remembering the last record fetch in the last retrieve operation.
|
||||
* @param pageSize The max number of records to be retrieved in one list object operation.
|
||||
* @param prefix The prefix for the list operation, this can serve as the folder whose contents
|
||||
* are to be listed.
|
||||
* @return
|
||||
*/
|
||||
protected abstract PaginatedObjectsView doListObjects(String bucketName,
|
||||
String nextMarker, int pageSize, String prefix);
|
||||
|
||||
|
||||
/**
|
||||
* The abstract method to be implemented by the subclass that would be doing the job
|
||||
* of uploading the given file against the given key in the given bucket
|
||||
*
|
||||
* @param bucketName The bucket on S3 where this object is to be put
|
||||
* @param key The key against which this Object is to be stored in S3
|
||||
* @param file resource to be uploaded to S3
|
||||
* @param objectACL the Object's Access controls for the object to be uploaded
|
||||
* @param userMetadata The user's metadata to be associated with the object uploaded
|
||||
* @param The MD5 sum of the contents of the file to be uploaded
|
||||
*
|
||||
*/
|
||||
protected abstract void doPut(String bucket,String key,File file,
|
||||
AmazonS3ObjectACL objectACL, Map<String, String> userMetadata,String stringContentMD5);
|
||||
|
||||
}
|
||||
|
||||
class PagninatedObjectsViewImpl implements PaginatedObjectsView {
|
||||
|
||||
private final List<S3ObjectSummary> objectSummary;
|
||||
private final String nextMarker;
|
||||
|
||||
public PagninatedObjectsViewImpl(List<S3ObjectSummary> objectSummary,
|
||||
String nextMarker) {
|
||||
this.objectSummary = objectSummary;
|
||||
this.nextMarker = nextMarker;
|
||||
}
|
||||
|
||||
|
||||
public List<S3ObjectSummary> getObjectSummary() {
|
||||
return objectSummary != null?objectSummary:new ArrayList<S3ObjectSummary>();
|
||||
}
|
||||
|
||||
|
||||
public boolean hasMoreResults() {
|
||||
return nextMarker != null;
|
||||
}
|
||||
|
||||
|
||||
public String getNextMarker() {
|
||||
return nextMarker;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.integration.aws.s3.core;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.io.Serializable;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* The Amazon S3 Object representing the Object in S3
|
||||
*
|
||||
* @author Amol Nayak
|
||||
*
|
||||
* @since 0.5
|
||||
*
|
||||
*/
|
||||
public class AmazonS3Object implements Serializable {
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private static final long serialVersionUID = -832622119907619624L;
|
||||
|
||||
private Map<String, String> userMetaData;
|
||||
|
||||
private Map<String, Object> metaData;
|
||||
|
||||
private final InputStream inputStream;
|
||||
|
||||
private final File fileSource;
|
||||
|
||||
private final AmazonS3ObjectACL objectACL;
|
||||
|
||||
/**
|
||||
* The default constructor
|
||||
*
|
||||
* @param userMetaData
|
||||
* @param metaData
|
||||
* @param inputStream
|
||||
*/
|
||||
public AmazonS3Object(Map<String, String> userMetaData,
|
||||
Map<String, Object> metaData, InputStream inputStream,File fileSource,AmazonS3ObjectACL objectACL) {
|
||||
if(userMetaData != null)
|
||||
this.userMetaData = Collections.unmodifiableMap(userMetaData);
|
||||
|
||||
if(metaData != null)
|
||||
this.metaData = Collections.unmodifiableMap(metaData);
|
||||
|
||||
Assert.isTrue((inputStream == null) ^ (fileSource == null),
|
||||
"Exactly one of 'inputStream' or 'fileSource' must be provided");
|
||||
|
||||
this.inputStream = inputStream;
|
||||
this.fileSource = fileSource;
|
||||
this.objectACL = objectACL;
|
||||
}
|
||||
|
||||
/**
|
||||
* The constructor that delegates to {@link #AmazonS3Object(Map, Map, InputStream, File, AmazonS3ObjectACL)}
|
||||
* with null {@link AmazonS3ObjectACL}
|
||||
*
|
||||
* @param userMetaData
|
||||
* @param metaData
|
||||
* @param inputStream
|
||||
* @param fileSource
|
||||
*/
|
||||
public AmazonS3Object(Map<String, String> userMetaData,
|
||||
Map<String, Object> metaData, InputStream inputStream,File fileSource) {
|
||||
this(userMetaData,metaData,inputStream,fileSource,null);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the User Metadata associated with given Amazon S3 object
|
||||
* @return
|
||||
*/
|
||||
public Map<String, String> getUserMetaData() {
|
||||
return userMetaData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Metadata associated with the given Amazon S3 object
|
||||
* @return
|
||||
*/
|
||||
public Map<String, Object> getMetaData() {
|
||||
return metaData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link InputStream} to the resource
|
||||
* @return
|
||||
*/
|
||||
public InputStream getInputStream() {
|
||||
return inputStream;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the file source
|
||||
* @return
|
||||
*/
|
||||
public File getFileSource() {
|
||||
return fileSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Access controls associated with the S3 Object
|
||||
* @return
|
||||
*/
|
||||
public AmazonS3ObjectACL getObjectACL() {
|
||||
return objectACL;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.integration.aws.s3.core;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* The object containing the Amazon S3 Object's ACL. Access is used to control the access to
|
||||
* the resource in S3 bucket
|
||||
*
|
||||
* @author Amol Nayak
|
||||
*
|
||||
* @since 0.5
|
||||
*
|
||||
*/
|
||||
public class AmazonS3ObjectACL {
|
||||
|
||||
private Set<ObjectGrant> grants = new HashSet<ObjectGrant>();
|
||||
|
||||
/**
|
||||
* Gets all the grants on the object in the bucket
|
||||
* @return
|
||||
*/
|
||||
public Set<ObjectGrant> getGrants() {
|
||||
return grants;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the provided grants on the S3 object
|
||||
*
|
||||
* @param grants
|
||||
*/
|
||||
public void setGrants(Set<ObjectGrant> grants) {
|
||||
Assert.notNull(grants, "Provide non null 'grants'");
|
||||
this.grants = grants;
|
||||
}
|
||||
|
||||
/**
|
||||
* A convenience method that will be used to add grants to the objects.
|
||||
*
|
||||
* @param grant
|
||||
*/
|
||||
public void addGrant(ObjectGrant grant) {
|
||||
Assert.notNull(grant,"Provide non null object grant");
|
||||
grants.add(grant);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.integration.aws.s3.core;
|
||||
|
||||
import org.springframework.integration.aws.core.AWSOperationException;
|
||||
|
||||
/**
|
||||
* A subclass of {@link AmazonWSOperationException} which indicates a failure in performing
|
||||
* an operation on the object in S3.
|
||||
*
|
||||
* @author Amol Nayak
|
||||
*
|
||||
* @since 0.5
|
||||
*
|
||||
*/
|
||||
public class AmazonS3OperationException extends AWSOperationException {
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private static final long serialVersionUID = 9518185510906801L;
|
||||
private final String bucket;
|
||||
private final String objectName;
|
||||
|
||||
/**
|
||||
*The constructor that instantiates with the Account's access key the bucket name
|
||||
*and the object name
|
||||
*
|
||||
* @param accessKey
|
||||
* @param bucket
|
||||
* @param objectName
|
||||
*/
|
||||
public AmazonS3OperationException(String accessKey, String bucket,
|
||||
String objectName) {
|
||||
super(accessKey);
|
||||
this.bucket = bucket;
|
||||
this.objectName = objectName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* The constructor that instantiates with the Account's access key the bucket name
|
||||
* the object name, the exception message and the actual exception
|
||||
*
|
||||
* @param accessKey
|
||||
* @param bucket
|
||||
* @param objectName
|
||||
* @param message
|
||||
* @param cause
|
||||
*/
|
||||
public AmazonS3OperationException(String accessKey, String bucket,
|
||||
String objectName,String message,
|
||||
Throwable cause) {
|
||||
super(accessKey, message, cause);
|
||||
this.bucket = bucket;
|
||||
this.objectName = objectName;
|
||||
}
|
||||
|
||||
/**
|
||||
* The constructor that instantiates with the Account's access key the bucket name
|
||||
* the object name, the exception message
|
||||
*
|
||||
* @param accessKey
|
||||
* @param bucket
|
||||
* @param objectName
|
||||
* @param message
|
||||
*/
|
||||
public AmazonS3OperationException(String accessKey, String bucket,
|
||||
String objectName,String message) {
|
||||
super(accessKey, message);
|
||||
this.bucket = bucket;
|
||||
this.objectName = objectName;
|
||||
}
|
||||
|
||||
/**
|
||||
* The constructor that instantiates with the Account's access key the bucket name
|
||||
* the object name, the root cause
|
||||
*
|
||||
* @param accessKey
|
||||
* @param bucket
|
||||
* @param objectName
|
||||
* @param cause
|
||||
*/
|
||||
public AmazonS3OperationException(String accessKey, String bucket,
|
||||
String objectName,Throwable cause) {
|
||||
super(accessKey, cause);
|
||||
this.bucket = bucket;
|
||||
this.objectName = objectName;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the bucket name for which an S3 operation failed
|
||||
* @return
|
||||
*/
|
||||
public String getBucket() {
|
||||
return bucket;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the object name where an S3 operation failed
|
||||
* @return
|
||||
*/
|
||||
public String getObjectName() {
|
||||
return objectName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.integration.aws.s3.core;
|
||||
|
||||
/**
|
||||
* The Core interface for performing various operations on Amazon S3 like listing objects
|
||||
* in the bucket, get an object, put an object and remove an object
|
||||
*
|
||||
* @author Amol Nayak
|
||||
*
|
||||
* @since 0.5
|
||||
*
|
||||
*/
|
||||
public interface AmazonS3Operations {
|
||||
|
||||
public static final String CONTENT_MD5_HEADER = "Content-MD5";
|
||||
|
||||
/**
|
||||
* Lists Objects in the given bucket and given folder. Provide / if you
|
||||
* wish to list objects at the root of the bucket
|
||||
*
|
||||
* @param bucketName
|
||||
* @param folder
|
||||
* @param nextMarker
|
||||
* @param pageSize
|
||||
* @return the {@link PaginatedObjectsView} of the matching result
|
||||
*/
|
||||
PaginatedObjectsView listObjects(String bucketName,String folder,String nextMarker,int pageSize);
|
||||
|
||||
/**
|
||||
* Put the given {@link AmazonS3Object} in the provided bucket in the folder specified with the name given
|
||||
* The object if exists, will be overwritten and the folder path hierarchy
|
||||
* if absent will be created
|
||||
*
|
||||
* @param bucketName
|
||||
* @param folder
|
||||
* @param objectName
|
||||
* @param s3Object
|
||||
*/
|
||||
void putObject(String bucketName,String folder,String objectName,AmazonS3Object s3Object);
|
||||
|
||||
/**
|
||||
* Gets the Object from Amazon S3 from the specified bucket,folder and with
|
||||
* the given objectName
|
||||
*
|
||||
* @param bucketName
|
||||
* @param folder
|
||||
* @param objectName
|
||||
* @return The S3 object corresponding to the given details. Null if no object found
|
||||
*/
|
||||
AmazonS3Object getObject(String bucketName,String folder,String objectName);
|
||||
|
||||
/**
|
||||
* Removes the specified object from the bucket given, folder specified
|
||||
* and the given object name from S3
|
||||
* @param bucketName
|
||||
* @param folder
|
||||
* @param objectName
|
||||
* @return true if the object was successfully removed else false
|
||||
*/
|
||||
boolean removeObject(String bucketName,String folder,String objectName);
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.integration.aws.s3.core;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
|
||||
import org.springframework.integration.aws.core.AWSCredentials;
|
||||
import org.springframework.integration.aws.core.AbstractAWSClientFactory;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import com.amazonaws.auth.BasicAWSCredentials;
|
||||
import com.amazonaws.services.s3.AmazonS3Client;
|
||||
import com.amazonaws.services.s3.model.AccessControlList;
|
||||
import com.amazonaws.services.s3.model.AmazonS3Exception;
|
||||
import com.amazonaws.services.s3.model.CanonicalGrantee;
|
||||
import com.amazonaws.services.s3.model.EmailAddressGrantee;
|
||||
import com.amazonaws.services.s3.model.GetObjectRequest;
|
||||
import com.amazonaws.services.s3.model.GroupGrantee;
|
||||
import com.amazonaws.services.s3.model.ListObjectsRequest;
|
||||
import com.amazonaws.services.s3.model.ObjectListing;
|
||||
import com.amazonaws.services.s3.model.ObjectMetadata;
|
||||
import com.amazonaws.services.s3.model.Permission;
|
||||
import com.amazonaws.services.s3.model.PutObjectRequest;
|
||||
import com.amazonaws.services.s3.model.S3Object;
|
||||
import com.amazonaws.services.s3.transfer.TransferManager;
|
||||
import com.amazonaws.services.s3.transfer.TransferManagerConfiguration;
|
||||
import com.amazonaws.services.s3.transfer.Upload;
|
||||
|
||||
|
||||
/**
|
||||
* The default, out of the box implementation of the {@link AmazonS3Operations} that is implemented
|
||||
* using AWS SDK.
|
||||
*
|
||||
* @author Amol Nayak
|
||||
*
|
||||
* @since 0.5
|
||||
*
|
||||
*/
|
||||
public class DefaultAmazonS3Operations extends AbstractAmazonS3Operations {
|
||||
|
||||
private final AWSCredentials credentials;
|
||||
|
||||
private AmazonS3Client client;
|
||||
|
||||
private volatile TransferManager transferManager; //Used to upload to S3
|
||||
|
||||
private volatile ThreadPoolExecutor threadPoolExecutor;
|
||||
|
||||
private volatile AbstractAWSClientFactory<AmazonS3Client> s3Factory;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param credentials
|
||||
*/
|
||||
public DefaultAmazonS3Operations(final AWSCredentials credentials) {
|
||||
super(credentials);
|
||||
this.credentials = credentials;
|
||||
s3Factory = new AbstractAWSClientFactory<AmazonS3Client>() {
|
||||
@Override
|
||||
protected AmazonS3Client getClientImplementation() {
|
||||
String accessKey = credentials.getAccessKey();
|
||||
String secretKey = credentials.getSecretKey();
|
||||
BasicAWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);
|
||||
return new AmazonS3Client(credentials);
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void init() {
|
||||
|
||||
client = s3Factory.getClient(getAwsEndpoint());
|
||||
|
||||
if(threadPoolExecutor == null) {
|
||||
//Will use the Default Executor,
|
||||
//See com.amazonaws.services.s3.transfer.internal.TransferManagerUtils for more details
|
||||
transferManager = new TransferManager(client);
|
||||
}
|
||||
else {
|
||||
transferManager = new TransferManager(client, threadPoolExecutor);
|
||||
}
|
||||
|
||||
//As per amazon it is recommended to use Multi part upload above 100 MB
|
||||
long multipartUploadThreshold = getMultipartUploadThreshold();
|
||||
if(multipartUploadThreshold > 0) {
|
||||
TransferManagerConfiguration config = new TransferManagerConfiguration();
|
||||
if(multipartUploadThreshold > Integer.MAX_VALUE) {
|
||||
config.setMultipartUploadThreshold(Integer.MAX_VALUE); //2GB
|
||||
}
|
||||
else {
|
||||
config.setMultipartUploadThreshold((int)multipartUploadThreshold);
|
||||
}
|
||||
transferManager.setConfiguration(config);
|
||||
}
|
||||
//If none is set, we use the default
|
||||
}
|
||||
|
||||
/**
|
||||
* The implementation that uses the AWS SDK to list objects from the given bucket
|
||||
*
|
||||
* @param bucketName The bucket in which we want to list the objects in
|
||||
* @param nextMarker The number of objects can be very large and this serves as the marker
|
||||
* for remembering the last record fetch in the last retrieve operation.
|
||||
* @param pageSize The max number of records to be retrieved in one list object operation.
|
||||
* @param prefix The prefix for the list operation, this can serve as the folder whose contents
|
||||
* are to be listed.
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
protected PaginatedObjectsView doListObjects(String bucketName,
|
||||
String nextMarker, int pageSize, String prefix) {
|
||||
|
||||
ListObjectsRequest listObjectsRequest =
|
||||
new ListObjectsRequest()
|
||||
.withBucketName(bucketName)
|
||||
.withPrefix(prefix)
|
||||
.withMarker(nextMarker);
|
||||
|
||||
if(pageSize > 0) {
|
||||
listObjectsRequest.withMaxKeys(pageSize);
|
||||
}
|
||||
|
||||
ObjectListing listing = client.listObjects(listObjectsRequest);
|
||||
PaginatedObjectsView view = null;
|
||||
List<com.amazonaws.services.s3.model.S3ObjectSummary> summaries = listing.getObjectSummaries();
|
||||
if(summaries != null && !summaries.isEmpty()) {
|
||||
List<S3ObjectSummary> objectSummaries = new ArrayList<S3ObjectSummary>();
|
||||
for(final com.amazonaws.services.s3.model.S3ObjectSummary summary:summaries) {
|
||||
S3ObjectSummary summ = new S3ObjectSummary() {
|
||||
|
||||
public long getSize() {
|
||||
return summary.getSize();
|
||||
}
|
||||
|
||||
public Date getLastModified() {
|
||||
return summary.getLastModified();
|
||||
}
|
||||
|
||||
public String getKey() {
|
||||
return summary.getKey();
|
||||
}
|
||||
|
||||
public String getETag() {
|
||||
return summary.getETag();
|
||||
}
|
||||
|
||||
public String getBucketName() {
|
||||
return summary.getBucketName();
|
||||
}
|
||||
};
|
||||
objectSummaries.add(summ);
|
||||
}
|
||||
view = new PagninatedObjectsViewImpl(objectSummaries,listing.getNextMarker());
|
||||
}
|
||||
return view;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the object from the given bucket with the given key using the AWS SDK implementation
|
||||
*
|
||||
* @param bucketName
|
||||
* @param key
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
protected AmazonS3Object doGetObject(String bucketName, String key) {
|
||||
GetObjectRequest request = new GetObjectRequest(bucketName, key);
|
||||
S3Object s3Object;
|
||||
try {
|
||||
s3Object = client.getObject(request);
|
||||
} catch (AmazonS3Exception e) {
|
||||
if("NoSuchKey".equals(e.getErrorCode())) {
|
||||
//If the key is not found, return null rather than throwing the exception
|
||||
return null;
|
||||
}
|
||||
else {
|
||||
//throw the exception to caller in all other cases
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return new AmazonS3Object(s3Object.getObjectMetadata().getUserMetadata(),
|
||||
s3Object.getObjectMetadata().getRawMetadata(),
|
||||
s3Object.getObjectContent(),
|
||||
null);
|
||||
}
|
||||
|
||||
/**
|
||||
* The implementation puts the given {@link File} instance to the provided bucket against
|
||||
* the given key.
|
||||
*
|
||||
* @param bucketName The bucket on S3 where this object is to be put
|
||||
* @param key The key against which this Object is to be stored in S3
|
||||
* @param file resource to be uploaded to S3
|
||||
* @param objectACL the Object's Access controls for the object to be uploaded
|
||||
* @param userMetadata The user's metadata to be associated with the object uploaded
|
||||
* @param The MD5 sum of the contents of the file to be uploaded
|
||||
*/
|
||||
@Override
|
||||
public void doPut(String bucketName, String key, File file, AmazonS3ObjectACL objectACL,
|
||||
Map<String, String> userMetadata,String stringContentMD5) {
|
||||
|
||||
ObjectMetadata metadata = new ObjectMetadata();
|
||||
PutObjectRequest request = new PutObjectRequest(bucketName, key, file);
|
||||
|
||||
request.withMetadata(metadata);
|
||||
|
||||
if(stringContentMD5 != null) {
|
||||
metadata.setContentMD5(stringContentMD5);
|
||||
}
|
||||
|
||||
if(userMetadata != null) {
|
||||
metadata.setUserMetadata(userMetadata);
|
||||
}
|
||||
|
||||
Upload upload;
|
||||
try {
|
||||
upload = transferManager.upload(request);
|
||||
} catch (Exception e) {
|
||||
throw new AmazonS3OperationException(
|
||||
credentials.getAccessKey(), bucketName,
|
||||
key,
|
||||
"Encountered Exception while invoking upload on multipart/single thread file, " +
|
||||
"see nested exceptions for more details",
|
||||
e);
|
||||
}
|
||||
//Wait till the upload completes, the call to putObject is synchronous
|
||||
try {
|
||||
if(logger.isInfoEnabled()) {
|
||||
logger.info("Waiting for Upload to complete");
|
||||
}
|
||||
upload.waitForCompletion();
|
||||
if(logger.isInfoEnabled()) {
|
||||
logger.info("Upload completed");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new AmazonS3OperationException(
|
||||
credentials.getAccessKey(), bucketName,
|
||||
key,
|
||||
"Encountered Exception while uploading the multipart/single thread file, " +
|
||||
"see nested exceptions for more details",
|
||||
e);
|
||||
}
|
||||
//Now since the object is present on S3, set the AccessControl list on it
|
||||
//Please note that it is not possible to set the object ACL with the
|
||||
//put object request, and hence both these operations cannot be atomic
|
||||
//it is possible the objects is uploaded and the ACl not set due to some
|
||||
//failure
|
||||
|
||||
if(objectACL != null) {
|
||||
if(logger.isInfoEnabled()) {
|
||||
logger.info("Setting Access control list for key " + key);
|
||||
}
|
||||
try {
|
||||
client.setObjectAcl(bucketName, key,
|
||||
getAccessControlList(bucketName, key, objectACL));
|
||||
} catch (Exception e) {
|
||||
throw new AmazonS3OperationException(
|
||||
credentials.getAccessKey(), bucketName,
|
||||
key,
|
||||
"Encountered Exception while setting the Object ACL for key , " + key +
|
||||
"see nested exceptions for more details",
|
||||
e);
|
||||
}
|
||||
if(logger.isDebugEnabled()) {
|
||||
logger.debug("Successfully set the object ACL");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link AccessControlList} from the given {@link AmazonS3ObjectACL}
|
||||
* @param acl
|
||||
* @return
|
||||
*/
|
||||
private AccessControlList getAccessControlList(String bucketName,String key,AmazonS3ObjectACL acl) {
|
||||
AccessControlList accessControlList = null;
|
||||
if(acl != null) {
|
||||
if(!acl.getGrants().isEmpty()) {
|
||||
accessControlList = client.getObjectAcl(bucketName, key);
|
||||
for(ObjectGrant objGrant:acl.getGrants()) {
|
||||
Grantee grantee = objGrant.getGrantee();
|
||||
com.amazonaws.services.s3.model.Grantee awsGrantee;
|
||||
if(grantee.getGranteeType() == GranteeType.CANONICAL_GRANTEE_TYPE) {
|
||||
awsGrantee = new CanonicalGrantee(grantee.getIdentifier());
|
||||
}
|
||||
else if(grantee.getGranteeType() == GranteeType.EMAIL_GRANTEE_TYPE) {
|
||||
awsGrantee = new EmailAddressGrantee(grantee.getIdentifier());
|
||||
}
|
||||
else {
|
||||
awsGrantee = GroupGrantee.parseGroupGrantee(grantee.getIdentifier());
|
||||
if(awsGrantee == null) {
|
||||
logger.warn("Group grantee with identifier: \"" + grantee.getIdentifier() + "\" not found. skipping this grant");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
ObjectPermissions perm = objGrant.getPermission();
|
||||
Permission permission;
|
||||
if(perm == ObjectPermissions.READ) {
|
||||
permission = Permission.Read;
|
||||
}
|
||||
else if(perm == ObjectPermissions.READ_ACP) {
|
||||
permission = Permission.ReadAcp;
|
||||
}
|
||||
else
|
||||
permission = Permission.WriteAcp;
|
||||
|
||||
accessControlList.grantPermission(awsGrantee, permission);
|
||||
}
|
||||
}
|
||||
}
|
||||
return accessControlList;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the thread pool executor that will be used to upload the object in multiparts
|
||||
* concurrently
|
||||
* @return
|
||||
*/
|
||||
public ThreadPoolExecutor getThreadPoolExecutor() {
|
||||
return threadPoolExecutor;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Used only when we upload the data using multi part upload. The thread pool will be used
|
||||
* to upload the data concurrently
|
||||
*
|
||||
* @param threadPool
|
||||
*/
|
||||
public void setThreadPoolExecutor(ThreadPoolExecutor threadPoolExecutor) {
|
||||
Assert.notNull(threadPoolExecutor, "'threadPoolExecutor' is null");
|
||||
this.threadPoolExecutor = threadPoolExecutor;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.integration.aws.s3.core;
|
||||
|
||||
/**
|
||||
* Indicates the Grantee who is being given the Access to a particular resource on S3
|
||||
*
|
||||
* @author Amol Nayak
|
||||
*
|
||||
* @since 0.5
|
||||
*
|
||||
*/
|
||||
public class Grantee {
|
||||
|
||||
private String identifier;
|
||||
private GranteeType granteeType;
|
||||
|
||||
public Grantee() {
|
||||
|
||||
}
|
||||
|
||||
public Grantee(String identifier, GranteeType granteeType) {
|
||||
this.identifier = identifier;
|
||||
this.granteeType = granteeType;
|
||||
}
|
||||
|
||||
/**
|
||||
* The identifier of a particular type identifying the grantee
|
||||
* @param id
|
||||
*/
|
||||
void setIdentifier(String identifier) {
|
||||
this.identifier = identifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the particular identifier representing the grantee
|
||||
* @return
|
||||
*/
|
||||
public String getIdentifier() {
|
||||
return identifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Particular grantee Type
|
||||
* @return
|
||||
*/
|
||||
public GranteeType getGranteeType() {
|
||||
return granteeType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Type of the grantee see {@link GranteeType} for more information
|
||||
* @param granteeType
|
||||
*/
|
||||
public void setGranteeType(GranteeType granteeType) {
|
||||
this.granteeType = granteeType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result
|
||||
+ ((granteeType == null) ? 0 : granteeType.hashCode());
|
||||
result = prime * result
|
||||
+ ((identifier == null) ? 0 : identifier.hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj)
|
||||
return true;
|
||||
if (obj == null)
|
||||
return false;
|
||||
if (getClass() != obj.getClass())
|
||||
return false;
|
||||
Grantee other = (Grantee) obj;
|
||||
if (granteeType != other.granteeType)
|
||||
return false;
|
||||
if (identifier == null) {
|
||||
if (other.identifier != null)
|
||||
return false;
|
||||
} else if (!identifier.equals(other.identifier))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.integration.aws.s3.core;
|
||||
|
||||
/**
|
||||
* Identifies the type of the grantee. E.g. A grantee can be identified using the canonical
|
||||
* identifier or the email id.
|
||||
*
|
||||
* @author Amol Nayak
|
||||
*
|
||||
* @since 0.5
|
||||
*
|
||||
*/
|
||||
public enum GranteeType {
|
||||
|
||||
/**
|
||||
* This represents the Canonical id of the users AWS account
|
||||
*/
|
||||
CANONICAL_GRANTEE_TYPE,
|
||||
|
||||
/**
|
||||
* This email is usually resolved to the canonical id of the user.
|
||||
* This would fail and an error would be thrown if more than 2 accounts are
|
||||
* related to the user's email account.
|
||||
*/
|
||||
EMAIL_GRANTEE_TYPE,
|
||||
|
||||
/**
|
||||
* These represent come constants representing some predefined groups by Amazon S3
|
||||
*/
|
||||
GROUP_GRANTEE_TYPE;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.integration.aws.s3.core;
|
||||
|
||||
/**
|
||||
* Various types of Groups who can be granted permissions on amazon S3 objects
|
||||
*
|
||||
* @author Amol Nayak
|
||||
*
|
||||
* @since 0.5
|
||||
*
|
||||
*/
|
||||
public enum GroupGranteeType {
|
||||
|
||||
/**
|
||||
* To grants anonymous access to all objects in the bucket
|
||||
*/
|
||||
AllUsers("http://acs.amazonaws.com/groups/global/AllUsers"),
|
||||
/**
|
||||
* To grant access to all authenticated users of AWS who is logged in using
|
||||
* their AWS credentials
|
||||
*/
|
||||
AuthenticatedUsers("http://acs.amazonaws.com/groups/global/AuthenticatedUsers");
|
||||
|
||||
private String identifier;
|
||||
|
||||
private GroupGranteeType(String identifier) {
|
||||
this.identifier = identifier;
|
||||
}
|
||||
|
||||
public String getIdentifier() {
|
||||
return identifier;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.integration.aws.s3.core;
|
||||
|
||||
/**
|
||||
* Represent one Grant for a Grantee and the associated permissions
|
||||
*
|
||||
* @author Amol Nayak
|
||||
*
|
||||
* @since 0.5
|
||||
*
|
||||
*/
|
||||
public class ObjectGrant {
|
||||
|
||||
private final Grantee grantee;
|
||||
private final ObjectPermissions permission;
|
||||
|
||||
|
||||
/**
|
||||
* Instantiate an Object grant for the given grantee and with given permissions
|
||||
* @param grantee
|
||||
* @param permission
|
||||
*/
|
||||
public ObjectGrant(Grantee grantee, ObjectPermissions permission) {
|
||||
super();
|
||||
this.grantee = grantee;
|
||||
this.permission = permission;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the grantee for this particular object permission
|
||||
* @return
|
||||
*/
|
||||
public Grantee getGrantee() {
|
||||
return grantee;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Corresponding object permission
|
||||
* @return
|
||||
*/
|
||||
public ObjectPermissions getPermission() {
|
||||
return permission;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + ((grantee == null) ? 0 : grantee.hashCode());
|
||||
result = prime * result
|
||||
+ ((permission == null) ? 0 : permission.hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj)
|
||||
return true;
|
||||
if (obj == null)
|
||||
return false;
|
||||
if (getClass() != obj.getClass())
|
||||
return false;
|
||||
ObjectGrant other = (ObjectGrant) obj;
|
||||
if (grantee == null) {
|
||||
if (other.grantee != null)
|
||||
return false;
|
||||
} else if (!grantee.equals(other.grantee))
|
||||
return false;
|
||||
if (permission != other.permission)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.integration.aws.s3.core;
|
||||
|
||||
/**
|
||||
* Represents the various types of permissions on the object in S3
|
||||
*
|
||||
* @author Amol Nayak
|
||||
*
|
||||
* @since 0.5
|
||||
*
|
||||
*/
|
||||
public enum ObjectPermissions {
|
||||
|
||||
/**
|
||||
* Indicates the grantee has permissions to read the object from the containing bucket
|
||||
*/
|
||||
READ,
|
||||
|
||||
/**
|
||||
* Indicates the grantee has permissions to read the Access control permissions of the
|
||||
* Object in S3
|
||||
*/
|
||||
READ_ACP,
|
||||
|
||||
/**
|
||||
* Indicates the grantee has permissions to write the Access control permissions of the
|
||||
* Object in S3
|
||||
*/
|
||||
WRITE_ACP;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.integration.aws.s3.core;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Returns the Paginated view of the objects in Amazon S3 for the queries bucket
|
||||
* See {@link AmazonS3Operations} for more details on various operations on S3
|
||||
*
|
||||
* @author Amol Nayak
|
||||
*
|
||||
* @since 0.5
|
||||
*
|
||||
*/
|
||||
public interface PaginatedObjectsView {
|
||||
|
||||
/**
|
||||
* Gets the Paginated List of Object names
|
||||
* @return: A {@link List} of paginated object names
|
||||
*/
|
||||
List<S3ObjectSummary> getObjectSummary();
|
||||
|
||||
/**
|
||||
* Invoke this method to know if more pages of results is present
|
||||
* @return true if more results are present in the {@link List} of objects returned
|
||||
*/
|
||||
boolean hasMoreResults();
|
||||
|
||||
/**
|
||||
* Contains the merker that can be used to get the next listing of objects from the
|
||||
* S3. Contains a null value if the listing is complete, the hasMoreResults
|
||||
* method will return true if this marker contains a non null value.
|
||||
* @return
|
||||
*/
|
||||
String getNextMarker();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.integration.aws.s3.core;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* The summary of the Object stored on Amazon S3.
|
||||
*
|
||||
* @author Amol Nayak
|
||||
*
|
||||
* @since 0.5
|
||||
*
|
||||
*/
|
||||
public interface S3ObjectSummary {
|
||||
|
||||
/**
|
||||
* Gets the Bucket nane in which the object is kept on S3
|
||||
* @return
|
||||
*/
|
||||
String getBucketName();
|
||||
|
||||
/**
|
||||
* Gets the keys under which the Object is stored on S3
|
||||
* @return
|
||||
*/
|
||||
String getKey();
|
||||
|
||||
/**
|
||||
* Gets the Hex encoded 128 bit MD5 digest of the contents of the object uploaded on S3
|
||||
* @return
|
||||
*/
|
||||
String getETag();
|
||||
|
||||
/**
|
||||
* Gets the size of the object in bytes
|
||||
* @return
|
||||
*/
|
||||
long getSize();
|
||||
|
||||
/**
|
||||
* Gets the Date the object was last modified
|
||||
* @return
|
||||
*/
|
||||
Date getLastModified();
|
||||
}
|
||||
@@ -104,4 +104,288 @@
|
||||
</xsd:attribute>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="s3-outbound-channel-adapter">
|
||||
<xsd:complexType>
|
||||
<xsd:annotation>
|
||||
<xsd:documentation>
|
||||
Defines an outbound S3 Channel Adapter for Uploading files to Amazon S3
|
||||
</xsd:documentation>
|
||||
</xsd:annotation>
|
||||
<xsd:attribute name="id" type="xsd:string"/>
|
||||
<xsd:attribute name="channel" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:appinfo>
|
||||
<tool:annotation kind="ref">
|
||||
<tool:expected-type type="org.springframework.integration.core.MessageChannel"/>
|
||||
</tool:annotation>
|
||||
</xsd:appinfo>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attributeGroup ref="awsAdaptersCommonAttributes"/>
|
||||
<xsd:attribute name="bucket" type="xsd:string" use="required">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation>
|
||||
The mandatory attribute that would be used to provide the AWS bucket to which
|
||||
the objects needs to be uploaded.
|
||||
</xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="charset" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation>
|
||||
Relevant only when the payload of the message to the outbound adapter is of
|
||||
type java.lang.String. The default charset is UTF-8.
|
||||
</xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="multipart-upload-threshold" type="xsd:integer">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation>
|
||||
Using 'Amazon Multipart Upload' you can upload data as a set of parts using
|
||||
parallel threads. This non-negative integer value representing bytes which is used
|
||||
to provide the threshold after which the upload to the S3 bucket will be done
|
||||
using 'Amazon Multipart Upload'. Amazon recommends the size to be 100 MB.
|
||||
The minimum threshold for 'Amazon Multipart Upload' is 5120 bytes.
|
||||
If the attribute is not specified, then the value used is the default value used
|
||||
by the underlying implementation. The default implementation uses AWS SDK which
|
||||
uses Multi part upload after 16 MB. The maximum value for this is 2 GB. Any value
|
||||
greater than 2 GB will not throw an exception but the value will be set to
|
||||
2 GB internally for the threshold.
|
||||
</xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="temporary-directory" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation>
|
||||
If the payload of the message is an InputStream, byte[] or String
|
||||
the contents are written to a temporary file in the provided temporary directory
|
||||
location before being uploaded to the S3. In absence of this attribute, the
|
||||
value is defaulted to the value of the system property "java.io.tmpdir"
|
||||
</xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="temporary-suffix" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation>
|
||||
The suffix for the files if a temporary file is to be generated. The value
|
||||
defaults to ".writing"
|
||||
</xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="thread-pool-executor" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:appinfo>
|
||||
<tool:annotation kind="ref">
|
||||
<tool:expected-type type="java.util.concurrent.ThreadPoolExecutor"/>
|
||||
</tool:annotation>
|
||||
<xsd:documentation>
|
||||
The thread pool executor to be used for multi part uploads.
|
||||
If none is provided, the default one used by the underlying SDK
|
||||
or library will be used.
|
||||
</xsd:documentation>
|
||||
</xsd:appinfo>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="remote-directory" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation>
|
||||
The String literal that gives the remote folder in the provided bucket where
|
||||
the files will be uploaded. This attribute is mutually exclusive to
|
||||
remote-directory-expression
|
||||
</xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="remote-directory-expression" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation>
|
||||
This attribute is mutually exclusive with the remote-directory attribute
|
||||
and is used to provide an expression that would be evaluated against the incoming
|
||||
message to derive the remote directory name in the given bucket.
|
||||
</xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="file-name-generator" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:appinfo>
|
||||
<tool:annotation kind="ref">
|
||||
<tool:expected-type type="org.springframework.integration.aws.s3.FileNameGenerationStrategy"/>
|
||||
</tool:annotation>
|
||||
</xsd:appinfo>
|
||||
<xsd:documentation>
|
||||
The instance that would be used to generate the name of the file that would be
|
||||
stored in S3. If none is specified then
|
||||
org.springframework.integration.aws.s3.DefaultFileNameGenerationStrategy would be used.
|
||||
</xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="file-name-generation-expression" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation>
|
||||
The filename generation expression that is mutually exclusive to the
|
||||
file-name-generator attribute. The default expression is "headers['file_name']"
|
||||
</xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
|
||||
<xsd:attribute name="auto-startup" type="xsd:string" default="true"/>
|
||||
<xsd:attribute name="order">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation>
|
||||
Specifies the order for invocation when this endpoint is connected as a
|
||||
subscriber to a SubscribableChannel.
|
||||
</xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
|
||||
<xsd:attribute name="s3-operations" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:appinfo>
|
||||
<tool:expected-type type="org.springframework.integration.aws.s3.core.AmazonS3Operations"/>
|
||||
</xsd:appinfo>
|
||||
<xsd:documentation>
|
||||
Reference to the bean with an implementation of org.springframework.integration.aws.s3.core.AmazonS3Operations
|
||||
that would be used to perform the operations on the S3 bucket. If not provided, the
|
||||
default implementation used is
|
||||
org.springframework.integration.aws.s3.core.DefaultAmazonS3Operations which is the
|
||||
implementation using the AWS SDK.
|
||||
</xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
|
||||
<xsd:attribute name="aws-endpoint" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation>
|
||||
The String that gives the endpoint to use for the adapter, if none is
|
||||
specified the default used is s3.amazonaws.com.
|
||||
</xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="s3-inbound-channel-adapter">
|
||||
<xsd:complexType>
|
||||
<xsd:annotation>
|
||||
<xsd:documentation>
|
||||
Defines the inbound channel adapter for Amazon S3. The component is used to synchronize
|
||||
the objects in an S3 bucket with the file system.
|
||||
</xsd:documentation>
|
||||
</xsd:annotation>
|
||||
<xsd:sequence minOccurs="0" maxOccurs="1">
|
||||
<xsd:element ref="integration:poller"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="id" type="xsd:string"/>
|
||||
<xsd:attribute name="channel" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:appinfo>
|
||||
<tool:annotation kind="ref">
|
||||
<tool:expected-type type="org.springframework.integration.core.MessageChannel"/>
|
||||
</tool:annotation>
|
||||
</xsd:appinfo>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attributeGroup ref="awsAdaptersCommonAttributes"/>
|
||||
<xsd:attribute name="bucket" type="xsd:string" use="required">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation>
|
||||
The mandatory attribute that would be used to provide the AWS bucket to which
|
||||
the objects needs to be uploaded.
|
||||
</xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="temporary-suffix" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation>
|
||||
The suffix files will use while the are being written to the local file system.
|
||||
A file present with this suffix on the local file system denotes that the file
|
||||
is not completely received from the s3 bucket.
|
||||
</xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="s3-operations" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:appinfo>
|
||||
<tool:expected-type type="org.springframework.integration.aws.s3.core.AmazonS3Operations"/>
|
||||
</xsd:appinfo>
|
||||
<xsd:documentation>
|
||||
Reference to the bean with an implementation of org.springframework.integration.aws.s3.core.AmazonS3Operations
|
||||
that would be used to perform the operations on the S3 bucket. If not provided, the
|
||||
default implementation used is
|
||||
org.springframework.integration.aws.s3.core.DefaultAmazonS3Operations which uses AWS SDK.
|
||||
</xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="aws-endpoint" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation>
|
||||
The String that gives the endpoint to use for the adapter, if none is
|
||||
specified the default used is s3.amazonaws.com.
|
||||
</xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="remote-directory" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation>
|
||||
This is the sub folder if any on the remote bucket that would be synchronized
|
||||
with the local directory. Useful if a part of the bucket is to be synchronized.
|
||||
If none specified, the entire bucket will be synchronized with the local directory.
|
||||
</xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="local-directory" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation>
|
||||
The attribute specifying the directory on the local file system where
|
||||
the objects from S3 bucket would be synchronized to. Either of local-directory
|
||||
or the local-directory-expression are mandatory.
|
||||
</xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="local-directory-expression" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation>
|
||||
The attribute specifying the expression to find the directory on the local file
|
||||
system where the objects from S3 bucket would be synchronized to. Either of local-directory
|
||||
or the locl-directory-expression are mandatory.
|
||||
</xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="auto-startup" type="xsd:string" default="true"/>
|
||||
<xsd:attribute name="max-objects-per-batch" type="xsd:integer">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation>
|
||||
The maximum number of objects returned in the listOperation performed on the S3 bucket
|
||||
The default value used internally is 100. That is not more than 100 objects would be returned
|
||||
in one call to list the objects, subsequent calls will be made to the Web service to retrieve the next
|
||||
batch of objects.
|
||||
</xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="accept-sub-folders" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation>
|
||||
The boolean value that would be used to specify if the sub folders of the given
|
||||
folder are to be synchronized or not. By default the value is false and only files
|
||||
at the level of the specified folder (or root of the bucket if no remote folder specified)
|
||||
are synchronized. The objects in the sub folder are ignored.
|
||||
</xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="file-name-wildcard" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation>
|
||||
The wildcard pattern that would be used to further filter out the objects listed.
|
||||
This attribute is mutually exclusive to the file-name-regex attribute.
|
||||
</xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="file-name-regex" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation>
|
||||
The regex that would be used to further filter out the objects listed.
|
||||
This attribute is mutually exclusive to the file-name-wildcard attribute.
|
||||
</xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.integration.aws.common;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import junit.framework.Assert;
|
||||
|
||||
import org.apache.commons.codec.binary.Base64;
|
||||
import org.springframework.integration.aws.core.AWSCredentials;
|
||||
import org.springframework.integration.aws.core.PropertiesAWSCredentials;
|
||||
|
||||
/**
|
||||
* Common test class utility to be used by AmazonWS test cases
|
||||
*
|
||||
* @author Amol Nayak
|
||||
*
|
||||
* @since 0.5
|
||||
*
|
||||
*/
|
||||
public final class AWSTestUtils {
|
||||
|
||||
private static PropertiesAWSCredentials credentials;
|
||||
|
||||
private AWSTestUtils() {
|
||||
throw new AssertionError("Cannot instantiate a utility class");
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that will be used to test the contents of the file to assert we are getting the
|
||||
* the right value
|
||||
*
|
||||
* @param permFile
|
||||
* @param expectedContent
|
||||
* @throws IOException
|
||||
*/
|
||||
public static final void assertFileContent(File permFile, String expectedContent)
|
||||
throws IOException {
|
||||
BufferedReader reader = null;
|
||||
try {
|
||||
reader = new BufferedReader(new InputStreamReader(new FileInputStream(permFile)));
|
||||
Assert.assertEquals(expectedContent, reader.readLine());
|
||||
} finally {
|
||||
if(reader != null) {
|
||||
reader.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the content to the file at given location.
|
||||
*
|
||||
* @param path
|
||||
* @param content
|
||||
*/
|
||||
public static final void writeToFile(String path, String content) throws IOException {
|
||||
//create the required directories if needed
|
||||
if(path.contains(File.separator)) {
|
||||
int index = path.lastIndexOf(File.separatorChar);
|
||||
if(index != 0) {
|
||||
new File(path.substring(0, index)).mkdirs();
|
||||
}
|
||||
}
|
||||
File file = new File(path);
|
||||
FileOutputStream fos = new FileOutputStream(file);
|
||||
fos.write(content.getBytes());
|
||||
fos.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* The static helper method that would be used by other AWS tests to
|
||||
* get the implementation of the {@link AmazonWSCredentials} instance
|
||||
* @return
|
||||
* @throws Exception
|
||||
*/
|
||||
public static AWSCredentials getCredentials() {
|
||||
if(credentials == null) {
|
||||
credentials =
|
||||
new PropertiesAWSCredentials("classpath:awscredentials.properties");
|
||||
try {
|
||||
credentials.afterPropertiesSet();
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return credentials;
|
||||
}
|
||||
|
||||
/**
|
||||
* When passed a {@link File} instance for the root directory, the contents are recursively
|
||||
* checked and the listing of the directories is retrieved.
|
||||
*
|
||||
* @param rootDirectory
|
||||
* @return
|
||||
*/
|
||||
public static List<File> getContentsRecursively(File rootDirectory) {
|
||||
if(!rootDirectory.isDirectory()) {
|
||||
return null;
|
||||
}
|
||||
else {
|
||||
List<File> files = new ArrayList<File>();
|
||||
getContentsRecursively(rootDirectory, files);
|
||||
return files;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively gets all the files present under the provided directory
|
||||
* @param file
|
||||
* @param files
|
||||
*/
|
||||
private static void getContentsRecursively(File file, List<File> files) {
|
||||
if(file.isFile()) {
|
||||
files.add(file);
|
||||
}
|
||||
else if(file.isDirectory()){
|
||||
File[] children = file.listFiles();
|
||||
if(children != null && children.length != 0) {
|
||||
for(File child:children) {
|
||||
getContentsRecursively(child, files);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method that will be used to generate the base64 encoded MD5 hash of the string
|
||||
*
|
||||
* @param input
|
||||
* @return
|
||||
*/
|
||||
public static String md5Hash(String input) {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("MD5");
|
||||
byte[] digestedBytes = digest.digest(input.getBytes("UTF-8"));
|
||||
return new String(Base64.encodeBase64(digestedBytes),"UTF-8");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
} catch(UnsupportedEncodingException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.integration.aws.s3;
|
||||
|
||||
import static junit.framework.Assert.assertEquals;
|
||||
import static junit.framework.Assert.assertNull;
|
||||
import static org.springframework.integration.aws.common.AWSTestUtils.md5Hash;
|
||||
import static org.springframework.integration.aws.s3.AmazonS3OperationsMockingUtil.BUCKET;
|
||||
import static org.springframework.integration.aws.s3.AmazonS3OperationsMockingUtil.mockAmazonS3Operations;
|
||||
import static org.springframework.integration.aws.s3.AmazonS3OperationsMockingUtil.mockS3Operations;
|
||||
import static org.springframework.integration.test.util.TestUtils.getPropertyValue;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
import org.springframework.expression.common.LiteralExpression;
|
||||
import org.springframework.integration.Message;
|
||||
import org.springframework.integration.aws.core.BasicAWSCredentials;
|
||||
import org.springframework.integration.aws.s3.core.AbstractAmazonS3Operations;
|
||||
import org.springframework.integration.aws.s3.core.AmazonS3Object;
|
||||
import org.springframework.integration.aws.s3.core.AmazonS3ObjectACL;
|
||||
import org.springframework.integration.aws.s3.core.AmazonS3Operations;
|
||||
import org.springframework.integration.aws.s3.core.PaginatedObjectsView;
|
||||
|
||||
/**
|
||||
* The test class for {@link AmazonS3InboundSynchronizationMessageSource}
|
||||
*
|
||||
* @author Amol Nayak
|
||||
*
|
||||
* @since 0.5
|
||||
*
|
||||
*/
|
||||
public class AmazonS3InboundSynchronizationMessageSourceTests {
|
||||
|
||||
@Rule
|
||||
public TemporaryFolder temp = new TemporaryFolder();
|
||||
private static AmazonS3Operations operations;
|
||||
|
||||
|
||||
@BeforeClass
|
||||
public static void setup() {
|
||||
operations = mockS3Operations();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Tests by providing null credentials.
|
||||
*/
|
||||
@Test(expected=IllegalArgumentException.class)
|
||||
public void withNullCredentials() {
|
||||
AmazonS3InboundSynchronizationMessageSource src = new AmazonS3InboundSynchronizationMessageSource();
|
||||
src.setCredentials(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests by providing null temporary suffix
|
||||
*/
|
||||
@Test(expected=IllegalArgumentException.class)
|
||||
public void withNullTempSuffix() {
|
||||
AmazonS3InboundSynchronizationMessageSource src = new AmazonS3InboundSynchronizationMessageSource();
|
||||
src.setTemporarySuffix(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests by providing null wildcard
|
||||
*/
|
||||
@Test(expected=IllegalArgumentException.class)
|
||||
public void withNullWildcard() {
|
||||
AmazonS3InboundSynchronizationMessageSource src = new AmazonS3InboundSynchronizationMessageSource();
|
||||
src.setFileNameWildcard(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests by providing null regex
|
||||
*/
|
||||
@Test(expected=IllegalArgumentException.class)
|
||||
public void withNullRegex() {
|
||||
AmazonS3InboundSynchronizationMessageSource src = new AmazonS3InboundSynchronizationMessageSource();
|
||||
src.setFileNameRegex(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests by providing both regex and wildcard
|
||||
*/
|
||||
@Test(expected=IllegalArgumentException.class)
|
||||
public void withBothRegexAndWildcard() {
|
||||
AmazonS3InboundSynchronizationMessageSource src = new AmazonS3InboundSynchronizationMessageSource();
|
||||
src.setFileNameRegex("[a-z]+\\.txt");
|
||||
src.setFileNameWildcard("*.txt");
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests by providing both wildcard and regex, unlike previous one, this sets the wildcard first
|
||||
*/
|
||||
@Test(expected=IllegalArgumentException.class)
|
||||
public void withBothWildcardAndRegex() {
|
||||
AmazonS3InboundSynchronizationMessageSource src = new AmazonS3InboundSynchronizationMessageSource();
|
||||
src.setFileNameWildcard("*.txt");
|
||||
src.setFileNameRegex("[a-z]+\\.txt");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Tests providing null remote directory
|
||||
*/
|
||||
@Test(expected=IllegalArgumentException.class)
|
||||
public void withNullRemoteDirectory() {
|
||||
AmazonS3InboundSynchronizationMessageSource src = new AmazonS3InboundSynchronizationMessageSource();
|
||||
src.setRemoteDirectory(null);
|
||||
}
|
||||
|
||||
/**
|
||||
*Tests with a non existent local directory
|
||||
*/
|
||||
@Test(expected=IllegalArgumentException.class)
|
||||
public void withNonExistentLocalDirectory() {
|
||||
AmazonS3InboundSynchronizationMessageSource src = new AmazonS3InboundSynchronizationMessageSource();
|
||||
src.setDirectory(new LiteralExpression("SomeNotExistentDir"));
|
||||
src.afterPropertiesSet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests with a {@link File} instance that is not a directory
|
||||
* @throws IOException
|
||||
*/
|
||||
@Test(expected=IllegalArgumentException.class)
|
||||
public void withNonDirectory() throws IOException {
|
||||
File file = temp.newFile("SomeFile.txt");
|
||||
AmazonS3InboundSynchronizationMessageSource src = new AmazonS3InboundSynchronizationMessageSource();
|
||||
src.setDirectory(new LiteralExpression(file.getAbsolutePath()));
|
||||
src.afterPropertiesSet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests with a null s3 operation.
|
||||
*/
|
||||
@Test(expected=IllegalArgumentException.class)
|
||||
public void withNullS3Operations() {
|
||||
AmazonS3InboundSynchronizationMessageSource src = new AmazonS3InboundSynchronizationMessageSource();
|
||||
src.setS3Operations(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Doesn't set the {@link AmazonS3Operations} instance and relies on the default one.
|
||||
* sets the temp suffix and the thread pool executor.
|
||||
*/
|
||||
@Test
|
||||
public void withDefaultS3Service() {
|
||||
AmazonS3InboundSynchronizationMessageSource src = new AmazonS3InboundSynchronizationMessageSource();
|
||||
BasicAWSCredentials credentials = new BasicAWSCredentials("dummy", "dummy");
|
||||
src.setTemporarySuffix(".temp");
|
||||
src.setCredentials(credentials);
|
||||
src.setDirectory(new LiteralExpression(temp.getRoot().getAbsolutePath()));
|
||||
src.afterPropertiesSet();
|
||||
assertEquals(".temp", getPropertyValue(src, "s3Operations.temporaryFileSuffix", String.class));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Instantiates with a custom implementation of {@link AmazonS3Operations}
|
||||
* which extends from {@link AbstractAmazonS3Operations}. Also sets the following
|
||||
* s3Operations, directory, fileNameRegex, remoteDirectory, maxObjectsPerBatch and temporarySuffix
|
||||
* attributes.
|
||||
*/
|
||||
@Test
|
||||
public void withCustomS3Operations() {
|
||||
BasicAWSCredentials credentials = new BasicAWSCredentials("dummy", "dummy");
|
||||
AbstractAmazonS3Operations ops = new AbstractAmazonS3Operations(credentials) {
|
||||
|
||||
@Override
|
||||
protected void doPut(String bucket, String key, File file,
|
||||
AmazonS3ObjectACL objectACL, Map<String, String> userMetadata,
|
||||
String stringContentMD5) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PaginatedObjectsView doListObjects(String bucketName,
|
||||
String nextMarker, int pageSize, String prefix) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AmazonS3Object doGetObject(String bucketName, String key) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
AmazonS3InboundSynchronizationMessageSource src = new AmazonS3InboundSynchronizationMessageSource();
|
||||
src.setS3Operations(ops);
|
||||
src.setBucket("testbucket");
|
||||
src.setDirectory(new LiteralExpression(temp.getRoot().getAbsolutePath()));
|
||||
src.setFileNameRegex("[a-z]+\\.txt");
|
||||
src.setRemoteDirectory("remotedirectory");
|
||||
src.setMaxObjectsPerBatch(15);
|
||||
src.setTemporarySuffix(".temp");
|
||||
src.setAcceptSubFolders(true);
|
||||
src.setDirectory(new LiteralExpression(temp.getRoot().getAbsolutePath()));
|
||||
src.afterPropertiesSet();
|
||||
|
||||
assertEquals(ops, getPropertyValue(src, "s3Operations",AmazonS3Operations.class));
|
||||
assertEquals(".temp", getPropertyValue(src, "s3Operations.temporaryFileSuffix",String.class));
|
||||
assertEquals("testbucket", getPropertyValue(src, "bucket", String.class));
|
||||
assertEquals(temp.getRoot(), getPropertyValue(src, "directory"));
|
||||
assertEquals("[a-z]+\\.txt", getPropertyValue(src, "synchronizer.fileNameRegex", String.class));
|
||||
assertEquals(true, getPropertyValue(src, "synchronizer.acceptSubFolders", Boolean.class).booleanValue());
|
||||
assertEquals(15, getPropertyValue(src, "synchronizer.maxObjectsPerBatch", Integer.class).intValue());
|
||||
assertEquals("remotedirectory", getPropertyValue(src, "remoteDirectory", String.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronizes the local directory to a remote bucket
|
||||
*/
|
||||
@Test
|
||||
public void synchronizeWithLocalDirectory() {
|
||||
mockAmazonS3Operations(Arrays.asList(
|
||||
new String[]{"test.txt","test.txt",md5Hash("test.txt"),null},
|
||||
new String[]{"sub1/test.txt","sub1/test.txt",md5Hash("sub1/test.txt"),null},
|
||||
new String[]{"sub1/sub11/test.txt","sub1/sub11/test.txt",md5Hash("sub1/sub11/test.txt"),null},
|
||||
new String[]{"sub2/test.txt","sub2/test.txt",md5Hash("sub2/test.txt"),null}
|
||||
));
|
||||
AmazonS3InboundSynchronizationMessageSource src = new AmazonS3InboundSynchronizationMessageSource();
|
||||
src.setS3Operations(operations);
|
||||
src.setBucket(BUCKET);
|
||||
src.setDirectory(new LiteralExpression(temp.getRoot().getAbsolutePath()));
|
||||
src.setFileNameRegex("[a-z]+\\.txt");
|
||||
src.setRemoteDirectory("/sub1");
|
||||
src.setMaxObjectsPerBatch(15);
|
||||
src.setTemporarySuffix(".temp");
|
||||
src.setAcceptSubFolders(true);
|
||||
src.setDirectory(new LiteralExpression(temp.getRoot().getAbsolutePath()));
|
||||
src.afterPropertiesSet();
|
||||
File file = src.receive().getPayload();
|
||||
assertEquals(temp.getRoot().getAbsoluteFile() +
|
||||
File.separator + "sub1" + File.separator + "test.txt", file.getAbsolutePath());
|
||||
file = src.receive().getPayload();
|
||||
assertEquals(temp.getRoot().getAbsoluteFile() +
|
||||
File.separator + "sub1" + File.separator + "sub11" + File.separator + "test.txt", file.getAbsolutePath());
|
||||
Message<File> message = src.receive();
|
||||
assertNull(message);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.integration.aws.s3;
|
||||
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Matchers.anyString;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.springframework.integration.aws.s3.AmazonS3MessageHeaders.FILE_NAME;
|
||||
import static org.springframework.integration.aws.s3.AmazonS3MessageHeaders.METADATA;
|
||||
import static org.springframework.integration.aws.s3.AmazonS3MessageHeaders.OBJECT_ACLS;
|
||||
import static org.springframework.integration.aws.s3.AmazonS3MessageHeaders.USER_METADATA;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import junit.framework.Assert;
|
||||
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
import org.mockito.stubbing.Answer;
|
||||
import org.springframework.expression.common.LiteralExpression;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.integration.Message;
|
||||
import org.springframework.integration.MessageHandlingException;
|
||||
import org.springframework.integration.aws.core.BasicAWSCredentials;
|
||||
import org.springframework.integration.aws.s3.core.AmazonS3Object;
|
||||
import org.springframework.integration.aws.s3.core.AmazonS3Operations;
|
||||
import org.springframework.integration.support.MessageBuilder;
|
||||
|
||||
/**
|
||||
* The test class for {@link AmazonS3MessageHandler}, we rely on mock of {@link AmazonS3Operations}
|
||||
* to test the behavior.
|
||||
*
|
||||
* @author Amol Nayak
|
||||
*
|
||||
* @since 0.5
|
||||
*
|
||||
*/
|
||||
public class AmazonS3MessageHandlerTests {
|
||||
|
||||
private static AmazonS3Operations operations;
|
||||
private static PutObjectParameterHolder holder = new PutObjectParameterHolder();
|
||||
|
||||
@BeforeClass
|
||||
public static void setup() {
|
||||
operations = Mockito.mock(AmazonS3Operations.class);
|
||||
|
||||
doAnswer(new Answer<Object>() {
|
||||
public Object answer(InvocationOnMock inv) {
|
||||
Object[] args = inv.getArguments();
|
||||
holder.setBucket((String)args[0]);
|
||||
holder.setFolder((String)args[1]);
|
||||
holder.setObjectName((String)args[2]);
|
||||
holder.setS3Object((AmazonS3Object)args[3]);
|
||||
return null;
|
||||
}
|
||||
}).
|
||||
when(operations)
|
||||
.putObject(anyString(), anyString(), anyString(), any(AmazonS3Object.class));
|
||||
|
||||
|
||||
}
|
||||
private AmazonS3MessageHandler getHandler() {
|
||||
AmazonS3MessageHandler handler = new AmazonS3MessageHandler(new BasicAWSCredentials(), operations);
|
||||
//set the remote directory to root by default
|
||||
handler.setRemoteDirectoryExpression(new LiteralExpression("/"));
|
||||
handler.setBucket("TestBucket");
|
||||
handler.afterPropertiesSet();
|
||||
return handler;
|
||||
}
|
||||
|
||||
private static class PutObjectParameterHolder {
|
||||
private String bucket;
|
||||
private String folder;
|
||||
private String objectName;
|
||||
private AmazonS3Object s3Object;
|
||||
|
||||
public String getBucket() {
|
||||
return bucket;
|
||||
}
|
||||
public void setBucket(String bucket) {
|
||||
this.bucket = bucket;
|
||||
}
|
||||
public String getFolder() {
|
||||
return folder;
|
||||
}
|
||||
public void setFolder(String folder) {
|
||||
this.folder = folder;
|
||||
}
|
||||
public String getObjectName() {
|
||||
return objectName;
|
||||
}
|
||||
public void setObjectName(String objectName) {
|
||||
this.objectName = objectName;
|
||||
}
|
||||
public AmazonS3Object getS3Object() {
|
||||
return s3Object;
|
||||
}
|
||||
public void setS3Object(AmazonS3Object s3Object) {
|
||||
this.s3Object = s3Object;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests with a message payload of type {@link String}
|
||||
*/
|
||||
@Test
|
||||
public void withStringPayload() {
|
||||
Message<String> message = MessageBuilder.withPayload("Test String").build();
|
||||
AmazonS3MessageHandler handler = getHandler();
|
||||
handler.handleMessage(message);
|
||||
AmazonS3Object object = holder.getS3Object();
|
||||
Assert.assertNotNull(object.getInputStream());
|
||||
Assert.assertNull(object.getFileSource());
|
||||
assertCommonValues(message,object);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests with a message with payload of type {@link InputStream}
|
||||
*/
|
||||
@Test
|
||||
public void withInputStreamPayload() {
|
||||
InputStream bin = new ByteArrayInputStream("SomeString".getBytes());
|
||||
Message<InputStream> message = MessageBuilder.withPayload(bin).build();
|
||||
AmazonS3MessageHandler handler = getHandler();
|
||||
handler.handleMessage(message);
|
||||
AmazonS3Object object = holder.getS3Object();
|
||||
Assert.assertNotNull(object.getInputStream());
|
||||
Assert.assertNull(object.getFileSource());
|
||||
assertCommonValues(message,object);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests with a message with payload of type byte[]
|
||||
*/
|
||||
@Test
|
||||
public void withByteArrayPayload() {
|
||||
Message<byte[]> message = MessageBuilder.withPayload("String".getBytes()).build();
|
||||
AmazonS3MessageHandler handler = getHandler();
|
||||
handler.handleMessage(message);
|
||||
AmazonS3Object object = holder.getS3Object();
|
||||
Assert.assertNotNull(object.getInputStream());
|
||||
Assert.assertNull(object.getFileSource());
|
||||
assertCommonValues(message,object);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests with a message with payload of type {@link File} which is a file with temporary suffix
|
||||
*/
|
||||
@Test
|
||||
public void withTempFileTypePayload() throws Exception {
|
||||
File file = new File(System.getProperty("java.io.tmpdir") + "TempFile.txt.writing");
|
||||
messageWithFileTypePayload(file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests with a message with payload of type {@link File} which is a file without temporary suffix
|
||||
*/
|
||||
@Test
|
||||
public void withFileTypePayload() throws Exception {
|
||||
File file = new File(System.getProperty("java.io.tmpdir") + "TempFile.txt");
|
||||
messageWithFileTypePayload(file);
|
||||
}
|
||||
|
||||
/**
|
||||
*Test case to with message of an incompatible type, {@link Integer} in this case.
|
||||
*
|
||||
*/
|
||||
@Test(expected=MessageHandlingException.class)
|
||||
public void withIncompatiblePayload() {
|
||||
Message<Integer> message = MessageBuilder.withPayload(1).build();
|
||||
AmazonS3MessageHandler handler = getHandler();
|
||||
handler.handleMessage(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests with all the header provided in the message
|
||||
*/
|
||||
@Test
|
||||
public void withAllHeaders() {
|
||||
Map<String, Collection<String>> acls = new HashMap<String, Collection<String>>();
|
||||
acls.put("test@test.com", Arrays.asList("Read", "Write acp"));
|
||||
Message<String> message = MessageBuilder.withPayload("Test Content")
|
||||
.setHeader(FILE_NAME, "TestFileName.txt")
|
||||
.setHeader(USER_METADATA, Collections.singletonMap("UserMD", "UserMD"))
|
||||
.setHeader(METADATA, Collections.singletonMap("Metadata", "Metadata"))
|
||||
.setHeader(OBJECT_ACLS, acls)
|
||||
.setHeader("remoteDirectory", "/remote")
|
||||
.build();
|
||||
AmazonS3MessageHandler handler = getHandler();
|
||||
SpelExpressionParser parser = new SpelExpressionParser();
|
||||
handler.setRemoteDirectoryExpression(parser.parseExpression("headers['remoteDirectory']"));
|
||||
handler.handleMessage(message);
|
||||
Assert.assertEquals("TestBucket", holder.getBucket());
|
||||
Assert.assertEquals("TestFileName.txt", holder.getObjectName());
|
||||
Assert.assertEquals("/remote", holder.getFolder());
|
||||
AmazonS3Object object = holder.getS3Object();
|
||||
Assert.assertNotNull(object);
|
||||
Assert.assertNotNull(object.getInputStream());
|
||||
Assert.assertNotNull(object.getMetaData());
|
||||
Assert.assertNotNull(object.getUserMetaData());
|
||||
Assert.assertNotNull(object.getObjectACL());
|
||||
Assert.assertEquals(2,object.getObjectACL().getGrants().size());
|
||||
}
|
||||
|
||||
/**
|
||||
* The common method to test messages with payload of type {@link File}
|
||||
* @param file
|
||||
*/
|
||||
private void messageWithFileTypePayload(File file) throws Exception {
|
||||
file.createNewFile();
|
||||
Message<File> message = MessageBuilder.withPayload(file).build();
|
||||
AmazonS3MessageHandler handler = getHandler();
|
||||
handler.handleMessage(message);
|
||||
AmazonS3Object object = holder.getS3Object();
|
||||
Assert.assertEquals("TempFile.txt", holder.getObjectName());
|
||||
Assert.assertNotNull(object.getFileSource());
|
||||
Assert.assertNull(object.getInputStream());
|
||||
Assert.assertNull(object.getMetaData());
|
||||
Assert.assertNull(object.getObjectACL());
|
||||
Assert.assertNull(object.getUserMetaData());
|
||||
file.delete();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The method used to assert the values for tests with String, InputStream and byte[] parameters
|
||||
* @param message
|
||||
*/
|
||||
private void assertCommonValues(Message<?> message,AmazonS3Object object) {
|
||||
Assert.assertEquals(message.getHeaders().getId().toString() + ".ext", holder.getObjectName());
|
||||
Assert.assertEquals("/", holder.getFolder());
|
||||
Assert.assertEquals("TestBucket", holder.getBucket());
|
||||
Assert.assertNotNull(object);
|
||||
Assert.assertNull(object.getMetaData());
|
||||
Assert.assertNull(object.getObjectACL());
|
||||
Assert.assertNull(object.getUserMetaData());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.integration.aws.s3;
|
||||
|
||||
import static org.springframework.integration.aws.s3.core.ObjectPermissions.READ;
|
||||
import static org.springframework.integration.aws.s3.core.ObjectPermissions.READ_ACP;
|
||||
import static org.springframework.integration.aws.s3.core.ObjectPermissions.WRITE_ACP;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import junit.framework.Assert;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
import org.springframework.integration.aws.s3.core.AmazonS3Object;
|
||||
import org.springframework.integration.aws.s3.core.AmazonS3ObjectACL;
|
||||
import org.springframework.integration.aws.s3.core.Grantee;
|
||||
import org.springframework.integration.aws.s3.core.GranteeType;
|
||||
import org.springframework.integration.aws.s3.core.ObjectGrant;
|
||||
import org.springframework.integration.aws.s3.core.ObjectPermissions;
|
||||
import org.springframework.integration.test.util.TestUtils;
|
||||
|
||||
/**
|
||||
* The test case for the class {@link AmazonS3ObjectBuilder}
|
||||
*
|
||||
* @author Amol Nayak
|
||||
*
|
||||
* @since 0.5
|
||||
*
|
||||
*/
|
||||
public class AmazonS3ObjectBuilderTests {
|
||||
|
||||
@Rule
|
||||
public static TemporaryFolder temp = new TemporaryFolder();
|
||||
|
||||
private static final String GROUP_GRANTEE = "http://acs.amazonaws.com/groups/global/AllUsers";
|
||||
private static final String EMAIL_GRANTEE = "test@test.com";
|
||||
private static final String CANONICAL_GRANTEE = "12345678900987654321abcdefabcdeab12345678900987654321abcdefabcde";
|
||||
|
||||
/**
|
||||
* Tries to construct the object with a null file instance
|
||||
*/
|
||||
@Test(expected=IllegalArgumentException.class)
|
||||
public void withNullFile() {
|
||||
AmazonS3ObjectBuilder.getInstance().fromFile(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to construct the file from a non existent file
|
||||
*/
|
||||
@Test(expected=IllegalArgumentException.class)
|
||||
public void withNonExistentFile() {
|
||||
AmazonS3ObjectBuilder.getInstance().fromFile(new File("somejunkfile"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to construct the file which is a directory
|
||||
*/
|
||||
@Test(expected=IllegalArgumentException.class)
|
||||
public void withADirectory() {
|
||||
File dir = temp.newFolder("tempdir");
|
||||
AmazonS3ObjectBuilder.getInstance().fromFile(dir);
|
||||
dir.delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to construct the {@link AmazonS3Object} from a null location
|
||||
*/
|
||||
@Test(expected=IllegalArgumentException.class)
|
||||
public void withNullPathString() {
|
||||
AmazonS3ObjectBuilder.getInstance().fromLocation(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to construct the {@link AmazonS3Object} from a valid file
|
||||
*/
|
||||
@Test
|
||||
public void withValidFile() throws Exception {
|
||||
File file = temp.newFile("temp.txt");
|
||||
String pathname = file.getAbsolutePath();
|
||||
AmazonS3ObjectBuilder builder =
|
||||
AmazonS3ObjectBuilder.getInstance().fromLocation(pathname);
|
||||
file = TestUtils.getPropertyValue(builder, "file", File.class);
|
||||
Assert.assertNotNull(file);
|
||||
Assert.assertEquals(pathname, file.getAbsolutePath());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to construct the {@link AmazonS3Object} from an {@link InputStream}
|
||||
*/
|
||||
@Test
|
||||
public void withInputStream()throws Exception {
|
||||
File tempFile = temp.newFile("Temp.txt");
|
||||
InputStream in = new FileInputStream(tempFile);
|
||||
AmazonS3ObjectBuilder builder =
|
||||
AmazonS3ObjectBuilder.getInstance().fromInputStream(in);
|
||||
Assert.assertNotNull(TestUtils.getPropertyValue(builder, "in", InputStream.class));
|
||||
in.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to construct the {@link AmazonS3Object} from an {@link InputStream} and a {@link File}
|
||||
* instance
|
||||
*/
|
||||
@Test(expected=IllegalArgumentException.class)
|
||||
public void withBothinputStreamAndFile() throws Exception {
|
||||
File tempFile = temp.newFile("Temp.txt");
|
||||
FileInputStream in = new FileInputStream(tempFile);
|
||||
AmazonS3ObjectBuilder
|
||||
.getInstance()
|
||||
.fromInputStream(in)
|
||||
.fromFile(tempFile);
|
||||
in.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs the {@link AmazonS3Object} with some user metadata
|
||||
*/
|
||||
@Test
|
||||
public void withUserMetadata() throws Exception {
|
||||
File tempFile = temp.newFile("Temp.txt");
|
||||
AmazonS3ObjectBuilder builder = AmazonS3ObjectBuilder
|
||||
.getInstance()
|
||||
.fromFile(tempFile)
|
||||
.withUserMetaData(Collections.singletonMap("Key", "Value"));
|
||||
Assert.assertNotNull(TestUtils.getPropertyValue(builder, "userMetaData", Map.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs the {@link AmazonS3Object} with some metadata
|
||||
*/
|
||||
@Test
|
||||
public void withMetadata() throws Exception {
|
||||
File tempFile = temp.newFile("Temp.txt");
|
||||
AmazonS3ObjectBuilder builder = AmazonS3ObjectBuilder
|
||||
.getInstance()
|
||||
.fromFile(tempFile)
|
||||
.withMetaData(Collections.singletonMap("Key", (Object)"Value"));
|
||||
Assert.assertNotNull(TestUtils.getPropertyValue(builder, "metaData", Map.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs the {@link AmazonS3Object} with an invalid ACL identifier
|
||||
*/
|
||||
@Test
|
||||
public void withValidACL() throws Exception {
|
||||
File tempFile = temp.newFile("Temp.txt");
|
||||
Map<String, Collection<String>> acls = generateObjectACLS();
|
||||
AmazonS3ObjectBuilder builder = AmazonS3ObjectBuilder
|
||||
.getInstance()
|
||||
.fromFile(tempFile)
|
||||
.withObjectACL(acls);
|
||||
AmazonS3ObjectACL acl = TestUtils.getPropertyValue(builder, "objectACL", AmazonS3ObjectACL.class);
|
||||
assertGrants(acl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a complete object with all the possible attributes and checks
|
||||
* if those are properly populated in the constructed {@link AmazonS3Object}
|
||||
*/
|
||||
@Test
|
||||
public void withCompleteObjectFromFile() throws Exception {
|
||||
File tempFile = temp.newFile("Temp.txt");
|
||||
Map<String, Collection<String>> acls = generateObjectACLS();
|
||||
Map<String, String> userMetadata = Collections.singletonMap("Key", "Value");
|
||||
Map<String, Object> metadata = Collections.singletonMap("Key", (Object)"Value");
|
||||
AmazonS3ObjectBuilder builder = AmazonS3ObjectBuilder
|
||||
.getInstance()
|
||||
.fromFile(tempFile)
|
||||
.withObjectACL(acls)
|
||||
.withMetaData(metadata)
|
||||
.withUserMetaData(userMetadata);
|
||||
AmazonS3Object object = builder.build();
|
||||
assertGrants(object.getObjectACL());
|
||||
Assert.assertEquals(userMetadata, object.getUserMetaData());
|
||||
Assert.assertEquals(metadata, object.getMetaData());
|
||||
Assert.assertEquals(tempFile, object.getFileSource());
|
||||
Assert.assertNull(object.getInputStream());
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an object with an {@link InputStream}
|
||||
*/
|
||||
@Test
|
||||
public void wothObjectFromStream() throws Exception {
|
||||
File tempFile = temp.newFile("Temp.txt");
|
||||
InputStream in = new FileInputStream(tempFile);
|
||||
AmazonS3ObjectBuilder builder = AmazonS3ObjectBuilder
|
||||
.getInstance()
|
||||
.fromInputStream(in);
|
||||
AmazonS3Object object = builder.build();
|
||||
Assert.assertNull(object.getFileSource());
|
||||
Assert.assertEquals(in,object.getInputStream());
|
||||
in.close();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Generate the object test ACLS
|
||||
* @return
|
||||
*/
|
||||
private Map<String, Collection<String>> generateObjectACLS() {
|
||||
Map<String, Collection<String>> acls = new HashMap<String, Collection<String>>();
|
||||
acls.put(CANONICAL_GRANTEE,
|
||||
Arrays.asList("write acp"));
|
||||
acls.put(EMAIL_GRANTEE,
|
||||
Arrays.asList("read acp", "write acp"));
|
||||
acls.put(GROUP_GRANTEE,
|
||||
Arrays.asList("read"));
|
||||
return acls;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for the grants in the object
|
||||
*
|
||||
*/
|
||||
private void assertGrants(AmazonS3ObjectACL acl) {
|
||||
Set<ObjectGrant> grants = acl.getGrants();
|
||||
Assert.assertEquals(4, grants.size());
|
||||
for(ObjectGrant grant:grants) {
|
||||
Grantee grantee = grant.getGrantee();
|
||||
GranteeType type = grantee.getGranteeType();
|
||||
ObjectPermissions permission = grant.getPermission();
|
||||
if(type == GranteeType.CANONICAL_GRANTEE_TYPE) {
|
||||
Assert.assertEquals(CANONICAL_GRANTEE, grantee.getIdentifier());
|
||||
Assert.assertEquals(WRITE_ACP,permission);
|
||||
}
|
||||
else if(type == GranteeType.EMAIL_GRANTEE_TYPE){
|
||||
Assert.assertEquals(EMAIL_GRANTEE, grantee.getIdentifier());
|
||||
Assert.assertTrue(permission == WRITE_ACP || permission == READ_ACP);
|
||||
}
|
||||
else if(type == GranteeType.GROUP_GRANTEE_TYPE) {
|
||||
Assert.assertEquals(GROUP_GRANTEE, grantee.getIdentifier());
|
||||
Assert.assertEquals(READ,permission);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.integration.aws.s3;
|
||||
|
||||
import static org.mockito.Matchers.anyInt;
|
||||
import static org.mockito.Matchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.integration.aws.s3.InboundFileSynchronizationImpl.CONTENT_MD5;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.codec.binary.Base64;
|
||||
import org.apache.commons.codec.binary.Hex;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
import org.mockito.stubbing.Answer;
|
||||
import org.springframework.integration.aws.s3.core.AmazonS3Object;
|
||||
import org.springframework.integration.aws.s3.core.AmazonS3Operations;
|
||||
import org.springframework.integration.aws.s3.core.PaginatedObjectsView;
|
||||
import org.springframework.integration.aws.s3.core.S3ObjectSummary;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* The utility class for mocking the {@link AmazonS3Operations}
|
||||
*
|
||||
* @author Amol Nayak
|
||||
*
|
||||
* @since 0.5
|
||||
*/
|
||||
public final class AmazonS3OperationsMockingUtil {
|
||||
|
||||
public static final String BUCKET = "com.si.aws.test.bucket";
|
||||
private static final List<S3ObjectSummary> summary = new ArrayList<S3ObjectSummary>();
|
||||
private static final Map<String, String[]> objectDetails = new HashMap<String, String[]>();
|
||||
|
||||
private AmazonS3OperationsMockingUtil() {
|
||||
throw new AssertionError("Cannot instantiate utility class");
|
||||
}
|
||||
|
||||
public static final AmazonS3Operations mockS3Operations() {
|
||||
|
||||
AmazonS3Operations operations;
|
||||
|
||||
PaginatedObjectsView view = new PaginatedObjectsView() {
|
||||
|
||||
@Override
|
||||
public boolean hasMoreResults() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<S3ObjectSummary> getObjectSummary() {
|
||||
return summary;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getNextMarker() {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
operations = mock(AmazonS3Operations.class);
|
||||
when(operations.listObjects(anyString(), anyString(), anyString(), anyInt()))
|
||||
.thenReturn(view);
|
||||
|
||||
when(operations.getObject(anyString(), anyString(), anyString()))
|
||||
.then(new Answer<AmazonS3Object>() {
|
||||
public AmazonS3Object answer(InvocationOnMock invocation)
|
||||
throws Throwable {
|
||||
String folderName = (String)invocation.getArguments()[1];
|
||||
String fileName = (String)invocation.getArguments()[2];
|
||||
|
||||
if(StringUtils.hasText(folderName)) {
|
||||
if(folderName.startsWith("/")) {
|
||||
folderName = folderName.substring(1);
|
||||
}
|
||||
if(folderName.endsWith("/")) {
|
||||
folderName = folderName + "/";
|
||||
}
|
||||
}
|
||||
else {
|
||||
folderName = "";
|
||||
}
|
||||
|
||||
|
||||
String[] object = objectDetails.get(folderName + fileName);
|
||||
AmazonS3Object s3Object;
|
||||
if(object != null) {
|
||||
s3Object = new AmazonS3ObjectBuilder()
|
||||
.fromInputStream(new ByteArrayInputStream(object[1].getBytes()))
|
||||
.withUserMetaData(Collections.singletonMap(CONTENT_MD5, object[2]))
|
||||
.build();
|
||||
}
|
||||
else {
|
||||
s3Object = null;
|
||||
}
|
||||
|
||||
return s3Object;
|
||||
}
|
||||
});
|
||||
|
||||
return operations;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The private helper method that is used to mock the {@link AmazonS3Operations#listObjects(String, String, String, int)
|
||||
* method
|
||||
*
|
||||
* The method accepts a List of object[] where each element of object array has the
|
||||
* following significance
|
||||
*
|
||||
* object[0] is the key of the file
|
||||
* object[1] is the content of the file
|
||||
* object[2] is the MD5 content to be used (optional)
|
||||
* object[3] is the etag of the object
|
||||
*
|
||||
* @param listObjects
|
||||
*/
|
||||
public static void mockAmazonS3Operations(final List<String[]> listObjects) {
|
||||
summary.clear();
|
||||
for(String[] listObject:listObjects) {
|
||||
addToSummaryList(listObject);
|
||||
objectDetails.put(listObject[0], listObject);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link S3ObjectSummary} with the given details and adds it to the summary object list
|
||||
* @param listObject
|
||||
*/
|
||||
private static void addToSummaryList(final String[] listObject) {
|
||||
summary.add(
|
||||
new S3ObjectSummary() {
|
||||
|
||||
@Override
|
||||
public long getSize() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Date getLastModified() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKey() {
|
||||
return listObject[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getETag() {
|
||||
if(listObject[3] != null) {
|
||||
return listObject[3];
|
||||
}
|
||||
else {
|
||||
byte[] b64 = Base64.decodeBase64((listObject[2]).getBytes());
|
||||
return Hex.encodeHexString(b64);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBucketName() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.integration.aws.s3;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.UUID;
|
||||
|
||||
import junit.framework.Assert;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.springframework.integration.Message;
|
||||
import org.springframework.integration.support.MessageBuilder;
|
||||
|
||||
|
||||
/**
|
||||
* The test class for {@link DefaultFileNameGenerationStrategy}
|
||||
*
|
||||
* @author Amol Nayak
|
||||
*
|
||||
* @since 0.5
|
||||
*
|
||||
*/
|
||||
public class DefaultFileNameGenerationStrategyTests {
|
||||
|
||||
/**
|
||||
* Tests with the file name present in the predetermined header "file_name" of the message
|
||||
*/
|
||||
@Test
|
||||
public void withNameInHeader() {
|
||||
Message<String> message = MessageBuilder.withPayload("SomeString")
|
||||
.setHeader(AmazonS3MessageHeaders.FILE_NAME, "FileName.txt")
|
||||
.build();
|
||||
DefaultFileNameGenerationStrategy strategy = new DefaultFileNameGenerationStrategy();
|
||||
Assert.assertEquals("FileName.txt", strategy.generateFileName(message));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Tests with a payload as a temp file payload
|
||||
*/
|
||||
@Test
|
||||
public void withATempFile() {
|
||||
File file = new File(System.getProperty("java.io.tmpdir") + "TempFile.txt.writing");
|
||||
Message<File> message = MessageBuilder.withPayload(file)
|
||||
.build();
|
||||
DefaultFileNameGenerationStrategy strategy = new DefaultFileNameGenerationStrategy();
|
||||
Assert.assertEquals("TempFile.txt", strategy.generateFileName(message));
|
||||
file.delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests with a payload as a temp file payload
|
||||
*/
|
||||
@Test
|
||||
public void withANonTempFile() {
|
||||
File file = new File(System.getProperty("java.io.tmpdir") + "TempFile.txt");
|
||||
Message<File> message = MessageBuilder.withPayload(file)
|
||||
.build();
|
||||
DefaultFileNameGenerationStrategy strategy = new DefaultFileNameGenerationStrategy();
|
||||
Assert.assertEquals("TempFile.txt", strategy.generateFileName(message));
|
||||
file.delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests with a payload as a temp file payload
|
||||
*/
|
||||
@Test
|
||||
public void withMessageIdName() {
|
||||
Message<String> message = MessageBuilder.withPayload("String")
|
||||
.build();
|
||||
DefaultFileNameGenerationStrategy strategy = new DefaultFileNameGenerationStrategy();
|
||||
UUID uid = message.getHeaders().getId();
|
||||
Assert.assertEquals(uid.toString() + ".ext", strategy.generateFileName(message));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests with the file name generation expression as a null value
|
||||
*/
|
||||
@Test(expected=IllegalArgumentException.class)
|
||||
public void withNullExprssion() {
|
||||
DefaultFileNameGenerationStrategy strategy = new DefaultFileNameGenerationStrategy();
|
||||
strategy.setFileNameExpression(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests with a null value for temporary suffix
|
||||
*/
|
||||
@Test(expected=IllegalArgumentException.class)
|
||||
public void withNullTemporarySuffix() {
|
||||
DefaultFileNameGenerationStrategy strategy = new DefaultFileNameGenerationStrategy();
|
||||
strategy.setTemporarySuffix(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.integration.aws.s3;
|
||||
|
||||
import junit.framework.Assert;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
*
|
||||
* The test cases for various {@link FileNameFilter} implementations.
|
||||
*
|
||||
* @author Amol Nayak
|
||||
*
|
||||
* @since 0.5
|
||||
*
|
||||
*/
|
||||
public class FileNameFilterTests {
|
||||
|
||||
/**
|
||||
* The case sets the folder of the filter to null so that it accepts all the files in the same
|
||||
* folder, the {@link AbstractFileNameFilter#isAcceptSubFolders() returns false.
|
||||
*
|
||||
*/
|
||||
@Test
|
||||
public void acceptAllFilesWithoutSubfolders() {
|
||||
AbstractFileNameFilter filter = new AlwaysTrueFileNamefilter();
|
||||
filter.setAcceptSubFolders(false);
|
||||
Assert.assertTrue(filter.accept("SomeFile.txt"));
|
||||
Assert.assertFalse(filter.accept("somessubfolder/SomeFile.txt"));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The case sets the folder of the filter to null so that it accepts all the files in the same
|
||||
* folder and the sub folders, the {@link AbstractFileNameFilter#isAcceptSubFolders() returns true.
|
||||
*
|
||||
*/
|
||||
@Test
|
||||
public void acceptAllFilesWithSubfolders() {
|
||||
AbstractFileNameFilter filter = new AlwaysTrueFileNamefilter();
|
||||
filter.setAcceptSubFolders(true);
|
||||
Assert.assertTrue(filter.accept("SomeFile.txt"));
|
||||
Assert.assertTrue(filter.accept("somessubfolder/SomeFile.txt"));
|
||||
}
|
||||
|
||||
/**
|
||||
* The test case sets a sub folder for the search and sets the
|
||||
* {@link AbstractFileNameFilter#setAcceptSubFolders(boolean) to false
|
||||
*/
|
||||
@Test
|
||||
public void acceptInSubfolderWithoutSubfolder() {
|
||||
AbstractFileNameFilter filter = new AlwaysTrueFileNamefilter();
|
||||
filter.setFolderName("/subfolder");
|
||||
filter.setAcceptSubFolders(false);
|
||||
Assert.assertFalse(filter.accept("FileName.txt"));
|
||||
Assert.assertTrue(filter.accept("subfolder/FileName.txt"));
|
||||
Assert.assertFalse(filter.accept("subfolder/anothersf/FileName.txt"));
|
||||
}
|
||||
|
||||
/**
|
||||
* The test case sets a sub folder for the search and sets the
|
||||
* {@link AbstractFileNameFilter#setAcceptSubFolders(boolean) to true
|
||||
*/
|
||||
@Test
|
||||
public void acceptInSubfolderWithSubfolder() {
|
||||
AbstractFileNameFilter filter = new AlwaysTrueFileNamefilter();
|
||||
filter.setFolderName("/subfolder");
|
||||
filter.setAcceptSubFolders(true);
|
||||
Assert.assertFalse(filter.accept("FileName.txt"));
|
||||
Assert.assertTrue(filter.accept("subfolder/FileName.txt"));
|
||||
Assert.assertTrue(filter.accept("subfolder/anothersf/FileName.txt"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the regex file filter
|
||||
*/
|
||||
@Test
|
||||
public void regexTest() {
|
||||
//accept only file names with name in lower case and ends with .txt
|
||||
AbstractFileNameFilter filter = new RegexFileNameFilter("[a-z]+\\.txt");
|
||||
filter.setAcceptSubFolders(true);
|
||||
Assert.assertTrue(filter.accept("test.txt"));
|
||||
Assert.assertFalse(filter.accept("Test.txt"));
|
||||
Assert.assertFalse(filter.accept("test123.txt"));
|
||||
Assert.assertFalse(filter.accept("test.tx"));
|
||||
Assert.assertFalse(filter.accept("test"));
|
||||
Assert.assertTrue(filter.accept("test/test.txt"));
|
||||
Assert.assertTrue(filter.accept("test/Test/12/test.txt"));
|
||||
Assert.assertFalse(filter.accept("test/Test/12/test.tx"));
|
||||
Assert.assertFalse(filter.accept("test/Test/12/Test.txt"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the wildcard file filter
|
||||
*/
|
||||
@Test
|
||||
public void wildCardTest() {
|
||||
//accept only file names with name in lower case and ends with .txt
|
||||
AbstractFileNameFilter filter = new WildcardFileNameFilter("*.txt");
|
||||
filter.setAcceptSubFolders(true);
|
||||
Assert.assertTrue(filter.accept("test.txt"));
|
||||
Assert.assertTrue(filter.accept("Test.txt"));
|
||||
Assert.assertTrue(filter.accept("test123.txt"));
|
||||
Assert.assertFalse(filter.accept("test.tx"));
|
||||
Assert.assertFalse(filter.accept("test"));
|
||||
Assert.assertTrue(filter.accept("test/test.txt"));
|
||||
Assert.assertTrue(filter.accept("test/Test/12/test.txt"));
|
||||
Assert.assertTrue(filter.accept("test/Test/12/Test.txt"));
|
||||
Assert.assertFalse(filter.accept("test/Test/12/Test.ext"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,462 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.integration.aws.s3;
|
||||
|
||||
import static junit.framework.Assert.assertEquals;
|
||||
import static junit.framework.Assert.assertFalse;
|
||||
import static junit.framework.Assert.assertNotNull;
|
||||
import static junit.framework.Assert.assertTrue;
|
||||
import static org.springframework.integration.aws.common.AWSTestUtils.assertFileContent;
|
||||
import static org.springframework.integration.aws.common.AWSTestUtils.getContentsRecursively;
|
||||
import static org.springframework.integration.aws.common.AWSTestUtils.md5Hash;
|
||||
import static org.springframework.integration.aws.common.AWSTestUtils.writeToFile;
|
||||
import static org.springframework.integration.aws.s3.AmazonS3OperationsMockingUtil.BUCKET;
|
||||
import static org.springframework.integration.aws.s3.AmazonS3OperationsMockingUtil.mockAmazonS3Operations;
|
||||
import static org.springframework.integration.aws.s3.AmazonS3OperationsMockingUtil.mockS3Operations;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.apache.commons.codec.binary.Base64;
|
||||
import org.apache.commons.codec.binary.Hex;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
import org.springframework.integration.aws.s3.core.AmazonS3Operations;
|
||||
import org.springframework.integration.test.util.TestUtils;
|
||||
|
||||
/**
|
||||
* Test class for {@link InboundFileSynchronizationImpl}
|
||||
*
|
||||
* @author Amol Nayak
|
||||
*
|
||||
* @since 0.5
|
||||
*
|
||||
*/
|
||||
public class InboundFileSynchronizationImplTests {
|
||||
|
||||
private static AmazonS3Operations operations;
|
||||
|
||||
@Rule
|
||||
public final TemporaryFolder tempFolder = new TemporaryFolder();
|
||||
|
||||
@BeforeClass
|
||||
public static void setup() {
|
||||
operations = mockS3Operations();
|
||||
}
|
||||
|
||||
private InboundFileSynchronizationImpl getInboundFileSynchronizationImpl() {
|
||||
//uncomment the below code if you want to execute against the actual bucket
|
||||
//but before that you need to do the following
|
||||
//create a bucket and set that in the constant BUCKET in the class
|
||||
//1. add a file test.txt with content test.txt to the root.
|
||||
//2. create a folder sub1 and add a file test.txt to it with content sub1/test.txt.
|
||||
//3. create a folder sub1/sub11 and add a file test.txt to it with content sub1/sub11/test.txt.
|
||||
//4. create a folder sub2 and add a file test.txt to it with content sub/test.txt.
|
||||
|
||||
// DefaultAmazonS3Operations operations = new DefaultAmazonS3Operations(AmazonWSTestUtils.getCredentials());
|
||||
// try {
|
||||
// operations.afterPropertiesSet();
|
||||
// } catch (Exception e) {
|
||||
// e.printStackTrace();
|
||||
// }
|
||||
|
||||
InboundFileSynchronizationImpl sync = new InboundFileSynchronizationImpl(operations,
|
||||
new InboundLocalFileOperationsImpl());
|
||||
return sync;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Tests with {@link AmazonS3Operations} instance as null
|
||||
*/
|
||||
@Test(expected=IllegalArgumentException.class)
|
||||
public void withS3OperationsAsNull() {
|
||||
new InboundFileSynchronizationImpl(null, new InboundLocalFileOperationsImpl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests with {@link InboundLocalFileOperations} instance as null
|
||||
*/
|
||||
@Test(expected=IllegalArgumentException.class)
|
||||
public void withLocalFileOperationsAsNull() {
|
||||
new InboundFileSynchronizationImpl(operations, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests after setting both the wildcard and filename regex
|
||||
*/
|
||||
@Test(expected=IllegalArgumentException.class)
|
||||
public void withBothWildCardAndRegex() throws Exception {
|
||||
InboundFileSynchronizationImpl sync = getInboundFileSynchronizationImpl();
|
||||
sync.setFileWildcard("*.txt");
|
||||
sync.setFileNamePattern("[a-z]+\\.txt");
|
||||
sync.afterPropertiesSet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets non of regex and wildcard
|
||||
*/
|
||||
@Test
|
||||
public void withNoneOfRegexAndWildcard() throws Exception {
|
||||
InboundFileSynchronizationImpl impl = getInboundFileSynchronizationImpl();
|
||||
impl.afterPropertiesSet();
|
||||
assertEquals(AlwaysTrueFileNamefilter.class,
|
||||
TestUtils.getPropertyValue(impl, "filter", FileNameFilter.class).getClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests after setting filename regex only
|
||||
*/
|
||||
@Test
|
||||
public void withRegexOnly() throws Exception {
|
||||
InboundFileSynchronizationImpl sync = getInboundFileSynchronizationImpl();
|
||||
sync.setFileNamePattern("[a-z]+\\.txt");
|
||||
sync.afterPropertiesSet();
|
||||
FileNameFilter filter = TestUtils.getPropertyValue(sync, "filter",FileNameFilter.class);
|
||||
assertNotNull(filter);
|
||||
assertEquals(RegexFileNameFilter.class, filter.getClass());
|
||||
assertEquals("[a-z]+\\.txt",
|
||||
TestUtils.getPropertyValue(filter, "filter.pattern.pattern", String.class));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests after setting filename wildcard only
|
||||
*/
|
||||
@Test
|
||||
public void withWildcardOnly() throws Exception {
|
||||
InboundFileSynchronizationImpl sync = getInboundFileSynchronizationImpl();
|
||||
sync.setFileWildcard("*.txt");
|
||||
sync.afterPropertiesSet();
|
||||
FileNameFilter filter = TestUtils.getPropertyValue(sync, "filter",FileNameFilter.class);
|
||||
assertNotNull(filter);
|
||||
assertEquals(WildcardFileNameFilter.class, filter.getClass());
|
||||
assertEquals("*.txt",
|
||||
TestUtils.getPropertyValue(filter, "filter.wildcards", String[].class)[0]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets the {@link InboundFileSynchronizationImpl#setAcceptSubFolders(boolean) as true
|
||||
*/
|
||||
@Test
|
||||
public void withAcceptSubfolderAsTrue() throws Exception {
|
||||
InboundFileSynchronizationImpl impl = getInboundFileSynchronizationImpl();
|
||||
impl.setAcceptSubFolders(true);
|
||||
impl.afterPropertiesSet();
|
||||
assertTrue(TestUtils.getPropertyValue(impl, "filter.acceptSubFolders",Boolean.class).booleanValue());
|
||||
assertTrue(TestUtils.getPropertyValue(impl, "fileOperations.createDirectoriesIfRequired",
|
||||
Boolean.class).booleanValue());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Invokes with remote directory as / and create directory set to true
|
||||
*/
|
||||
@Test
|
||||
public void withRemoteAsRootAndCreateDirectoryToTrue() throws Exception {
|
||||
setupMock();
|
||||
String rootDirectoryPath = tempFolder.getRoot().getAbsolutePath();
|
||||
String path = String.format("%s%s%s",
|
||||
rootDirectoryPath,File.separator,"test.txt");
|
||||
File fileOne = new File(path);
|
||||
path = String.format("%s%s%s%s%s",
|
||||
rootDirectoryPath,File.separator,"sub1",File.separator,"test.txt");
|
||||
File fileTwo = new File(path);
|
||||
path = String.format("%s%s%s%s%s%s%s",
|
||||
rootDirectoryPath,File.separator,"sub1",File.separator,"sub11",File.separator,"test.txt");
|
||||
File fileThree = new File(path);;
|
||||
path = String.format("%s%s%s%s%s",
|
||||
rootDirectoryPath,File.separator,"sub2",File.separator,"test.txt");
|
||||
File fileFour = new File(path);
|
||||
assertFalse(fileOne.exists());
|
||||
assertFalse(fileTwo.exists());
|
||||
assertFalse(fileThree.exists());
|
||||
assertFalse(fileFour.exists());
|
||||
InboundFileSynchronizationImpl impl = getInboundFileSynchronizationImpl();
|
||||
impl.setAcceptSubFolders(true);
|
||||
impl.afterPropertiesSet();
|
||||
impl.synchronizeToLocalDirectory(tempFolder.getRoot(), BUCKET, "/");
|
||||
assertTrue(fileOne.exists());
|
||||
assertFileContent(fileOne, "test.txt");
|
||||
assertTrue(fileTwo.exists());
|
||||
assertFileContent(fileTwo, "sub1/test.txt");
|
||||
assertTrue(fileThree.exists());
|
||||
assertFileContent(fileThree, "sub1/sub11/test.txt");
|
||||
assertTrue(fileFour.exists());
|
||||
assertFileContent(fileFour, "sub2/test.txt");
|
||||
assertEquals(4, getContentsRecursively(tempFolder.getRoot()).size());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Invokes with remote directory as / and create directory set to false
|
||||
*/
|
||||
@Test
|
||||
public void withRemoteAsRootAndCreateDirectoryToFalse() throws Exception {
|
||||
setupMock();
|
||||
String rootDirectoryPath = tempFolder.getRoot().getAbsolutePath();
|
||||
String path = String.format("%s%s%s",
|
||||
rootDirectoryPath,File.separator,"test.txt");
|
||||
File fileOne = new File(path);
|
||||
assertFalse(fileOne.exists());
|
||||
InboundFileSynchronizationImpl impl = getInboundFileSynchronizationImpl();
|
||||
impl.setAcceptSubFolders(false);
|
||||
impl.afterPropertiesSet();
|
||||
impl.synchronizeToLocalDirectory(tempFolder.getRoot(), BUCKET, "/");
|
||||
assertTrue(fileOne.exists());
|
||||
assertFileContent(fileOne, "test.txt");
|
||||
assertEquals(1, getContentsRecursively(tempFolder.getRoot()).size());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Invokes with remote directory as /sub1 and create directory set to true
|
||||
*/
|
||||
@Test
|
||||
public void withRemoteAssub1AndCreateDirectoryToTrue() throws Exception {
|
||||
setupMock();
|
||||
String rootDirectoryPath = tempFolder.getRoot().getAbsolutePath();
|
||||
String path = String.format("%s%s%s%s%s",
|
||||
rootDirectoryPath,File.separator,"sub1",File.separator,"test.txt");
|
||||
File fileOne = new File(path);
|
||||
path = String.format("%s%s%s%s%s%s%s",
|
||||
rootDirectoryPath,File.separator,"sub1",File.separator,"sub11",File.separator,"test.txt");
|
||||
File fileTwo = new File(path);
|
||||
assertFalse(fileOne.exists());
|
||||
assertFalse(fileTwo.exists());
|
||||
InboundFileSynchronizationImpl impl = getInboundFileSynchronizationImpl();
|
||||
impl.setAcceptSubFolders(true);
|
||||
impl.afterPropertiesSet();
|
||||
impl.synchronizeToLocalDirectory(tempFolder.getRoot(), BUCKET, "/sub1");
|
||||
assertTrue(fileOne.exists());
|
||||
assertFileContent(fileOne, "sub1/test.txt");
|
||||
assertTrue(fileTwo.exists());
|
||||
assertFileContent(fileTwo, "sub1/sub11/test.txt");
|
||||
assertEquals(2, getContentsRecursively(tempFolder.getRoot()).size());
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes with remote directory as /sub1 and create directory set to false
|
||||
*/
|
||||
@Test
|
||||
public void withRemoteAssub1AndCreateDirectoryToFalse() throws Exception {
|
||||
setupMock();
|
||||
String rootDirectoryPath = tempFolder.getRoot().getAbsolutePath();
|
||||
String path = String.format("%s%s%s%s%s",
|
||||
rootDirectoryPath,File.separator,"sub1",File.separator,"test.txt");
|
||||
File file = new File(path);
|
||||
assertFalse(file.exists());
|
||||
InboundFileSynchronizationImpl impl = getInboundFileSynchronizationImpl();
|
||||
impl.setAcceptSubFolders(false);
|
||||
impl.afterPropertiesSet();
|
||||
impl.synchronizeToLocalDirectory(tempFolder.getRoot(), BUCKET, "/sub1");
|
||||
assertTrue(file.exists());
|
||||
assertFileContent(file, "sub1/test.txt");
|
||||
assertEquals(1, getContentsRecursively(tempFolder.getRoot()).size());
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes with remote directory as / and create directory set to false
|
||||
* The two files test.txt and sub1/test.txt would already be present on
|
||||
* the file system. Both test.txt and sub/test.txt will have content different that the remote one.
|
||||
* test.txt will be replaced and sub/test.txt will not be replaced.
|
||||
*
|
||||
*/
|
||||
@Test
|
||||
public void withRemoteAsRootAndCreateDirectoryToFalse2() throws Exception {
|
||||
setupMock();
|
||||
String rootDirectoryPath = tempFolder.getRoot().getAbsolutePath();
|
||||
//create test.txt and sub1/test.txt
|
||||
|
||||
String path = String.format("%s%s%s",
|
||||
rootDirectoryPath,File.separator,"test.txt");
|
||||
writeToFile(path, "OldContents");
|
||||
File fileOne = new File(path);
|
||||
assertTrue(fileOne.exists());
|
||||
assertFileContent(fileOne, "OldContents");
|
||||
|
||||
path = String.format("%s%s%s%s%s",
|
||||
rootDirectoryPath,File.separator,"sub1",File.separator,"test.txt");
|
||||
writeToFile(path, "OldContents");
|
||||
File fileTwo = new File(path);
|
||||
assertTrue(fileTwo.exists());
|
||||
assertFileContent(fileTwo, "OldContents");
|
||||
|
||||
InboundFileSynchronizationImpl impl = getInboundFileSynchronizationImpl();
|
||||
impl.setAcceptSubFolders(false);
|
||||
impl.afterPropertiesSet();
|
||||
impl.synchronizeToLocalDirectory(tempFolder.getRoot(), BUCKET, "/");
|
||||
assertTrue(fileOne.exists());
|
||||
assertFileContent(fileOne, "test.txt");
|
||||
|
||||
assertTrue(fileTwo.exists());
|
||||
assertFileContent(fileTwo, "OldContents");
|
||||
|
||||
assertEquals(2, getContentsRecursively(tempFolder.getRoot()).size());
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes with remote directory as / and create directory set to true
|
||||
* The two files test.txt and sub1/test.txt would already be present on
|
||||
* the file system. Both test.txt and sub/test.txt will have content different that the remote one.
|
||||
* test.txt and sub/test.txt both will be replaced.
|
||||
*
|
||||
*/
|
||||
@Test
|
||||
public void withRemoteAsRootAndCreateDirectoryToTrue2() throws Exception {
|
||||
setupMock();
|
||||
String rootDirectoryPath = tempFolder.getRoot().getAbsolutePath();
|
||||
//create test.txt and sub1/test.txt
|
||||
|
||||
String path = String.format("%s%s%s",
|
||||
rootDirectoryPath,File.separator,"test.txt");
|
||||
writeToFile(path, "OldContents");
|
||||
File fileOne = new File(path);
|
||||
assertTrue(fileOne.exists());
|
||||
assertFileContent(fileOne, "OldContents");
|
||||
|
||||
path = String.format("%s%s%s%s%s",
|
||||
rootDirectoryPath,File.separator,"sub1",File.separator,"test.txt");
|
||||
writeToFile(path, "OldContents");
|
||||
File fileTwo = new File(path);
|
||||
assertTrue(fileTwo.exists());
|
||||
assertFileContent(fileTwo, "OldContents");
|
||||
|
||||
InboundFileSynchronizationImpl impl = getInboundFileSynchronizationImpl();
|
||||
impl.setAcceptSubFolders(true);
|
||||
impl.afterPropertiesSet();
|
||||
impl.synchronizeToLocalDirectory(tempFolder.getRoot(), BUCKET, "/");
|
||||
assertTrue(fileOne.exists());
|
||||
assertFileContent(fileOne, "test.txt");
|
||||
|
||||
assertTrue(fileTwo.exists());
|
||||
assertFileContent(fileTwo, "sub1/test.txt");
|
||||
|
||||
assertEquals(4, getContentsRecursively(tempFolder.getRoot()).size());
|
||||
}
|
||||
|
||||
/**
|
||||
* The case is slightly different then previous ones.
|
||||
* We list from the /sub1 with /sub1/test.txt and /sub1/sub11/test.txt
|
||||
* having different contents, however the etag of
|
||||
* /sub1/sub11/test.txt is same as remote one hence the local one should not get replaced
|
||||
* where as /sub1/test.txt should.
|
||||
*
|
||||
*/
|
||||
@Test
|
||||
public void withRemoteAsRootAndCreateDirectoryToTrue3() throws Exception {
|
||||
mockAmazonS3Operations(Arrays.asList(
|
||||
new String[]{"test.txt","test.txt",md5Hash("test.txt"),null},
|
||||
new String[]{"sub1/test.txt","sub1/test.txt",md5Hash("sub1/test.txt"),null},
|
||||
new String[]{"sub1/sub11/test.txt","sub1/sub11/test.txt",md5Hash("OldContents"),null},
|
||||
new String[]{"sub2/test.txt","sub2/test.txt",md5Hash("sub2/test.txt"),null}
|
||||
));
|
||||
|
||||
withinSub1FolderTests(true);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* The scenario tests by listing the directory /sub1 which has two files
|
||||
* /sub1/test.txt and /sub1/sub11/test.txt. The MD5 of the file will be absent and the etag is
|
||||
* for MultiUpload. This should force replace the file irrespective of the content.
|
||||
*/
|
||||
@Test
|
||||
public void withMultipartUploadForceReplace() throws Exception {
|
||||
mockAmazonS3Operations(Arrays.asList(
|
||||
new String[]{"sub1/test.txt","sub1/test.txt",null,
|
||||
new String(Hex.encodeHex(Base64.decodeBase64(md5Hash("SomeContentSub1").getBytes()))) + "-1"},
|
||||
new String[]{"sub1/sub11/test.txt","sub1/sub11/test.txt",null,
|
||||
new String(Hex.encodeHex(Base64.decodeBase64(md5Hash("SomeContentSub1/Sub11").getBytes()))) + "-1"}
|
||||
));
|
||||
withinSub1FolderTests(false);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* The scenario will test with two files present in /sub1 directory, /sub1/test.txt and
|
||||
* /sub1/sub11/test.txt. Now both these files have multipart upload etag but both have
|
||||
* MD5 hash in the user's metadata. The contents of both the files is different than the one
|
||||
* on remote but /sub/test.txt has MD5 sum same as remote, so this should not get replaced
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
@Test
|
||||
public void withMultipartUploadWithMD5Metadata() throws Exception {
|
||||
mockAmazonS3Operations(Arrays.asList(
|
||||
new String[]{"sub1/test.txt","sub1/test.txt",md5Hash("sub1/test.txt"),
|
||||
new String(Hex.encodeHex(Base64.decodeBase64(md5Hash("sub1/test.txt").getBytes()))) + "-1"},
|
||||
new String[]{"sub1/sub11/test.txt","sub1/sub11/test.txt",md5Hash("OldContents"),
|
||||
new String(Hex.encodeHex(Base64.decodeBase64(md5Hash("OldContents").getBytes()))) + "-1"}
|
||||
));
|
||||
|
||||
withinSub1FolderTests(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Private method that extracts the common assertion logic for the files in sub1 folder
|
||||
* @throws Exception
|
||||
*/
|
||||
private void withinSub1FolderTests(boolean acceptSubfolder) throws Exception {
|
||||
String rootDirectoryPath = tempFolder.getRoot().getAbsolutePath();
|
||||
|
||||
//create sub1/sub11/test.txt and sub1/test.txt
|
||||
String path = String.format("%s%s%s%s%s%s%s",
|
||||
rootDirectoryPath,File.separator,"sub1",File.separator,"sub11",File.separator,"test.txt");
|
||||
writeToFile(path, "OldContents");
|
||||
File fileOne = new File(path);
|
||||
assertTrue(fileOne.exists());
|
||||
assertFileContent(fileOne, "OldContents");
|
||||
|
||||
path = String.format("%s%s%s%s%s",
|
||||
rootDirectoryPath,File.separator,"sub1",File.separator,"test.txt");
|
||||
writeToFile(path, "OldContents");
|
||||
File fileTwo = new File(path);
|
||||
assertTrue(fileTwo.exists());
|
||||
assertFileContent(fileTwo, "OldContents");
|
||||
|
||||
InboundFileSynchronizationImpl impl = getInboundFileSynchronizationImpl();
|
||||
impl.setAcceptSubFolders(acceptSubfolder);
|
||||
impl.afterPropertiesSet();
|
||||
impl.synchronizeToLocalDirectory(tempFolder.getRoot(), BUCKET, "/sub1");
|
||||
|
||||
assertTrue(fileOne.exists());
|
||||
assertFileContent(fileOne, "OldContents");
|
||||
|
||||
assertTrue(fileTwo.exists());
|
||||
assertFileContent(fileTwo, "sub1/test.txt");
|
||||
|
||||
assertEquals(2, getContentsRecursively(tempFolder.getRoot()).size());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Private helper method that will be setup mock s3 operations to give an illusion
|
||||
* that it has 4 objects in the remote bucket
|
||||
*
|
||||
*/
|
||||
private void setupMock() {
|
||||
mockAmazonS3Operations(Arrays.asList(
|
||||
new String[]{"test.txt","test.txt",md5Hash("test.txt"),null},
|
||||
new String[]{"sub1/test.txt","sub1/test.txt",md5Hash("sub1/test.txt"),null},
|
||||
new String[]{"sub1/sub11/test.txt","sub1/sub11/test.txt",md5Hash("sub1/sub11/test.txt"),null},
|
||||
new String[]{"sub2/test.txt","sub2/test.txt",md5Hash("sub2/test.txt"),null}
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.integration.aws.s3;
|
||||
|
||||
import static org.springframework.integration.aws.common.AWSTestUtils.assertFileContent;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.util.Collections;
|
||||
|
||||
import junit.framework.Assert;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
import org.springframework.integration.test.util.TestUtils;
|
||||
|
||||
/**
|
||||
* The test class for {@link InboundLocalFileOperationsImpl} that is used to perform
|
||||
* operations on local file system
|
||||
*
|
||||
* @author Amol Nayak
|
||||
*
|
||||
* @since 0.5
|
||||
*
|
||||
*/
|
||||
public class InboundLocalFileOperationsImplTests {
|
||||
|
||||
@Rule
|
||||
public TemporaryFolder tempFolder = new TemporaryFolder();
|
||||
|
||||
/**
|
||||
* Tries registering a null listener with the class
|
||||
*/
|
||||
@Test(expected=IllegalArgumentException.class)
|
||||
public void withNullListener() {
|
||||
InboundLocalFileOperations operations = new InboundLocalFileOperationsImpl();
|
||||
operations.addEventListener(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries setting the listeners which is an empty list
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test(expected=IllegalArgumentException.class)
|
||||
public void setEmptyListeners() {
|
||||
InboundLocalFileOperations operations = new InboundLocalFileOperationsImpl();
|
||||
operations.setEventListeners(Collections.EMPTY_LIST);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test case for setting a temporary suffix that begins with a .
|
||||
*/
|
||||
@Test
|
||||
public void setTempSuffixBeginningWithDot() {
|
||||
InboundLocalFileOperations operations = new InboundLocalFileOperationsImpl();
|
||||
operations.setTemporaryFileSuffix(".write");
|
||||
Assert.assertEquals(".write", TestUtils.getPropertyValue(operations, "tempFileSuffix"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test case for setting a temporary suffix that does not begins with a .
|
||||
*/
|
||||
@Test
|
||||
public void setTempSuffixNotBeginningWithDot() {
|
||||
InboundLocalFileOperations operations = new InboundLocalFileOperationsImpl();
|
||||
operations.setTemporaryFileSuffix("write");
|
||||
Assert.assertEquals(".write", TestUtils.getPropertyValue(operations, "tempFileSuffix"));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*Since the provided directory is null, we expect an {@link IllegalArgumentException}
|
||||
*/
|
||||
@Test(expected=IllegalArgumentException.class)
|
||||
public void writeWithNullDirectory() throws Exception {
|
||||
InboundLocalFileOperations operations = new InboundLocalFileOperationsImpl();
|
||||
operations.writeToFile(null, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
*Since the provided file name is null, we expect an {@link IllegalArgumentException}
|
||||
*/
|
||||
@Test(expected=IllegalArgumentException.class)
|
||||
public void writeWithNullFileName() throws Exception {
|
||||
InboundLocalFileOperations operations = new InboundLocalFileOperationsImpl();
|
||||
operations.writeToFile(tempFolder.newFolder("Test"), null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
*Since the provided stream as null, we expect an {@link IllegalArgumentException}
|
||||
*/
|
||||
@Test(expected=IllegalArgumentException.class)
|
||||
public void writeWithNullStream() throws Exception {
|
||||
InboundLocalFileOperations operations = new InboundLocalFileOperationsImpl();
|
||||
operations.writeToFile(tempFolder.newFolder("Test"), "TestFile.txt", null);
|
||||
}
|
||||
|
||||
/**
|
||||
*Provided {@link File} for directory exists and is not a directory
|
||||
*/
|
||||
@Test(expected=IllegalArgumentException.class)
|
||||
public void writeWithExistantNonDirectory() throws Exception {
|
||||
InboundLocalFileOperations operations = new InboundLocalFileOperationsImpl();
|
||||
operations.writeToFile(tempFolder.newFile("Test"), "TestFile.txt", new ByteArrayInputStream(new byte[]{}));
|
||||
}
|
||||
|
||||
/**
|
||||
*Provided {@link File} for directory does not exist exists and the create flag is false
|
||||
*/
|
||||
@Test(expected=IllegalArgumentException.class)
|
||||
public void writeWithNonExistentDirectory() throws Exception {
|
||||
InboundLocalFileOperations operations = new InboundLocalFileOperationsImpl();
|
||||
operations.writeToFile(new File(tempFolder.getRoot() + "SomeDir"), "TestFile.txt", new ByteArrayInputStream(new byte[]{}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes some test content to the file
|
||||
*/
|
||||
@Test
|
||||
public void writeTestContentToFile() throws Exception {
|
||||
InboundLocalFileOperations operations = new InboundLocalFileOperationsImpl();
|
||||
operations.setCreateDirectoriesIfRequired(true);
|
||||
File directory = new File(tempFolder.getRoot() + File.separator + "someNestedDir");
|
||||
File tempFile = new File(directory.getAbsolutePath() + File.separator + "SomeFileName.txt.writing");
|
||||
File permFile = new File(directory.getAbsolutePath() + File.separator + "SomeFileName.txt");
|
||||
Assert.assertFalse(tempFile.exists());
|
||||
Assert.assertFalse(permFile.exists());
|
||||
operations.writeToFile(directory, "SomeFileName.txt", new ByteArrayInputStream("Some Test Content".getBytes()));
|
||||
Assert.assertFalse(tempFile.exists());
|
||||
Assert.assertTrue(permFile.exists());
|
||||
//Check the content
|
||||
assertFileContent(permFile, "Some Test Content");
|
||||
//TODO: Test FileEventHandlers
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes some test content to the file with teh given target file existent
|
||||
*/
|
||||
@Test
|
||||
public void writeTestContentWithTargetExistent() throws Exception {
|
||||
InboundLocalFileOperations operations = new InboundLocalFileOperationsImpl();
|
||||
operations.setCreateDirectoriesIfRequired(true);
|
||||
File directory = tempFolder.newFolder("someNestedDir");
|
||||
File tempFile = new File(directory.getAbsolutePath() + File.separator + "SomeFileName.txt.writing");
|
||||
File permFile = new File(directory.getAbsolutePath() + File.separator + "SomeFileName.txt");
|
||||
permFile.createNewFile();
|
||||
//Write Some content
|
||||
FileOutputStream fos = new FileOutputStream(permFile);
|
||||
fos.write("Some Old Contents".getBytes());
|
||||
fos.close();
|
||||
assertFileContent(permFile, "Some Old Contents");
|
||||
Assert.assertFalse(tempFile.exists());
|
||||
Assert.assertTrue(permFile.exists());
|
||||
operations.writeToFile(directory, "SomeFileName.txt", new ByteArrayInputStream("Some Test Content".getBytes()));
|
||||
Assert.assertFalse(tempFile.exists());
|
||||
Assert.assertTrue(permFile.exists());
|
||||
//Check the content
|
||||
assertFileContent(permFile, "Some Test Content");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.integration.aws.s3.config.xml;
|
||||
|
||||
import org.springframework.integration.aws.s3.core.AmazonS3Object;
|
||||
import org.springframework.integration.aws.s3.core.AmazonS3Operations;
|
||||
import org.springframework.integration.aws.s3.core.PaginatedObjectsView;
|
||||
|
||||
/**
|
||||
* The dummy {@link AmazonS3Operations} for tests
|
||||
*
|
||||
* @author Amol Nayak
|
||||
*
|
||||
* @since 0.5
|
||||
*
|
||||
*/
|
||||
public class AmazonS3DummyOperations implements AmazonS3Operations {
|
||||
|
||||
@Override
|
||||
public PaginatedObjectsView listObjects(String bucketName,
|
||||
String folder, String nextMarker, int pageSize) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putObject(String bucketName, String folder,
|
||||
String objectName, AmazonS3Object s3Object) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public AmazonS3Object getObject(String bucketName, String folder,
|
||||
String objectName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean removeObject(String bucketName, String folder,
|
||||
String objectName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.integration.aws.s3.config.xml;
|
||||
|
||||
import static junit.framework.Assert.assertEquals;
|
||||
import static org.springframework.integration.test.util.TestUtils.getPropertyValue;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.URI;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.springframework.beans.factory.BeanDefinitionStoreException;
|
||||
import org.springframework.context.support.ClassPathXmlApplicationContext;
|
||||
import org.springframework.integration.aws.s3.AmazonS3InboundSynchronizationMessageSource;
|
||||
import org.springframework.integration.aws.s3.core.AmazonS3Operations;
|
||||
import org.springframework.integration.aws.s3.core.DefaultAmazonS3Operations;
|
||||
import org.springframework.integration.endpoint.SourcePollingChannelAdapter;
|
||||
|
||||
/**
|
||||
* The test case class for S3 inbound channel adapter
|
||||
*
|
||||
* @author Amol Nayak
|
||||
*
|
||||
* @since 0.5
|
||||
*
|
||||
*/
|
||||
public class AmazonS3InboundChannelAdapterParserTests {
|
||||
|
||||
|
||||
/**
|
||||
* Tests the inbound channel adapter definition with a valid combination of attributes
|
||||
*/
|
||||
@Test
|
||||
public void withValidAttributeValues() {
|
||||
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:s3-valid-inbound-cases.xml");
|
||||
SourcePollingChannelAdapter valid = ctx.getBean("validInbound", SourcePollingChannelAdapter.class);
|
||||
AmazonS3InboundSynchronizationMessageSource source = getPropertyValue(valid, "source", AmazonS3InboundSynchronizationMessageSource.class);
|
||||
assertEquals("TestBucket", getPropertyValue(source, "bucket"));
|
||||
assertEquals(".temp", getPropertyValue(source, "temporarySuffix"));
|
||||
assertEquals(new File(System.getProperty("java.io.tmpdir")), getPropertyValue(source, "directory"));
|
||||
assertEquals("remote", getPropertyValue(source, "remoteDirectory"));
|
||||
assertEquals(true, getPropertyValue(source, "acceptSubFolders", Boolean.class).booleanValue());
|
||||
assertEquals(100, getPropertyValue(source, "maxObjectsPerBatch", Integer.class).intValue());
|
||||
assertEquals("[A-Za-z0-9]+\\\\.txt", getPropertyValue(source, "fileNameRegex"));
|
||||
|
||||
//test the second definition with custom attributes
|
||||
valid = ctx.getBean("validInboundWithCustomOps", SourcePollingChannelAdapter.class);
|
||||
source = getPropertyValue(valid, "source", AmazonS3InboundSynchronizationMessageSource.class);
|
||||
AmazonS3Operations s3Operations = getPropertyValue(source, "s3Operations", AmazonS3Operations.class);
|
||||
assertEquals(AmazonS3DummyOperations.class, s3Operations.getClass());
|
||||
|
||||
//test with aws endpoint set
|
||||
valid = ctx.getBean("withAWSEndpoint", SourcePollingChannelAdapter.class);
|
||||
source = getPropertyValue(valid, "source", AmazonS3InboundSynchronizationMessageSource.class);
|
||||
s3Operations = getPropertyValue(source, "s3Operations", AmazonS3Operations.class);
|
||||
assertEquals(DefaultAmazonS3Operations.class, s3Operations.getClass());
|
||||
assertEquals("https://s3-eu-west-1.amazonaws.com", getPropertyValue(s3Operations, "client.endpoint", URI.class).toString());
|
||||
|
||||
|
||||
ctx.close();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests with a definition where none of directory and directory-expression attributes are provided
|
||||
*/
|
||||
@Test(expected=BeanDefinitionStoreException.class)
|
||||
public void withNoneOfDirectoryExprAndDirectory() {
|
||||
new ClassPathXmlApplicationContext("classpath:s3-with-none-of-direxpr-and-dir.xml");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.integration.aws.s3.config.xml;
|
||||
|
||||
import static junit.framework.Assert.assertEquals;
|
||||
import static junit.framework.Assert.assertNotNull;
|
||||
import static org.springframework.integration.test.util.TestUtils.getPropertyValue;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.springframework.beans.factory.BeanCreationException;
|
||||
import org.springframework.beans.factory.BeanDefinitionStoreException;
|
||||
import org.springframework.context.support.ClassPathXmlApplicationContext;
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.expression.common.LiteralExpression;
|
||||
import org.springframework.expression.spel.standard.SpelExpression;
|
||||
import org.springframework.integration.Message;
|
||||
import org.springframework.integration.aws.s3.AmazonS3MessageHandler;
|
||||
import org.springframework.integration.aws.s3.FileNameGenerationStrategy;
|
||||
import org.springframework.integration.aws.s3.core.AmazonS3Operations;
|
||||
import org.springframework.integration.aws.s3.core.DefaultAmazonS3Operations;
|
||||
import org.springframework.integration.endpoint.EventDrivenConsumer;
|
||||
|
||||
/**
|
||||
* The test case for the aws-s3 namespace's {@link AmazonS3OutboundChannelAdapterParser} class
|
||||
*
|
||||
* @author Amol Nayak
|
||||
*
|
||||
* @since 0.5
|
||||
*
|
||||
*/
|
||||
public class AmazonS3OutboundChannelAdapterParserTests {
|
||||
|
||||
|
||||
/**
|
||||
* Test case for the xml definition with a custom implementation of {@link AmazonS3Operations}
|
||||
*
|
||||
*/
|
||||
@Test
|
||||
public void withCustomOperations() {
|
||||
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:s3-valid-outbound-cases.xml");
|
||||
EventDrivenConsumer consumer = ctx.getBean("withCustomService",EventDrivenConsumer.class);
|
||||
AmazonS3MessageHandler handler = getPropertyValue(consumer, "handler", AmazonS3MessageHandler.class);
|
||||
assertEquals(AmazonS3DummyOperations.class, getPropertyValue(handler, "operations").getClass());
|
||||
Expression expression =
|
||||
getPropertyValue(handler, "remoteDirectoryProcessor.expression",Expression.class);
|
||||
assertNotNull(expression);
|
||||
assertEquals(LiteralExpression.class, expression.getClass());
|
||||
assertEquals("/", getPropertyValue(expression, "literalValue", String.class));
|
||||
ctx.destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test case for the xml definition with the default implementation of {@link AmazonS3Operations}
|
||||
*/
|
||||
@Test
|
||||
public void withDefaultOperationsImplementation() {
|
||||
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:s3-valid-outbound-cases.xml");
|
||||
EventDrivenConsumer consumer = ctx.getBean("withDefaultServices",EventDrivenConsumer.class);
|
||||
AmazonS3MessageHandler handler = getPropertyValue(consumer, "handler", AmazonS3MessageHandler.class);
|
||||
assertEquals(DefaultAmazonS3Operations.class, getPropertyValue(handler, "operations").getClass());
|
||||
Expression expression =
|
||||
getPropertyValue(handler, "remoteDirectoryProcessor.expression",Expression.class);
|
||||
assertNotNull(expression);
|
||||
assertEquals(SpelExpression.class, expression.getClass());
|
||||
assertEquals("headers['remoteDirectory']", getPropertyValue(expression, "expression", String.class));
|
||||
assertEquals("TestBucket", getPropertyValue(handler, "bucket", String.class));
|
||||
assertEquals("US-ASCII", getPropertyValue(handler, "charset", String.class));
|
||||
assertEquals("dummy", getPropertyValue(handler, "credentials.accessKey", String.class));
|
||||
assertEquals("dummy", getPropertyValue(handler, "credentials.secretKey", String.class));
|
||||
assertEquals("dummy", getPropertyValue(handler, "operations.credentials.accessKey", String.class));
|
||||
assertEquals("dummy", getPropertyValue(handler, "operations.credentials.secretKey", String.class));
|
||||
assertEquals(5120, getPropertyValue(handler, "operations.multipartUploadThreshold", Long.class).longValue());
|
||||
assertEquals(".write", getPropertyValue(handler, "operations.temporaryFileSuffix", String.class));
|
||||
assertEquals(".write", getPropertyValue(handler, "fileNameGenerator.temporarySuffix", String.class));
|
||||
assertEquals("headers['name']", getPropertyValue(handler, "fileNameGenerator.fileNameExpression", String.class));
|
||||
assertEquals(ctx.getBean("executor"), getPropertyValue(handler, "operations.threadPoolExecutor"));
|
||||
ctx.destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test case for the xml definition with a custom implementation of {@link FileNameGenerationStrategy}
|
||||
*
|
||||
*/
|
||||
@Test
|
||||
public void withCustomNameGenerator() {
|
||||
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("s3-valid-outbound-cases.xml");
|
||||
EventDrivenConsumer consumer = ctx.getBean("withCustomNameGenerator",EventDrivenConsumer.class);
|
||||
AmazonS3MessageHandler handler = getPropertyValue(consumer, "handler", AmazonS3MessageHandler.class);
|
||||
assertEquals(DummyFileNameGenerator.class, getPropertyValue(handler, "fileNameGenerator").getClass());
|
||||
ctx.destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test case for the xml definition with a custom AWS endpoint
|
||||
*
|
||||
*/
|
||||
@Test
|
||||
public void withCustomEndpoint() {
|
||||
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("s3-valid-outbound-cases.xml");
|
||||
EventDrivenConsumer consumer = ctx.getBean("withCustomEndpoint",EventDrivenConsumer.class);
|
||||
AmazonS3MessageHandler handler = getPropertyValue(consumer, "handler", AmazonS3MessageHandler.class);
|
||||
assertEquals("http://s3-eu-west-1.amazonaws.com",
|
||||
getPropertyValue(handler, "operations.client.endpoint", URI.class).toString());
|
||||
ctx.destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Multi part upload should have a size of 5120 and above, any value less than 5120 will
|
||||
* thrown an exception
|
||||
*/
|
||||
@Test(expected=BeanCreationException.class)
|
||||
public void withMultiUploadLessthan5120() {
|
||||
new ClassPathXmlApplicationContext("s3-multiupload-lessthan-5120.xml");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test with both the custom file generator and expression attribute set.
|
||||
*/
|
||||
@Test(expected=BeanDefinitionStoreException.class)
|
||||
public void withBothFileGeneratorAndExpression() {
|
||||
new ClassPathXmlApplicationContext("s3-both-customfilegenerator-and-expression.xml");
|
||||
}
|
||||
|
||||
/**
|
||||
* When custom implementation of {@link AmazonS3Operations} is provided, the attributes
|
||||
* multipart-upload-threshold, temporary-directory, temporary-suffix and thread-pool-executor
|
||||
* are not allowed
|
||||
*/
|
||||
@Test(expected=BeanDefinitionStoreException.class)
|
||||
public void withCustomOperationsAndDisallowedAttributes() {
|
||||
new ClassPathXmlApplicationContext("s3-custom-operations-with-disallowed-attributes.xml");
|
||||
}
|
||||
|
||||
|
||||
public static class DummyFileNameGenerator implements FileNameGenerationStrategy {
|
||||
|
||||
@Override
|
||||
public String generateFileName(Message<?> message) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,629 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.integration.aws.s3.core;
|
||||
|
||||
import static org.springframework.integration.aws.common.AWSTestUtils.getCredentials;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import junit.framework.Assert;
|
||||
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
import org.springframework.integration.aws.core.AWSCredentials;
|
||||
import org.springframework.integration.aws.core.PropertiesAWSCredentials;
|
||||
|
||||
import com.amazonaws.auth.BasicAWSCredentials;
|
||||
import com.amazonaws.services.s3.AmazonS3Client;
|
||||
import com.amazonaws.services.s3.model.AccessControlList;
|
||||
import com.amazonaws.services.s3.model.GetObjectRequest;
|
||||
import com.amazonaws.services.s3.model.Grant;
|
||||
import com.amazonaws.services.s3.model.ObjectMetadata;
|
||||
import com.amazonaws.services.s3.model.S3Object;
|
||||
|
||||
/**
|
||||
* The abstract test class for testing all the common functionality for AWS operations
|
||||
* on S3 using the appropriate implementation provided by the subclass.
|
||||
*
|
||||
* Note: To run the test, you will have to create one bucket for yourself and
|
||||
* set the name in the {@link #BUCKET_NAME}
|
||||
*
|
||||
* @author Amol Nayak
|
||||
*
|
||||
* @since 0.5
|
||||
*
|
||||
*/
|
||||
public abstract class AbstractAmazonS3OperationsImplAWSTests {
|
||||
|
||||
private static final String VALID_CANONICAL_ID = "f854da004ee08cf4f8664334d288561c8512c508db9785388de7319ded85f8f3";
|
||||
|
||||
@Rule
|
||||
public static final TemporaryFolder temp = new TemporaryFolder();
|
||||
|
||||
//private static final String UPLOAD_SOURCE_DIRECTORY = System.getProperty("java.io.tmpdir") + "upload";
|
||||
|
||||
|
||||
//To run the test, you will have to create one one bucket for yourself and
|
||||
//set the name here
|
||||
protected static final String BUCKET_NAME = "com.si.aws.test.bucket";
|
||||
|
||||
private static AmazonS3Client client;
|
||||
private static PropertiesAWSCredentials credentials;
|
||||
|
||||
@BeforeClass
|
||||
public static final void setup() throws Exception {
|
||||
AWSCredentials credentials = getCredentials();
|
||||
client = new AmazonS3Client(
|
||||
new BasicAWSCredentials(credentials.getAccessKey(), credentials.getSecretKey()));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets the multipart threshold value of the upload to 5K,
|
||||
* this should get executed successfully
|
||||
*/
|
||||
@Test
|
||||
public void withMultipartThresholdWith5k() {
|
||||
AbstractAmazonS3Operations impl = getS3OperationsImplementation();
|
||||
impl.setMultipartUploadThreshold(5120);
|
||||
Assert.assertEquals(5120,impl.getMultipartUploadThreshold());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the multipart threshold value of the upload to a value < 5K,
|
||||
* should throw IllegalArgumentException
|
||||
*/
|
||||
@Test(expected=IllegalArgumentException.class)
|
||||
public void withMultipartThresholdWithLt5k() {
|
||||
getS3OperationsImplementation().setMultipartUploadThreshold(5000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the directory path as a null value.
|
||||
*/
|
||||
@Test(expected=IllegalArgumentException.class)
|
||||
public void setNullDirectoryString() {
|
||||
getS3OperationsImplementation().setTemporaryDirectory((String)null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pass a non null string that exists
|
||||
*/
|
||||
@Test
|
||||
public void setNonNullDirectoryString() {
|
||||
//this will exist
|
||||
String directory = System.getProperty("java.io.tmpdir");
|
||||
getS3OperationsImplementation().setTemporaryDirectory(directory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pass a String to a directory that doesn't exist
|
||||
*/
|
||||
@Test(expected=IllegalArgumentException.class)
|
||||
public void setNonExistentDirectory() {
|
||||
//getting the current time in millis and hope no folder with that name exists
|
||||
long current = System.currentTimeMillis();
|
||||
DefaultAmazonS3Operations s3Service = new DefaultAmazonS3Operations(credentials);
|
||||
s3Service.setTemporaryDirectory("./" + current);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the temporary file suffix to the null value
|
||||
*/
|
||||
@Test(expected=IllegalArgumentException.class)
|
||||
public void setNullTemporaryFileSuffix() {
|
||||
getS3OperationsImplementation().setTemporaryFileSuffix(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the temporary file suffix to a string that begins with a "."
|
||||
*/
|
||||
@Test
|
||||
public void setValidTempFileSuffixStartingWithDot() {
|
||||
AbstractAmazonS3Operations impl = getS3OperationsImplementation();
|
||||
impl.setTemporaryFileSuffix(".tempsuff");
|
||||
Assert.assertEquals(".tempsuff", impl.getTemporaryFileSuffix());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets the temporary file suffix to a string that does not begin with a "."
|
||||
*/
|
||||
@Test
|
||||
public void setValidTempFileSuffixStartingWithoutDot() {
|
||||
AbstractAmazonS3Operations impl = getS3OperationsImplementation();
|
||||
impl.setTemporaryFileSuffix("tmpsuff");
|
||||
Assert.assertEquals(".tmpsuff", impl.getTemporaryFileSuffix());
|
||||
}
|
||||
|
||||
//TODO. Test all the conditions that test the folder generation logic, null folder
|
||||
//null bucket etc
|
||||
|
||||
/**
|
||||
* The AWS Service test put a file with null bucket given
|
||||
*
|
||||
*/
|
||||
public void putToNullBucket() {
|
||||
AbstractAmazonS3Operations impl = getS3OperationsImplementation();
|
||||
impl.putObject(null, "/", "name", null);
|
||||
}
|
||||
|
||||
//TODO: Execute the following cases for putObject
|
||||
|
||||
/**
|
||||
* Executes the put object with a null bucket name, should throw
|
||||
* an {@link IllegalArgumentException}
|
||||
*/
|
||||
@Test(expected=IllegalArgumentException.class)
|
||||
public void putWithNullBucket() {
|
||||
AmazonS3Operations impl = getS3OperationsImplementation();
|
||||
impl.putObject(null, null, "SomeObject", null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the put object with a null object name, should throw
|
||||
* an {@link IllegalArgumentException}
|
||||
*/
|
||||
@Test(expected=IllegalArgumentException.class)
|
||||
public void withNullObjectName() {
|
||||
AmazonS3Operations impl = getS3OperationsImplementation();
|
||||
impl.putObject(BUCKET_NAME, "/", null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the put object with a null s3 object, should throw
|
||||
* an {@link IllegalArgumentException}
|
||||
*/
|
||||
@Test(expected=IllegalArgumentException.class)
|
||||
public void withNullS3Object() {
|
||||
AmazonS3Operations impl = getS3OperationsImplementation();
|
||||
impl.putObject(BUCKET_NAME, "/", "TestObjectName.txt", null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the put object with both file source and input stream provided
|
||||
*
|
||||
* an {@link IllegalArgumentException}
|
||||
*/
|
||||
@Test(expected=IllegalArgumentException.class)
|
||||
public void withBothFileSourceAndInputStream() throws Exception {
|
||||
String folder = temp.getRoot().getAbsolutePath();
|
||||
File file = new File(folder + File.separator + "SomeTestFile.txt");
|
||||
file.createNewFile();
|
||||
FileInputStream fin = new FileInputStream(file);
|
||||
new AmazonS3Object(null, null,fin, file);
|
||||
fin.close();
|
||||
file.delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the put object with none of file source and input stream provided
|
||||
*
|
||||
* an {@link IllegalArgumentException}
|
||||
*/
|
||||
@Test(expected=IllegalArgumentException.class)
|
||||
public void withNoneOfFileSourceAndInputStream() throws Exception {
|
||||
new AmazonS3Object(null, null, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* With a temp file, upload to the bucket and see temp file gets deleted and the object
|
||||
* successfully uploaded. Also the ACL of the provided object is null, so the
|
||||
* object should get default ACLs
|
||||
*
|
||||
*/
|
||||
@Test
|
||||
public void putFromTempFile() throws Exception {
|
||||
//first delete the file from the AWS bucket
|
||||
String key = "TestPutFromTempFile.txt";
|
||||
deleteObject(BUCKET_NAME, key);
|
||||
|
||||
|
||||
File file = generateUploadFile();
|
||||
|
||||
//Now put the object
|
||||
AbstractAmazonS3Operations operations = getS3OperationsImplementation();
|
||||
operations.setTemporaryDirectory(temp.getRoot());
|
||||
FileInputStream fin = new FileInputStream(file);
|
||||
//No ACL associated
|
||||
AmazonS3Object object = new AmazonS3Object(null, null,fin, null);
|
||||
operations.putObject(BUCKET_NAME, null, key, object);
|
||||
|
||||
assertTempFileDeletion(temp.getRoot().getAbsolutePath(), key);
|
||||
|
||||
assertObjectExistenceInBucket(key);
|
||||
fin.close();
|
||||
//delete the source file.
|
||||
file.delete();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Upload a file to a folder in the given bucket with the folder name
|
||||
* ending with a slash. Also the ACL of the provided object is null, so the
|
||||
* object should get default ACLs.
|
||||
*
|
||||
*/
|
||||
@Test
|
||||
public void putToFolderWithEndingSlash() throws Exception {
|
||||
String uploadFileName = "TestPutWithEndingSlash.txt";
|
||||
String key = "somedir/with/endingslash/" + uploadFileName;
|
||||
//first delete the file from the AWS bucket
|
||||
deleteObject(BUCKET_NAME, key);
|
||||
|
||||
File file = generateUploadFile();
|
||||
|
||||
//Now put the object
|
||||
AmazonS3Operations operations = getS3OperationsImplementation();
|
||||
|
||||
//No ACL associated
|
||||
AmazonS3Object object = new AmazonS3Object(null, null, null, file);
|
||||
operations.putObject(BUCKET_NAME, "somedir/with/endingslash/", uploadFileName, object);
|
||||
|
||||
|
||||
assertObjectExistenceInBucket(key);
|
||||
//delete the source file.
|
||||
file.delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a file to a folder in the given bucket with the folder name
|
||||
* ending without a slash. Also the ACL of the provided object is null, so the
|
||||
* object should get default ACLs.
|
||||
*
|
||||
*/
|
||||
@Test
|
||||
public void putToFolderWithoutEndingSlash() throws Exception {
|
||||
String uploadFile = "TestPutWithoutEndingSlash.txt";
|
||||
String key = "somedir/without/endingslash/" + uploadFile;
|
||||
//first delete the file from the AWS bucket
|
||||
deleteObject(BUCKET_NAME, key);
|
||||
|
||||
File file = generateUploadFile();
|
||||
|
||||
//Now put the object
|
||||
AmazonS3Operations operations = getS3OperationsImplementation();
|
||||
|
||||
//No ACL associated
|
||||
AmazonS3Object object = new AmazonS3Object(null, null, null, file);
|
||||
operations.putObject(BUCKET_NAME, "somedir/without/endingslash", uploadFile, object);
|
||||
|
||||
assertObjectExistenceInBucket(key);
|
||||
//delete the source file.
|
||||
file.delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a file to a folder in the given bucket with the folder name
|
||||
* beginning a slash. Also the ACL of the provided object is null, so the
|
||||
* object should get default ACLs.
|
||||
*
|
||||
*/
|
||||
@Test
|
||||
public void putToFolderBeginningWithSlash() throws Exception {
|
||||
String uploadFileName = "TestPutBeginningWithSlash.txt";
|
||||
String key = "beginning/with/slash/" + uploadFileName;
|
||||
//first delete the file from the AWS bucket
|
||||
deleteObject(BUCKET_NAME, key);
|
||||
|
||||
File file = generateUploadFile();
|
||||
//Now put the object
|
||||
AmazonS3Operations operations = getS3OperationsImplementation();
|
||||
|
||||
//No ACL associated
|
||||
AmazonS3Object object = new AmazonS3Object(null, null, null, file);
|
||||
operations.putObject(BUCKET_NAME, "/beginning/with/slash/", uploadFileName, object);
|
||||
|
||||
assertObjectExistenceInBucket(key);
|
||||
|
||||
//delete the source file.
|
||||
file.delete();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Upload a file with the provided ACLs and meta data, the test verifies if the
|
||||
* ACLs and the metadata of the file is appropriately set
|
||||
*
|
||||
*/
|
||||
@Test
|
||||
public void putToFolderForACLAndMetadataTest() throws Exception {
|
||||
String uploadFileName = "TestObjectACLAndMetaData.txt";
|
||||
String key = "acl/and/metadata/test/" + uploadFileName;
|
||||
//first delete the file from the AWS bucket
|
||||
deleteObject(BUCKET_NAME, key);
|
||||
|
||||
File file = generateUploadFile();
|
||||
FileInputStream fin = new FileInputStream(file);
|
||||
|
||||
//Now put the object
|
||||
AmazonS3Operations operations = getS3OperationsImplementation();
|
||||
|
||||
Map<String, String> userMetaData = Collections.singletonMap("TestKey", "TestValue");
|
||||
AmazonS3ObjectACL acl = new AmazonS3ObjectACL();
|
||||
ObjectGrant grant = new ObjectGrant(new Grantee(VALID_CANONICAL_ID, GranteeType.CANONICAL_GRANTEE_TYPE),
|
||||
ObjectPermissions.READ_ACP);
|
||||
acl.addGrant(grant);
|
||||
AmazonS3Object object = new AmazonS3Object(userMetaData, null, fin, null,acl);
|
||||
operations.putObject(BUCKET_NAME, "/acl/and/metadata/test/", uploadFileName, object);
|
||||
|
||||
//This fails somehow on my machine
|
||||
//assertTempFileDeletion(uploadFileName);
|
||||
|
||||
//NOTE: The case of the key is no longer in the case we used, its all lower case.
|
||||
//lets get the object's User metadata first
|
||||
S3Object s3Object = getObject(BUCKET_NAME,key);
|
||||
ObjectMetadata objectMetadata = s3Object.getObjectMetadata();
|
||||
userMetaData = objectMetadata.getUserMetadata();
|
||||
Assert.assertNotNull("User metadata is not expected to be null, but got null", userMetaData);
|
||||
Assert.assertTrue("Expecting the key 'testkey' in user MetaData", userMetaData.containsKey("testkey"));
|
||||
Assert.assertEquals("TestValue", userMetaData.get("testkey"));
|
||||
|
||||
//lets verify the object's ACL
|
||||
AccessControlList acls = getObjectACL(BUCKET_NAME, key);
|
||||
Set<Grant> grants = acls.getGrants();
|
||||
boolean isACLValid = false;
|
||||
for(Grant g:grants) {
|
||||
com.amazonaws.services.s3.model.Grantee grantee = g.getGrantee();
|
||||
if(VALID_CANONICAL_ID.equals(grantee.getIdentifier())
|
||||
&& "READ_ACP".equals(grant.getPermission().toString())) {
|
||||
isACLValid = true;
|
||||
}
|
||||
}
|
||||
Assert.assertTrue("Expected Object ACl not found", isACLValid);
|
||||
fin.close();
|
||||
|
||||
//delete the source file.
|
||||
file.delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* List the contents in the bucket with null bucket name
|
||||
*/
|
||||
@Test(expected=IllegalArgumentException.class)
|
||||
public void listWithNullBucket() {
|
||||
AmazonS3Operations impl = getS3OperationsImplementation();
|
||||
impl.listObjects(null, "folder", null, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* List objects with negative page size
|
||||
*/
|
||||
@Test(expected=IllegalArgumentException.class)
|
||||
public void listWithPageSizeLt0() {
|
||||
AmazonS3Operations impl = getS3OperationsImplementation();
|
||||
impl.listObjects(BUCKET_NAME, "folder", null, -2);
|
||||
}
|
||||
|
||||
/**
|
||||
* List with null folder
|
||||
*/
|
||||
@Test
|
||||
public void listWithNullFolder() {
|
||||
AmazonS3Operations impl = getS3OperationsImplementation();
|
||||
PaginatedObjectsView pov = impl.listObjects(BUCKET_NAME, null, null, 100);
|
||||
List<S3ObjectSummary> summary = pov.getObjectSummary();
|
||||
Assert.assertNotNull(summary);
|
||||
Assert.assertTrue(summary.size() > 0);
|
||||
System.out.println("Summary list size is " + summary.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* List with folder as a slash(/), for root folder
|
||||
*/
|
||||
@Test
|
||||
public void listWithSlashOnRoot() {
|
||||
AmazonS3Operations impl = getS3OperationsImplementation();
|
||||
PaginatedObjectsView pov = impl.listObjects(BUCKET_NAME, "/", null, 100);
|
||||
List<S3ObjectSummary> summary = pov.getObjectSummary();
|
||||
Assert.assertNotNull(summary);
|
||||
Assert.assertTrue(summary.size() > 0);
|
||||
System.out.println("Summary list size is " + summary.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* List with folder as a slash(/)
|
||||
*/
|
||||
@Test
|
||||
public void listWithFolderAsSlash() {
|
||||
AmazonS3Operations impl = getS3OperationsImplementation();
|
||||
PaginatedObjectsView pov = impl.listObjects(BUCKET_NAME, "/acl", null, 100);
|
||||
List<S3ObjectSummary> summary = pov.getObjectSummary();
|
||||
Assert.assertNotNull(summary);
|
||||
Assert.assertTrue(summary.size() > 0);
|
||||
System.out.println("Summary list size is " + summary.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* List with folder as a not beginning with slash(/)
|
||||
*/
|
||||
@Test
|
||||
public void listWithFolderNotBeginningWithSlash() {
|
||||
AmazonS3Operations impl = getS3OperationsImplementation();
|
||||
PaginatedObjectsView pov = impl.listObjects(BUCKET_NAME, "somedir/with", null, 100);
|
||||
List<S3ObjectSummary> summary = pov.getObjectSummary();
|
||||
Assert.assertNotNull(summary);
|
||||
Assert.assertTrue(summary.size() > 0);
|
||||
System.out.println("Summary list size is " + summary.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* The test case assumes that all previous AWS tests are executed and we have at least 4 objects
|
||||
* in the bucket, on running all the above tests you will have 5, so we need not do anything
|
||||
* special to add more objects to execute this test
|
||||
*
|
||||
*/
|
||||
@Test
|
||||
public void paginateRecords() {
|
||||
AmazonS3Operations impl = getS3OperationsImplementation();
|
||||
PaginatedObjectsView pov = impl.listObjects(BUCKET_NAME, "/", null, 3);
|
||||
List<S3ObjectSummary> summary = pov.getObjectSummary();
|
||||
Assert.assertNotNull(summary);
|
||||
Assert.assertEquals(3, summary.size());
|
||||
String nextMarker = pov.getNextMarker();
|
||||
Assert.assertNotNull("Expected a non null marker", nextMarker);
|
||||
pov = impl.listObjects(BUCKET_NAME, "/", nextMarker, 3);
|
||||
summary = pov.getObjectSummary();
|
||||
Assert.assertNotNull(summary);
|
||||
Assert.assertTrue(summary.size() > 0);
|
||||
System.out.printf("Number of records on second page are %d\n",summary.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the get object with a null bucket
|
||||
*/
|
||||
@Test(expected=IllegalArgumentException.class)
|
||||
public void getObjectFromNullBucket() {
|
||||
AmazonS3Operations impl = getS3OperationsImplementation();
|
||||
impl.getObject(null, null, "ObjectName.txt");
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a non existent object from the bucket, should return null on unsuccessful search
|
||||
* and if the object with the key doesn't exist.
|
||||
*/
|
||||
@Test
|
||||
public void getNonExistentObject() {
|
||||
AmazonS3Operations impl = getS3OperationsImplementation();
|
||||
AmazonS3Object object = impl.getObject(BUCKET_NAME, null, "jhgkmjbhdc.thb");
|
||||
Assert.assertNull("Expecting a null object but got a non null one", object);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked the getObject with null folder, this will get the object
|
||||
* from the root of the bucket.
|
||||
*
|
||||
*/
|
||||
@Test
|
||||
public void getObjectWithNullFolder() {
|
||||
AmazonS3Operations impl = getS3OperationsImplementation();
|
||||
AmazonS3Object object = impl.getObject(BUCKET_NAME, null, "TestPutFromTempFile.txt");
|
||||
Assert.assertNotNull("Expecting a non null object but got a null one", object);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked the getObject with folder name beginning with /
|
||||
*
|
||||
*/
|
||||
@Test
|
||||
public void getObjectromFolderBeginningWithSlash() {
|
||||
AmazonS3Operations impl = getS3OperationsImplementation();
|
||||
AmazonS3Object object = impl.getObject(BUCKET_NAME, "/acl/and/metadata/test", "TestObjectACLAndMetaData.txt");
|
||||
Assert.assertNotNull("Expecting a non null object but got a null one", object);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked the getObject with folder name beginning with /
|
||||
*
|
||||
*/
|
||||
@Test
|
||||
public void getObjectFromFolderBeginningWithoutSlash() {
|
||||
AmazonS3Operations impl = getS3OperationsImplementation();
|
||||
AmazonS3Object object = impl.getObject(BUCKET_NAME, "acl/and/metadata/test", "TestObjectACLAndMetaData.txt");
|
||||
Assert.assertNotNull("Expecting a non null object but got a null one", object);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The common method that checks if the temp file generated for the
|
||||
* test file uploaded is deleted.
|
||||
*/
|
||||
private void assertTempFileDeletion(String rootFolder, String baseFileName) {
|
||||
//Check if the temp file exists.
|
||||
File tempFile = new File(rootFolder + File.separator + baseFileName + ".writing");
|
||||
Assert.assertFalse("Was expecting the temp file to be deleted, but is present", tempFile.exists());
|
||||
}
|
||||
|
||||
/**
|
||||
* Common method that will assert the existence of the object with the given key in the
|
||||
* bucket
|
||||
*
|
||||
* @param key
|
||||
*/
|
||||
private void assertObjectExistenceInBucket(String key) {
|
||||
S3Object s3Object = getObject(BUCKET_NAME, key);
|
||||
//This is not needed as an exception will be thrown if the key does not exist
|
||||
Assert.assertNotNull("Non null S3Object expected",s3Object);
|
||||
}
|
||||
|
||||
/**
|
||||
* The private helper method that generates the test file to be uploaded
|
||||
* @return
|
||||
* @throws IOException
|
||||
* @throws FileNotFoundException
|
||||
*/
|
||||
private File generateUploadFile() throws IOException, FileNotFoundException {
|
||||
//TODO: Move this to @BeforeClass?
|
||||
String fileName = System.currentTimeMillis() + ".txt";
|
||||
File file = new File(temp.getRoot().getAbsolutePath() + File.separator + fileName);
|
||||
file.createNewFile();
|
||||
//Write something to it
|
||||
FileOutputStream fos = new FileOutputStream(file);
|
||||
fos.write("Test".getBytes());
|
||||
fos.close();
|
||||
return file;
|
||||
}
|
||||
|
||||
|
||||
|
||||
//-- helper methods to interact with AWS S3 services
|
||||
//These methods are used to verify and assert if the implementation
|
||||
//has performed the desired operation
|
||||
/**
|
||||
* Gets the object from the S3 bucket
|
||||
*
|
||||
* @param gets the object from the bucket with the given key
|
||||
*/
|
||||
protected S3Object getObject(String bucket,String key) {
|
||||
return client.getObject(new GetObjectRequest(bucket, key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the objects's {@link AccessControlList} (ACL) for the given bucket and key
|
||||
*
|
||||
* @param bucket
|
||||
* @param key
|
||||
* @return
|
||||
*/
|
||||
protected AccessControlList getObjectACL(String bucket,String key) {
|
||||
return client.getObjectAcl(bucket, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deleted the given object name from the given bucket and key
|
||||
*
|
||||
* @param bucket
|
||||
* @param key
|
||||
*
|
||||
*/
|
||||
protected void deleteObject(String bucket,String key) {
|
||||
client.deleteObject(bucket, key);
|
||||
}
|
||||
|
||||
|
||||
protected abstract AbstractAmazonS3Operations getS3OperationsImplementation();
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.integration.aws.s3.core;
|
||||
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
|
||||
import junit.framework.Assert;
|
||||
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.springframework.integration.aws.core.PropertiesAWSCredentials;
|
||||
|
||||
/**
|
||||
* The test class for the {@link DefaultAmazonS3Operations}, the default implementation that
|
||||
* uses the AWS SDK to implement the functionality. The tests are present in the superclass
|
||||
* {@link AbstractAmazonS3OperationsImplAWSTests}
|
||||
*
|
||||
* Please note that that this test needs connectivity with the AWS S3 service
|
||||
* to be successfully executed. It is excluded from the maven's test execution by default
|
||||
*
|
||||
* To run this test you need to have your AWSAccess key and Secret key in the
|
||||
* file awscredentials.properties in the classpath. This file is not present in the
|
||||
* repository and you need to add one yourselves to src/test/resources folder and have
|
||||
* two properties accessKey and secretKey in it containing the access and the secret key
|
||||
*
|
||||
*
|
||||
* @author Amol Nayak
|
||||
*
|
||||
* @since 0.5
|
||||
*
|
||||
*/
|
||||
public class DefaultAmazonS3OperationsAWSTests extends AbstractAmazonS3OperationsImplAWSTests {
|
||||
|
||||
|
||||
private static DefaultAmazonS3Operations impl;
|
||||
|
||||
@BeforeClass
|
||||
public static void setupS3Operations() throws Exception {
|
||||
PropertiesAWSCredentials credentials =
|
||||
new PropertiesAWSCredentials("classpath:awscredentials.properties");
|
||||
credentials.afterPropertiesSet();
|
||||
impl = new DefaultAmazonS3Operations(credentials);
|
||||
}
|
||||
/**
|
||||
* Sets the thread pool executor to a non null value, execution should
|
||||
* complete successfully
|
||||
*
|
||||
*/
|
||||
@Test
|
||||
public void withNonNullThreadPoolExecutor() {
|
||||
ThreadPoolExecutor executor = (ThreadPoolExecutor)Executors.newFixedThreadPool(10);
|
||||
impl.setThreadPoolExecutor(executor);
|
||||
Assert.assertEquals(executor, impl.getThreadPoolExecutor());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the thread pool executor to a null value, should throw an
|
||||
* {@link IllegalArgumentException}
|
||||
*/
|
||||
@Test(expected=IllegalArgumentException.class)
|
||||
public void withNullThreadPoolExecutor() {
|
||||
impl.setThreadPoolExecutor(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AbstractAmazonS3Operations getS3OperationsImplementation() {
|
||||
return impl;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<beans xmlns="http://www.springframework.org/schema/beans"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:integration="http://www.springframework.org/schema/integration"
|
||||
xmlns:int-aws="http://www.springframework.org/schema/integration/aws/"
|
||||
xsi:schemaLocation=
|
||||
"http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
|
||||
http://www.springframework.org/schema/integration/aws http://www.springframework.org/schema/integration/aws/spring-integration-aws.xsd
|
||||
http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration.xsd">
|
||||
|
||||
<integration:channel id="input"/>
|
||||
|
||||
<int-aws:s3-outbound-channel-adapter
|
||||
id="withCustomNameGenerator"
|
||||
channel="input"
|
||||
bucket="TestBucket"
|
||||
accessKey="dummy"
|
||||
secretKey="dummy"
|
||||
remote-directory="/"
|
||||
file-name-generator="customFileNameGenerator"
|
||||
file-name-generation-expression="headers['name']"
|
||||
auto-startup="false"/>
|
||||
|
||||
<bean id="customFileNameGenerator"
|
||||
class="org.springframework.integration.aws.s3.config.xml.AmazonS3OutboundChannelAdapterParserTests.DummyFileNameGenerator"/>
|
||||
|
||||
</beans>
|
||||
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<beans xmlns="http://www.springframework.org/schema/beans"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:integration="http://www.springframework.org/schema/integration"
|
||||
xmlns:int-aws="http://www.springframework.org/schema/integration/aws"
|
||||
xsi:schemaLocation=
|
||||
"http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
|
||||
http://www.springframework.org/schema/integration/aws http://www.springframework.org/schema/integration/aws/spring-integration-aws.xsd
|
||||
http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration.xsd">
|
||||
|
||||
<integration:channel id="input"/>
|
||||
|
||||
|
||||
<int-aws:s3-outbound-channel-adapter
|
||||
id="withCustomNameGenerator"
|
||||
channel="input"
|
||||
s3-operations="customOps"
|
||||
bucket="TestBucket"
|
||||
accessKey="dummy"
|
||||
secretKey="dummy"
|
||||
multipart-upload-threshold="5120"
|
||||
remote-directory="/"
|
||||
file-name-generation-expression="headers['name']"
|
||||
auto-startup="false"/>
|
||||
|
||||
<bean id="customOps"
|
||||
class="org.springframework.integration.aws.s3.config.xml.AmazonS3OutboundChannelAdapterParserTests.DummyS3Operations"/>
|
||||
</beans>
|
||||
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<beans xmlns="http://www.springframework.org/schema/beans"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:integration="http://www.springframework.org/schema/integration"
|
||||
xmlns:int-aws="http://www.springframework.org/schema/integration/aws"
|
||||
xsi:schemaLocation=
|
||||
"http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
|
||||
http://www.springframework.org/schema/integration/aws http://www.springframework.org/schema/integration/aws/spring-integration-aws.xsd
|
||||
http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration.xsd">
|
||||
|
||||
<integration:channel id="input"/>
|
||||
|
||||
<int-aws:s3-outbound-channel-adapter
|
||||
id="withDefaultServices"
|
||||
channel="input"
|
||||
bucket="TestBucket"
|
||||
accessKey="dummy"
|
||||
secretKey="dummy"
|
||||
remote-directory-expression="headers['remoteDirectory']"
|
||||
multipart-upload-threshold="1024"/>
|
||||
</beans>
|
||||
@@ -0,0 +1,65 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<beans xmlns="http://www.springframework.org/schema/beans"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:integration="http://www.springframework.org/schema/integration"
|
||||
xmlns:int-aws="http://www.springframework.org/schema/integration/aws"
|
||||
xsi:schemaLocation=
|
||||
"http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
|
||||
http://www.springframework.org/schema/integration/aws http://www.springframework.org/schema/integration/aws/spring-integration-aws.xsd
|
||||
http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration.xsd">
|
||||
|
||||
<bean id="localDirectory" class="java.lang.String">
|
||||
<constructor-arg value="#{T(java.lang.System).getProperty('java.io.tmpdir')}"/>
|
||||
</bean>
|
||||
|
||||
<integration:channel id="inbound"/>
|
||||
|
||||
<int-aws:s3-inbound-channel-adapter
|
||||
id="validInbound"
|
||||
bucket="TestBucket"
|
||||
propertiesFile="testawscredentials.properties"
|
||||
temporary-suffix=".temp"
|
||||
local-directory-expression="@localDirectory"
|
||||
remote-directory="remote"
|
||||
accept-sub-folders="true"
|
||||
max-objects-per-batch="100"
|
||||
file-name-regex="[A-Za-z0-9]+\\.txt"
|
||||
channel="inbound"
|
||||
auto-startup="false">
|
||||
<integration:poller fixed-rate="1000"/>
|
||||
</int-aws:s3-inbound-channel-adapter>
|
||||
|
||||
<int-aws:s3-inbound-channel-adapter
|
||||
id="withAWSEndpoint"
|
||||
bucket="TestBucket"
|
||||
propertiesFile="testawscredentials.properties"
|
||||
aws-endpoint="s3-eu-west-1.amazonaws.com"
|
||||
temporary-suffix=".temp"
|
||||
local-directory-expression="@localDirectory"
|
||||
remote-directory="remote"
|
||||
accept-sub-folders="true"
|
||||
channel="inbound"
|
||||
auto-startup="false">
|
||||
<integration:poller fixed-rate="1000"/>
|
||||
</int-aws:s3-inbound-channel-adapter>
|
||||
|
||||
<int-aws:s3-inbound-channel-adapter
|
||||
id="validInboundWithCustomOps"
|
||||
bucket="TestBucket"
|
||||
s3-operations="customOps"
|
||||
propertiesFile="testawscredentials.properties"
|
||||
temporary-suffix=".temp"
|
||||
local-directory-expression="@localDirectory"
|
||||
remote-directory="remote"
|
||||
accept-sub-folders="true"
|
||||
max-objects-per-batch="100"
|
||||
file-name-regex="[A-Za-z0-9]+\\.txt"
|
||||
channel="inbound"
|
||||
auto-startup="false">
|
||||
<integration:poller fixed-rate="1000"/>
|
||||
</int-aws:s3-inbound-channel-adapter>
|
||||
|
||||
<bean id="customOps"
|
||||
class="org.springframework.integration.aws.s3.config.xml.AmazonS3DummyOperations"/>
|
||||
|
||||
</beans>
|
||||
@@ -0,0 +1,75 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<beans xmlns="http://www.springframework.org/schema/beans"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:integration="http://www.springframework.org/schema/integration"
|
||||
xmlns:int-aws="http://www.springframework.org/schema/integration/aws"
|
||||
xsi:schemaLocation=
|
||||
"http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
|
||||
http://www.springframework.org/schema/integration/aws http://www.springframework.org/schema/integration/aws/spring-integration-aws.xsd
|
||||
http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration.xsd">
|
||||
|
||||
<integration:channel id="input"/>
|
||||
|
||||
<int-aws:s3-outbound-channel-adapter
|
||||
id="withCustomService"
|
||||
channel="input"
|
||||
bucket="TestBucket"
|
||||
accessKey="dummy"
|
||||
secretKey="dummy"
|
||||
s3-operations="customOps"
|
||||
remote-directory="/"
|
||||
auto-startup="false"/>
|
||||
|
||||
<int-aws:s3-outbound-channel-adapter
|
||||
id="withDefaultServices"
|
||||
channel="input"
|
||||
bucket="TestBucket"
|
||||
accessKey="dummy"
|
||||
secretKey="dummy"
|
||||
remote-directory-expression="headers['remoteDirectory']"
|
||||
charset="US-ASCII"
|
||||
thread-pool-executor="executor"
|
||||
multipart-upload-threshold="5120"
|
||||
temporary-suffix=".write"
|
||||
file-name-generation-expression="headers['name']"/>
|
||||
|
||||
|
||||
<int-aws:s3-outbound-channel-adapter
|
||||
id="withCustomNameGenerator"
|
||||
channel="input"
|
||||
bucket="TestBucket"
|
||||
accessKey="dummy"
|
||||
secretKey="dummy"
|
||||
remote-directory="/"
|
||||
file-name-generator="customFileNameGenerator"
|
||||
auto-startup="false"/>
|
||||
|
||||
<int-aws:s3-outbound-channel-adapter
|
||||
id="withCustomEndpoint"
|
||||
aws-endpoint="http://s3-eu-west-1.amazonaws.com"
|
||||
channel="input"
|
||||
bucket="TestBucket"
|
||||
accessKey="dummy"
|
||||
secretKey="dummy"
|
||||
remote-directory="/"
|
||||
auto-startup="false"/>
|
||||
|
||||
<bean id="queue" class="java.util.concurrent.ArrayBlockingQueue">
|
||||
<constructor-arg value="10"/>
|
||||
</bean>
|
||||
|
||||
<bean id="executor" class="java.util.concurrent.ThreadPoolExecutor">
|
||||
<constructor-arg index="0" value="5"/>
|
||||
<constructor-arg index="1" value="10"/>
|
||||
<constructor-arg index="2" value="10"/>
|
||||
<constructor-arg index="3" value="MILLISECONDS"/>
|
||||
<constructor-arg index="4" ref="queue"/>
|
||||
</bean>
|
||||
|
||||
<bean id="customOps"
|
||||
class="org.springframework.integration.aws.s3.config.xml.AmazonS3DummyOperations"/>
|
||||
|
||||
<bean id="customFileNameGenerator"
|
||||
class="org.springframework.integration.aws.s3.config.xml.AmazonS3OutboundChannelAdapterParserTests.DummyFileNameGenerator"/>
|
||||
|
||||
</beans>
|
||||
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<beans xmlns="http://www.springframework.org/schema/beans"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:integration="http://www.springframework.org/schema/integration"
|
||||
xmlns:int-aws="http://www.springframework.org/schema/integration/aws"
|
||||
xsi:schemaLocation=
|
||||
"http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
|
||||
http://www.springframework.org/schema/integration/aws http://www.springframework.org/schema/integration/aws/spring-integration-aws.xsd
|
||||
http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration.xsd">
|
||||
|
||||
<integration:channel id="inbound"/>
|
||||
|
||||
<int-aws:s3-inbound-channel-adapter
|
||||
id="validInbound"
|
||||
bucket="TestBucket"
|
||||
propertiesFile="testawscredentials.properties"
|
||||
temporary-suffix=".temp"
|
||||
remote-directory="remote"
|
||||
accept-sub-folders="true"
|
||||
max-objects-per-batch="100"
|
||||
file-name-regex="[A-Za-z0-9]+\\.txt"
|
||||
channel="inbound">
|
||||
<integration:poller fixed-rate="1000"/>
|
||||
</int-aws:s3-inbound-channel-adapter>
|
||||
|
||||
</beans>
|
||||
Reference in New Issue
Block a user