You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by rf...@apache.org on 2018/06/23 11:18:13 UTC

[maven-resolver] 01/01: [MRESOLVER-46] Add support InputStream/OutputStream transformers

This is an automated email from the ASF dual-hosted git repository.

rfscholte pushed a commit to branch MRESOLVER-46
in repository https://gitbox.apache.org/repos/asf/maven-resolver.git

commit 55658dbae4bf29adb372302887ea85039bee7ba1
Author: rfscholte <rf...@apache.org>
AuthorDate: Sat Jun 23 13:18:06 2018 +0200

    [MRESOLVER-46] Add support InputStream/OutputStream transformers
---
 .../AbstractForwardingRepositorySystemSession.java | 32 +++++++
 .../aether/DefaultRepositorySystemSession.java     | 36 ++++++++
 .../eclipse/aether/RepositorySystemSession.java    |  8 ++
 .../eclipse/aether/transform/FileTransformer.java  | 51 +++++++++++
 .../aether/transform/FileTransformerManager.java   | 42 ++++++++++
 .../connector/basic/BasicRepositoryConnector.java  | 60 +++++++++++--
 .../aether/internal/impl/DefaultDeployer.java      | 28 ++++++-
 .../aether/internal/impl/DefaultInstaller.java     | 57 ++++++++++---
 .../aether/internal/impl/DefaultDeployerTest.java  | 37 ++++++++
 .../aether/internal/impl/DefaultInstallerTest.java | 47 +++++++++++
 .../internal/impl/StubFileTransformerManager.java  | 50 +++++++++++
 .../aether/spi/connector/ArtifactUpload.java       | 30 ++++++-
 .../aether/transport/http/HttpTransporterTest.java |  2 +-
 .../org/eclipse/aether/util/ChecksumUtils.java     | 55 ++++--------
 .../org/eclipse/aether/util/ChecksumUtilTest.java  | 98 +++++++++++++++-------
 15 files changed, 544 insertions(+), 89 deletions(-)

diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/AbstractForwardingRepositorySystemSession.java b/maven-resolver-api/src/main/java/org/eclipse/aether/AbstractForwardingRepositorySystemSession.java
index 20df431..35759cb 100644
--- a/maven-resolver-api/src/main/java/org/eclipse/aether/AbstractForwardingRepositorySystemSession.java
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/AbstractForwardingRepositorySystemSession.java
@@ -36,6 +36,7 @@ import org.eclipse.aether.repository.WorkspaceReader;
 import org.eclipse.aether.resolution.ArtifactDescriptorPolicy;
 import org.eclipse.aether.resolution.ResolutionErrorPolicy;
 import org.eclipse.aether.transfer.TransferListener;
+import org.eclipse.aether.transform.FileTransformerManager;
 
 /**
  * A special repository system session to enable decorating or proxying another session. To do so, clients have to
@@ -61,129 +62,160 @@ public abstract class AbstractForwardingRepositorySystemSession
      */
     protected abstract RepositorySystemSession getSession();
 
+    @Override
     public boolean isOffline()
     {
         return getSession().isOffline();
     }
 
+    @Override
     public boolean isIgnoreArtifactDescriptorRepositories()
     {
         return getSession().isIgnoreArtifactDescriptorRepositories();
     }
 
+    @Override
     public ResolutionErrorPolicy getResolutionErrorPolicy()
     {
         return getSession().getResolutionErrorPolicy();
     }
 
+    @Override
     public ArtifactDescriptorPolicy getArtifactDescriptorPolicy()
     {
         return getSession().getArtifactDescriptorPolicy();
     }
 
+    @Override
     public String getChecksumPolicy()
     {
         return getSession().getChecksumPolicy();
     }
 
+    @Override
     public String getUpdatePolicy()
     {
         return getSession().getUpdatePolicy();
     }
 
+    @Override
     public LocalRepository getLocalRepository()
     {
         return getSession().getLocalRepository();
     }
 
+    @Override
     public LocalRepositoryManager getLocalRepositoryManager()
     {
         return getSession().getLocalRepositoryManager();
     }
 
+    @Override
     public WorkspaceReader getWorkspaceReader()
     {
         return getSession().getWorkspaceReader();
     }
 
+    @Override
     public RepositoryListener getRepositoryListener()
     {
         return getSession().getRepositoryListener();
     }
 
+    @Override
     public TransferListener getTransferListener()
     {
         return getSession().getTransferListener();
     }
 
+    @Override
     public Map<String, String> getSystemProperties()
     {
         return getSession().getSystemProperties();
     }
 
+    @Override
     public Map<String, String> getUserProperties()
     {
         return getSession().getUserProperties();
     }
 
+    @Override
     public Map<String, Object> getConfigProperties()
     {
         return getSession().getConfigProperties();
     }
 
+    @Override
     public MirrorSelector getMirrorSelector()
     {
         return getSession().getMirrorSelector();
     }
 
+    @Override
     public ProxySelector getProxySelector()
     {
         return getSession().getProxySelector();
     }
 
+    @Override
     public AuthenticationSelector getAuthenticationSelector()
     {
         return getSession().getAuthenticationSelector();
     }
 
+    @Override
     public ArtifactTypeRegistry getArtifactTypeRegistry()
     {
         return getSession().getArtifactTypeRegistry();
     }
 
+    @Override
     public DependencyTraverser getDependencyTraverser()
     {
         return getSession().getDependencyTraverser();
     }
 
+    @Override
     public DependencyManager getDependencyManager()
     {
         return getSession().getDependencyManager();
     }
 
+    @Override
     public DependencySelector getDependencySelector()
     {
         return getSession().getDependencySelector();
     }
 
+    @Override
     public VersionFilter getVersionFilter()
     {
         return getSession().getVersionFilter();
     }
 
+    @Override
     public DependencyGraphTransformer getDependencyGraphTransformer()
     {
         return getSession().getDependencyGraphTransformer();
     }
 
+    @Override
     public SessionData getData()
     {
         return getSession().getData();
     }
 
+    @Override
     public RepositoryCache getCache()
     {
         return getSession().getCache();
     }
+    
+    @Override
+    public FileTransformerManager geFileTransformerManager()
+    {
+        return getSession().geFileTransformerManager();
+    }
 
 }
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/DefaultRepositorySystemSession.java b/maven-resolver-api/src/main/java/org/eclipse/aether/DefaultRepositorySystemSession.java
index 13773df..8ba4e15 100644
--- a/maven-resolver-api/src/main/java/org/eclipse/aether/DefaultRepositorySystemSession.java
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/DefaultRepositorySystemSession.java
@@ -24,6 +24,9 @@ import java.util.HashMap;
 import java.util.Map;
 import static java.util.Objects.requireNonNull;
 
+import java.util.Collection;
+
+import org.eclipse.aether.artifact.Artifact;
 import org.eclipse.aether.artifact.ArtifactType;
 import org.eclipse.aether.artifact.ArtifactTypeRegistry;
 import org.eclipse.aether.collection.DependencyGraphTransformer;
@@ -44,6 +47,8 @@ import org.eclipse.aether.repository.WorkspaceReader;
 import org.eclipse.aether.resolution.ArtifactDescriptorPolicy;
 import org.eclipse.aether.resolution.ResolutionErrorPolicy;
 import org.eclipse.aether.transfer.TransferListener;
+import org.eclipse.aether.transform.FileTransformer;
+import org.eclipse.aether.transform.FileTransformerManager;
 
 /**
  * A simple repository system session.
@@ -73,6 +78,8 @@ public final class DefaultRepositorySystemSession
 
     private LocalRepositoryManager localRepositoryManager;
 
+    private FileTransformerManager fileTransformerManager;
+
     private WorkspaceReader workspaceReader;
 
     private RepositoryListener repositoryListener;
@@ -130,6 +137,7 @@ public final class DefaultRepositorySystemSession
         proxySelector = NullProxySelector.INSTANCE;
         authenticationSelector = NullAuthenticationSelector.INSTANCE;
         artifactTypeRegistry = NullArtifactTypeRegistry.INSTANCE;
+        fileTransformerManager = NullFileTransformerManager.INSTANCE;
         data = new DefaultSessionData();
     }
 
@@ -317,6 +325,23 @@ public final class DefaultRepositorySystemSession
         return this;
     }
 
+    @Override
+    public FileTransformerManager geFileTransformerManager()
+    {
+        return fileTransformerManager;
+    }
+
+    public DefaultRepositorySystemSession setFileTransformerManager( FileTransformerManager fileTransformerManager )
+    {
+        failIfReadOnly();
+        this.fileTransformerManager = fileTransformerManager;
+        if ( this.fileTransformerManager == null )
+        {
+            this.fileTransformerManager = NullFileTransformerManager.INSTANCE;
+        }
+        return this;
+    }
+
     public WorkspaceReader getWorkspaceReader()
     {
         return workspaceReader;
@@ -829,4 +854,15 @@ public final class DefaultRepositorySystemSession
 
     }
 
+    static final class NullFileTransformerManager implements FileTransformerManager
+    {
+        public static final FileTransformerManager INSTANCE = new NullFileTransformerManager();
+
+        @Override
+        public Collection<FileTransformer> getTransformersForArtifact( Artifact artifact )
+        {
+            return Collections.emptyList();
+        }
+    }
+
 }
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/RepositorySystemSession.java b/maven-resolver-api/src/main/java/org/eclipse/aether/RepositorySystemSession.java
index 888f29c..025ec80 100644
--- a/maven-resolver-api/src/main/java/org/eclipse/aether/RepositorySystemSession.java
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/RepositorySystemSession.java
@@ -37,6 +37,7 @@ import org.eclipse.aether.repository.WorkspaceReader;
 import org.eclipse.aether.resolution.ArtifactDescriptorPolicy;
 import org.eclipse.aether.resolution.ResolutionErrorPolicy;
 import org.eclipse.aether.transfer.TransferListener;
+import org.eclipse.aether.transform.FileTransformerManager;
 
 /**
  * Defines settings and components that control the repository system. Once initialized, the session object itself is
@@ -260,4 +261,11 @@ public interface RepositorySystemSession
      */
     RepositoryCache getCache();
 
+    /**
+     * Get the file transformer manager
+     * 
+     * @return the manager, never {@code null}
+     */
+    FileTransformerManager geFileTransformerManager();
+
 }
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/transform/FileTransformer.java b/maven-resolver-api/src/main/java/org/eclipse/aether/transform/FileTransformer.java
new file mode 100644
index 0000000..17c997c
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/transform/FileTransformer.java
@@ -0,0 +1,51 @@
+package org.eclipse.aether.transform;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.eclipse.aether.artifact.Artifact;
+
+/**
+ * Can transform a file while installing/deploying
+ * 
+ * @author Robert Scholte
+ * @since 1.2.0
+ */
+public interface FileTransformer
+{
+    /**
+     * Transform the target location  
+     * 
+     * @param artifact the original artifact
+     * @return the transformed artifact
+     */
+    Artifact transformArtifact( Artifact artifact );
+    
+    /**
+     * Transform the data
+     * 
+     * @param file the file with the original data
+     * @return the transformed data
+     */
+    InputStream transformData( File file ) throws IOException;
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/transform/FileTransformerManager.java b/maven-resolver-api/src/main/java/org/eclipse/aether/transform/FileTransformerManager.java
new file mode 100644
index 0000000..753a739
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/transform/FileTransformerManager.java
@@ -0,0 +1,42 @@
+package org.eclipse.aether.transform;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+import java.util.Collection;
+
+import org.eclipse.aether.artifact.Artifact;
+
+/**
+ * Manager the FileTransformers 
+ * 
+ * @author Robert Scholte
+ * @since 1.2.0
+ */
+public interface FileTransformerManager
+{
+    /**
+     * All transformers for this specific artifact. Be aware that if you want to create additional files, but also want to the original
+     * to be deployed, you must add an explicit transformer for that file too (one that doesn't transform the artifact and data).
+     *  
+     * @param artifact
+     * @return
+     */
+    Collection<FileTransformer> getTransformersForArtifact( Artifact artifact );
+}
diff --git a/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/BasicRepositoryConnector.java b/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/BasicRepositoryConnector.java
index 87c8191..968581f 100644
--- a/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/BasicRepositoryConnector.java
+++ b/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/BasicRepositoryConnector.java
@@ -19,6 +19,9 @@ package org.eclipse.aether.connector.basic;
  * under the License.
  */
 
+import static java.util.Objects.requireNonNull;
+
+import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.IOException;
 import java.net.URI;
@@ -27,7 +30,6 @@ import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
-import static java.util.Objects.requireNonNull;
 import java.util.Set;
 import java.util.concurrent.Executor;
 import java.util.concurrent.ExecutorService;
@@ -60,6 +62,7 @@ import org.eclipse.aether.transfer.NoRepositoryLayoutException;
 import org.eclipse.aether.transfer.NoTransporterException;
 import org.eclipse.aether.transfer.TransferEvent;
 import org.eclipse.aether.transfer.TransferResource;
+import org.eclipse.aether.transform.FileTransformer;
 import org.eclipse.aether.util.ChecksumUtils;
 import org.eclipse.aether.util.ConfigUtils;
 import org.eclipse.aether.util.concurrency.RunnableErrorForwarder;
@@ -279,7 +282,7 @@ final class BasicRepositoryConnector
 
             List<RepositoryLayout.Checksum> checksums = layout.getChecksums( transfer.getArtifact(), true, location );
 
-            Runnable task = new PutTaskRunner( location, transfer.getFile(), checksums, listener );
+            Runnable task = new PutTaskRunner( location, transfer.getFile(), transfer.getFileTransformer(), checksums, listener );
             task.run();
         }
 
@@ -495,24 +498,57 @@ final class BasicRepositoryConnector
 
         private final File file;
 
+        private final FileTransformer fileTransformer; 
+
         private final Collection<RepositoryLayout.Checksum> checksums;
 
         PutTaskRunner( URI path, File file, List<RepositoryLayout.Checksum> checksums,
+                       TransferTransportListener<?> listener )
+        {
+            this( path, file, null, checksums, listener );
+        }
+
+        PutTaskRunner( URI path, File file, FileTransformer fileTransformer, List<RepositoryLayout.Checksum> checksums,
                               TransferTransportListener<?> listener )
         {
             super( path, listener );
             this.file = requireNonNull( file, "source file cannot be null" );
+            this.fileTransformer = fileTransformer;
             this.checksums = safe( checksums );
         }
 
         protected void runTask()
             throws Exception
         {
-            transporter.put( new PutTask( path ).setDataFile( file ).setListener( listener ) );
-            uploadChecksums( file, path );
+            if ( fileTransformer != null )
+            {
+                // transform data once to byte array, ensure constant data for checksum
+                ByteArrayOutputStream baos = new ByteArrayOutputStream();
+                byte[] buffer = new byte[1024];
+                for ( int read; ( read =
+                    fileTransformer.transformData( file ).read( buffer, 0, buffer.length ) ) != -1; )
+                {
+                    baos.write( buffer, 0, read );
+                }
+
+                byte[] bytes = baos.toByteArray();
+                transporter.put( new PutTask( path ).setDataBytes( bytes ).setListener( listener ) );
+                uploadChecksums( file, bytes, path );
+            }
+            else
+            {
+                transporter.put( new PutTask( path ).setDataFile( file ).setListener( listener ) );
+                uploadChecksums( file, null , path );
+            }
         }
 
-        private void uploadChecksums( File file, URI location )
+        /**
+         * 
+         * @param file source
+         * @param bytes transformed data from file or {@code null}
+         * @param location target
+         */
+        private void uploadChecksums( File file, byte[] bytes, URI location )
         {
             if ( checksums.isEmpty() )
             {
@@ -520,12 +556,22 @@ final class BasicRepositoryConnector
             }
             try
             {
-                Set<String> algos = new HashSet<String>();
+                Set<String> algos = new HashSet<>();
                 for ( RepositoryLayout.Checksum checksum : checksums )
                 {
                     algos.add( checksum.getAlgorithm() );
                 }
-                Map<String, Object> sumsByAlgo = ChecksumUtils.calc( file, algos );
+                
+                Map<String, Object> sumsByAlgo;
+                if ( bytes != null )
+                {
+                    sumsByAlgo = ChecksumUtils.calc( bytes, algos );
+                }
+                else
+                {
+                    sumsByAlgo = ChecksumUtils.calc( file, algos );
+                }
+
                 for ( RepositoryLayout.Checksum checksum : checksums )
                 {
                     uploadChecksum( checksum.getLocation(), sumsByAlgo.get( checksum.getAlgorithm() ) );
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultDeployer.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultDeployer.java
index 194ce30..4d6f5be 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultDeployer.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultDeployer.java
@@ -71,6 +71,8 @@ import org.eclipse.aether.transfer.NoRepositoryConnectorException;
 import org.eclipse.aether.transfer.RepositoryOfflineException;
 import org.eclipse.aether.transfer.TransferCancelledException;
 import org.eclipse.aether.transfer.TransferEvent;
+import org.eclipse.aether.transform.FileTransformer;
+import org.eclipse.aether.transform.FileTransformerManager;
 
 /**
  */
@@ -237,6 +239,8 @@ public class DefaultDeployer
         {
             List<? extends MetadataGenerator> generators = getMetadataGenerators( session, request );
 
+            FileTransformerManager fileTransformerManager = session.geFileTransformerManager();
+
             List<ArtifactUpload> artifactUploads = new ArrayList<ArtifactUpload>();
             List<MetadataUpload> metadataUploads = new ArrayList<MetadataUpload>();
             IdentityHashMap<Metadata, Object> processedMetadata = new IdentityHashMap<Metadata, Object>();
@@ -266,10 +270,26 @@ public class DefaultDeployer
 
                 artifacts.set( i, artifact );
 
-                ArtifactUpload upload = new ArtifactUpload( artifact, artifact.getFile() );
-                upload.setTrace( trace );
-                upload.setListener( new ArtifactUploadListener( catapult, upload ) );
-                artifactUploads.add( upload );
+                Collection<FileTransformer> fileTransformers = fileTransformerManager.getTransformersForArtifact( artifact );
+                if ( !fileTransformers.isEmpty() )
+                {
+                    for ( FileTransformer fileTransformer : fileTransformers )
+                    {
+                        Artifact targetArtifact = fileTransformer.transformArtifact( artifact );
+
+                        ArtifactUpload upload = new ArtifactUpload( targetArtifact, artifact.getFile(), fileTransformer );
+                        upload.setTrace( trace );
+                        upload.setListener( new ArtifactUploadListener( catapult, upload ) );
+                        artifactUploads.add( upload );
+                    }
+                }
+                else
+                {
+                    ArtifactUpload upload = new ArtifactUpload( artifact, artifact.getFile() );
+                    upload.setTrace( trace );
+                    upload.setListener( new ArtifactUploadListener( catapult, upload ) );
+                    artifactUploads.add( upload );
+                }
             }
 
             connector.put( artifactUploads, null );
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultInstaller.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultInstaller.java
index 7cd14d0..2bd34fb 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultInstaller.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultInstaller.java
@@ -19,12 +19,14 @@ package org.eclipse.aether.internal.impl;
  * under the License.
  */
 
+import static java.util.Objects.requireNonNull;
+
 import java.io.File;
+import java.io.InputStream;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.IdentityHashMap;
 import java.util.List;
-import static java.util.Objects.requireNonNull;
 import java.util.Set;
 
 import javax.inject.Inject;
@@ -52,6 +54,7 @@ import org.eclipse.aether.repository.LocalRepositoryManager;
 import org.eclipse.aether.spi.io.FileProcessor;
 import org.eclipse.aether.spi.locator.Service;
 import org.eclipse.aether.spi.locator.ServiceLocator;
+import org.eclipse.aether.transform.FileTransformer;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -236,9 +239,37 @@ public class DefaultInstaller
 
         File srcFile = artifact.getFile();
 
-        File dstFile = new File( lrm.getRepository().getBasedir(), lrm.getPathForLocalArtifact( artifact ) );
+        Collection<FileTransformer> fileTransformers = session.geFileTransformerManager().getTransformersForArtifact( artifact );
+        if ( fileTransformers.isEmpty() )
+        {
+            install( session, trace, artifact, lrm, srcFile, null );
+        }
+        else
+        {
+            for ( FileTransformer fileTransformer : fileTransformers )
+            {
+                install( session, trace, artifact, lrm, srcFile, fileTransformer );
+            }
+        }
+    }
 
-        artifactInstalling( session, trace, artifact, dstFile );
+    private void install( RepositorySystemSession session, RequestTrace trace, Artifact artifact,
+                          LocalRepositoryManager lrm, File srcFile, FileTransformer fileTransformer )
+        throws InstallationException
+    {
+        final Artifact targetArtifact;
+        if ( fileTransformer != null )
+        {
+            targetArtifact = fileTransformer.transformArtifact( artifact );
+        }
+        else
+        {
+            targetArtifact = artifact;
+        }
+
+        File dstFile = new File( lrm.getRepository().getBasedir(), lrm.getPathForLocalArtifact( targetArtifact ) );
+
+        artifactInstalling( session, trace, targetArtifact, dstFile );
 
         Exception exception = null;
         try
@@ -249,29 +280,35 @@ public class DefaultInstaller
             }
 
             boolean copy =
-                "pom".equals( artifact.getExtension() ) || srcFile.lastModified() != dstFile.lastModified()
+                "pom".equals( targetArtifact.getExtension() ) || srcFile.lastModified() != dstFile.lastModified()
                     || srcFile.length() != dstFile.length() || !srcFile.exists();
 
-            if ( copy )
+            if ( !copy )
             {
-                fileProcessor.copy( srcFile, dstFile );
+                LOGGER.debug( "Skipped re-installing {} to {}, seems unchanged", srcFile, dstFile );
+            }
+            else if ( fileTransformer != null ) 
+            {
+                InputStream is = fileTransformer.transformData( srcFile );
+                fileProcessor.write( dstFile, is );
                 dstFile.setLastModified( srcFile.lastModified() );
             }
             else
             {
-                LOGGER.debug( "Skipped re-installing {} to {}, seems unchanged", srcFile, dstFile );
+                fileProcessor.copy( srcFile, dstFile );
+                dstFile.setLastModified( srcFile.lastModified() );
             }
 
-            lrm.add( session, new LocalArtifactRegistration( artifact ) );
+            lrm.add( session, new LocalArtifactRegistration( targetArtifact ) );
         }
         catch ( Exception e )
         {
             exception = e;
-            throw new InstallationException( "Failed to install artifact " + artifact + ": " + e.getMessage(), e );
+            throw new InstallationException( "Failed to install artifact " + targetArtifact + ": " + e.getMessage(), e );
         }
         finally
         {
-            artifactInstalled( session, trace, artifact, dstFile, exception );
+            artifactInstalled( session, trace, targetArtifact, dstFile, exception );
         }
     }
 
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultDeployerTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultDeployerTest.java
index 9465e87..be47aad 100644
--- a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultDeployerTest.java
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultDeployerTest.java
@@ -21,8 +21,11 @@ package org.eclipse.aether.internal.impl;
 
 import static org.junit.Assert.*;
 
+import java.io.ByteArrayInputStream;
 import java.io.File;
 import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.Charset;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
@@ -52,6 +55,8 @@ import org.eclipse.aether.spi.connector.MetadataDownload;
 import org.eclipse.aether.spi.connector.MetadataUpload;
 import org.eclipse.aether.spi.connector.RepositoryConnector;
 import org.eclipse.aether.transfer.MetadataNotFoundException;
+import org.eclipse.aether.transform.FileTransformer;
+import org.eclipse.aether.util.artifact.SubArtifact;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -382,4 +387,36 @@ public class DefaultDeployerTest
         assertNull( props.toString(), props.get( "old" ) );
     }
 
+    @Test
+    public void testFileTransformer() throws Exception
+    {
+        final Artifact transformedArtifact = new SubArtifact( artifact, null, "raj" );
+        FileTransformer transformer = new FileTransformer()
+        {
+            @Override
+            public InputStream transformData( File file )
+                throws IOException
+            {
+                return new ByteArrayInputStream( "transformed data".getBytes( Charset.forName( "UTF-8" ) ) );
+            }
+            
+            @Override
+            public Artifact transformArtifact( Artifact artifact )
+            {
+                return transformedArtifact;
+            }
+        };
+        
+        StubFileTransformerManager fileTransformerManager = new StubFileTransformerManager();
+        fileTransformerManager.addFileTransformer( transformer, "jar" );
+        session.setFileTransformerManager( fileTransformerManager );
+        
+        request = new DeployRequest();
+        request.addArtifact( artifact );
+        deployer.deploy( session, request );
+        
+        Artifact putArtifact = connector.getActualArtifactPutRequests().get( 0 );
+        assertEquals( transformedArtifact, putArtifact );
+    }
+
 }
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultInstallerTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultInstallerTest.java
index efabedd..df87e1a 100644
--- a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultInstallerTest.java
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultInstallerTest.java
@@ -21,8 +21,13 @@ package org.eclipse.aether.internal.impl;
 
 import static org.junit.Assert.*;
 
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
 import java.io.File;
+import java.io.FileReader;
 import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.Charset;
 import java.util.List;
 
 import org.eclipse.aether.DefaultRepositorySystemSession;
@@ -42,6 +47,8 @@ import org.eclipse.aether.internal.test.util.TestUtils;
 import org.eclipse.aether.metadata.DefaultMetadata;
 import org.eclipse.aether.metadata.Metadata;
 import org.eclipse.aether.metadata.Metadata.Nature;
+import org.eclipse.aether.transform.FileTransformer;
+import org.eclipse.aether.util.artifact.SubArtifact;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -410,4 +417,44 @@ public class DefaultInstallerTest
         assertEquals( "artifact timestamp was not set to src file", artifact.getFile().lastModified(),
                       localArtifactFile.lastModified() );
     }
+    
+    @Test
+    public void testFileTransformer() throws Exception
+    {
+        final Artifact transformedArtifact = new SubArtifact( artifact, null, "raj" );
+        FileTransformer transformer = new FileTransformer()
+        {
+            @Override
+            public InputStream transformData( File file )
+                throws IOException
+            {
+                return new ByteArrayInputStream( "transformed data".getBytes( Charset.forName( "UTF-8" ) ) );
+            }
+            
+            @Override
+            public Artifact transformArtifact( Artifact artifact )
+            {
+                return transformedArtifact;
+            }
+        };
+        
+        StubFileTransformerManager fileTransformerManager = new StubFileTransformerManager();
+        fileTransformerManager.addFileTransformer( transformer, "jar" );
+        session.setFileTransformerManager( fileTransformerManager );
+        
+        request = new InstallRequest();
+        request.addArtifact( artifact );
+        installer.install( session, request );
+        
+        assertFalse( localArtifactFile.exists() );
+        
+        String transformedArtifactPath = session.getLocalRepositoryManager().getPathForLocalArtifact( transformedArtifact );
+        File transformedArtifactFile = new File( session.getLocalRepository().getBasedir(), transformedArtifactPath );
+        assertTrue( transformedArtifactFile.exists() );
+        
+        try ( BufferedReader r = new BufferedReader( new FileReader( transformedArtifactFile ) ) )
+        {
+            assertEquals( "transformed data", r.readLine() );
+        }
+    }
 }
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/StubFileTransformerManager.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/StubFileTransformerManager.java
new file mode 100644
index 0000000..cafb52e
--- /dev/null
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/StubFileTransformerManager.java
@@ -0,0 +1,50 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.transform.FileTransformer;
+import org.eclipse.aether.transform.FileTransformerManager;
+
+public class StubFileTransformerManager implements FileTransformerManager
+{
+    private Map<String, Collection<FileTransformer>> fileTransformers = new HashMap<>();
+    
+    @Override
+    public Collection<FileTransformer> getTransformersForArtifact( Artifact artifact )
+    {
+        return fileTransformers.get( artifact.getExtension() );
+    }
+    
+    public void addFileTransformer( FileTransformer fileTransformer, String extension )
+    {
+        if ( !fileTransformers.containsKey( extension ) )
+        {
+            fileTransformers.put( extension, new HashSet<FileTransformer>() );
+        }
+        fileTransformers.get( extension ).add( fileTransformer );
+    }
+
+}
diff --git a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/ArtifactUpload.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/ArtifactUpload.java
index f85539e..000e65a 100644
--- a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/ArtifactUpload.java
+++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/ArtifactUpload.java
@@ -25,6 +25,7 @@ import org.eclipse.aether.RequestTrace;
 import org.eclipse.aether.artifact.Artifact;
 import org.eclipse.aether.transfer.ArtifactTransferException;
 import org.eclipse.aether.transfer.TransferListener;
+import org.eclipse.aether.transform.FileTransformer;
 
 /**
  * An upload of an artifact to a remote repository. A repository connector processing this upload has to use
@@ -33,6 +34,7 @@ import org.eclipse.aether.transfer.TransferListener;
 public final class ArtifactUpload
     extends ArtifactTransfer
 {
+    private FileTransformer fileTransformer;
 
     /**
      * Creates a new uninitialized upload.
@@ -54,6 +56,13 @@ public final class ArtifactUpload
         setFile( file );
     }
 
+    public ArtifactUpload( Artifact artifact, File file, FileTransformer fileTransformer )
+    {
+        setArtifact( artifact );
+        setFile( file );
+        setFileTransformer( fileTransformer );
+    }
+
     @Override
     public ArtifactUpload setArtifact( Artifact artifact )
     {
@@ -88,11 +97,30 @@ public final class ArtifactUpload
         super.setTrace( trace );
         return this;
     }
+    
+    public ArtifactUpload setFileTransformer( FileTransformer fileTransformer )
+    {
+        this.fileTransformer = fileTransformer;
+        return this;
+    }
+    
+    public FileTransformer getFileTransformer()
+    {
+        return fileTransformer;
+    }
 
     @Override
     public String toString()
     {
-        return getArtifact() + " - " + getFile();
+        if ( getFileTransformer() != null )
+        {
+            return getArtifact() + " >>> " + getFileTransformer().transformArtifact( getArtifact() )
+                + " - " + getFile();
+        }
+        else
+        {
+            return getArtifact() + " - " + getFile();
+        }
     }
 
 }
diff --git a/maven-resolver-transport-http/src/test/java/org/eclipse/aether/transport/http/HttpTransporterTest.java b/maven-resolver-transport-http/src/test/java/org/eclipse/aether/transport/http/HttpTransporterTest.java
index 880f2d8..384827f 100644
--- a/maven-resolver-transport-http/src/test/java/org/eclipse/aether/transport/http/HttpTransporterTest.java
+++ b/maven-resolver-transport-http/src/test/java/org/eclipse/aether/transport/http/HttpTransporterTest.java
@@ -921,7 +921,7 @@ public class HttpTransporterTest
         assertEquals( 1, listener.startedCount );
     }
 
-    @Test( timeout = 10000L )
+    @Test( timeout = 20000L )
     public void testConcurrency()
         throws Exception
     {
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/ChecksumUtils.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/ChecksumUtils.java
index 415e712..9b4714c 100644
--- a/maven-resolver-util/src/main/java/org/eclipse/aether/util/ChecksumUtils.java
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/ChecksumUtils.java
@@ -20,6 +20,7 @@ package org.eclipse.aether.util;
  */
 
 import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.IOException;
@@ -54,10 +55,8 @@ public final class ChecksumUtils
         throws IOException
     {
         String checksum = "";
-        BufferedReader br = null;
-        try
+        try ( BufferedReader br = new BufferedReader( new InputStreamReader( new FileInputStream( checksumFile ), StandardCharsets.UTF_8 ), 512 ) )
         {
-            br = new BufferedReader( new InputStreamReader( new FileInputStream( checksumFile ), StandardCharsets.UTF_8 ), 512 );
             while ( true )
             {
                 String line = br.readLine();
@@ -73,21 +72,6 @@ public final class ChecksumUtils
                 }
             }
         }
-        finally
-        {
-            try
-            {
-                if ( br != null )
-                {
-                    br.close();
-                    br = null;
-                }
-            }
-            catch ( IOException e )
-            {
-                // Suppressed due to an exception already thrown in the try block.
-            }
-        }
 
         if ( checksum.matches( ".+= [0-9A-Fa-f]+" ) )
         {
@@ -118,6 +102,20 @@ public final class ChecksumUtils
      * @throws IOException If the data file could not be read.
      */
     public static Map<String, Object> calc( File dataFile, Collection<String> algos )
+                    throws IOException
+    {
+       return calc( new FileInputStream( dataFile ), algos );
+    }
+
+    
+    public static Map<String, Object> calc( byte[] dataBytes, Collection<String> algos )
+                    throws IOException
+    {
+        return calc( new ByteArrayInputStream( dataBytes ), algos );
+    }
+
+    
+    private static Map<String, Object> calc( InputStream data, Collection<String> algos )
         throws IOException
     {
         Map<String, Object> results = new LinkedHashMap<String, Object>();
@@ -135,10 +133,8 @@ public final class ChecksumUtils
             }
         }
 
-        InputStream in = null;
-        try
+        try ( InputStream in = data )
         {
-            in = new FileInputStream( dataFile );
             for ( byte[] buffer = new byte[ 32 * 1024 ];; )
             {
                 int read = in.read( buffer );
@@ -151,22 +147,6 @@ public final class ChecksumUtils
                     digest.update( buffer, 0, read );
                 }
             }
-            in.close();
-            in = null;
-        }
-        finally
-        {
-            try
-            {
-                if ( in != null )
-                {
-                    in.close();
-                }
-            }
-            catch ( IOException e )
-            {
-                // Suppressed due to an exception already thrown in the try block.
-            }
         }
 
         for ( Map.Entry<String, MessageDigest> entry : digests.entrySet() )
@@ -178,6 +158,7 @@ public final class ChecksumUtils
 
         return results;
     }
+    
 
     /**
      * Creates a hexadecimal representation of the specified bytes. Each byte is converted into a two-digit hex number
diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/ChecksumUtilTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/ChecksumUtilTest.java
index b249e82..08d1c64 100644
--- a/maven-resolver-util/src/test/java/org/eclipse/aether/util/ChecksumUtilTest.java
+++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/ChecksumUtilTest.java
@@ -37,30 +37,32 @@ import org.junit.Test;
 
 public class ChecksumUtilTest
 {
-    private File emptyFile;
+    private static final String EMPTY = "EMPTY";
+    private static final String PATTERN = "PATTERN";
+    private static final String TEXT = "TEXT";
+    
+    private Map<String, File> files = new HashMap<>(3);
+    
+    private Map<String, byte[]> bytes = new HashMap<>(3);
+    
+    private static Map<String, String> emptyChecksums = new HashMap<>();
 
-    private File patternFile;
+    private static Map<String, String> patternChecksums = new HashMap<>();
 
-    private File textFile;
+    private static Map<String, String> textChecksums = new HashMap<>();
 
-    private static Map<String, String> emptyFileChecksums = new HashMap<String, String>();
-
-    private static Map<String, String> patternFileChecksums = new HashMap<String, String>();
-
-    private static Map<String, String> textFileChecksums = new HashMap<String, String>();
-
-    private Map<File, Map<String, String>> sums = new HashMap<File, Map<String, String>>();
+    private Map<String, Map<String, String>> sums = new HashMap<>();
 
     @BeforeClass
     public static void beforeClass()
         throws IOException
     {
-        emptyFileChecksums.put( "MD5", "d41d8cd98f00b204e9800998ecf8427e" );
-        emptyFileChecksums.put( "SHA-1", "da39a3ee5e6b4b0d3255bfef95601890afd80709" );
-        patternFileChecksums.put( "MD5", "14f01d6c7de7d4cf0a4887baa3528b5a" );
-        patternFileChecksums.put( "SHA-1", "feeeda19f626f9b0ef6cbf5948c1ec9531694295" );
-        textFileChecksums.put( "MD5", "12582d1a662cefe3385f2113998e43ed" );
-        textFileChecksums.put( "SHA-1", "a8ae272db549850eef2ff54376f8cac2770745ee" );
+        emptyChecksums.put( "MD5", "d41d8cd98f00b204e9800998ecf8427e" );
+        emptyChecksums.put( "SHA-1", "da39a3ee5e6b4b0d3255bfef95601890afd80709" );
+        patternChecksums.put( "MD5", "14f01d6c7de7d4cf0a4887baa3528b5a" );
+        patternChecksums.put( "SHA-1", "feeeda19f626f9b0ef6cbf5948c1ec9531694295" );
+        textChecksums.put( "MD5", "12582d1a662cefe3385f2113998e43ed" );
+        textChecksums.put( "SHA-1", "a8ae272db549850eef2ff54376f8cac2770745ee" );
     }
 
     @Before
@@ -69,15 +71,20 @@ public class ChecksumUtilTest
     {
         sums.clear();
 
-        emptyFile = createTempFile( new byte[] {}, 0 );
-        sums.put( emptyFile, emptyFileChecksums );
+        byte[] emptyBytes = new byte[0];
+        bytes.put( EMPTY, emptyBytes );
+        files.put( EMPTY, createTempFile( emptyBytes, 0 ) );
+        sums.put( EMPTY, emptyChecksums );
 
-        patternFile =
-            createTempFile( new byte[] { 0, 1, 2, 4, 8, 16, 32, 64, 127, -1, -2, -4, -8, -16, -32, -64, -127 }, 1000 );
-        sums.put( patternFile, patternFileChecksums );
+        byte[] patternBytes = writeBytes( new byte[] { 0, 1, 2, 4, 8, 16, 32, 64, 127, -1, -2, -4, -8, -16, -32, -64, -127 }, 1000 );
+        bytes.put( PATTERN, patternBytes );
+        files.put( PATTERN, createTempFile( patternBytes, 1 ) );
+        sums.put( PATTERN, patternChecksums );
 
-        textFile = createTempFile( "the quick brown fox jumps over the lazy dog\n".getBytes( StandardCharsets.UTF_8 ), 500 );
-        sums.put( textFile, textFileChecksums );
+        byte[] textBytes = writeBytes( "the quick brown fox jumps over the lazy dog\n".getBytes( StandardCharsets.UTF_8 ), 500 );
+        bytes.put( TEXT, textBytes );
+        files.put( TEXT, createTempFile( textBytes, 1 ) );
+        sums.put( TEXT, textChecksums );
 
     }
 
@@ -87,10 +94,10 @@ public class ChecksumUtilTest
     {
         Map<String, Object> checksums = null;
 
-        for ( File file : new File[] { emptyFile, patternFile, textFile } )
+        for ( Map.Entry<String,File> fileEntry : files.entrySet() )
         {
 
-            checksums = ChecksumUtils.calc( file, Arrays.asList( "SHA-1", "MD5" ) );
+            checksums = ChecksumUtils.calc( fileEntry.getValue(), Arrays.asList( "SHA-1", "MD5" ) );
 
             for ( Entry<String, Object> entry : checksums.entrySet() )
             {
@@ -99,11 +106,11 @@ public class ChecksumUtilTest
                     throw (Throwable) entry.getValue();
                 }
                 String actual = entry.getValue().toString();
-                String expected = sums.get( file ).get( entry.getKey() );
-                assertEquals( String.format( "checksums do not match for '%s', algorithm '%s'", file.getName(),
+                String expected = sums.get( fileEntry.getKey() ).get( entry.getKey() );
+                assertEquals( String.format( "checksums do not match for '%s', algorithm '%s'", fileEntry.getValue().getName(),
                                              entry.getKey() ), expected, actual );
             }
-            assertTrue( "Could not delete file", file.delete() );
+            assertTrue( "Could not delete file", fileEntry.getValue().delete() );
         }
     }
 
@@ -111,7 +118,7 @@ public class ChecksumUtilTest
     public void testFileHandleLeakage()
         throws IOException
     {
-        for ( File file : new File[] { emptyFile, patternFile, textFile } )
+        for ( File file : files.values() )
         {
             for ( int i = 0; i < 150; i++ )
             {
@@ -182,5 +189,38 @@ public class ChecksumUtilTest
         assertEquals( "ff", ChecksumUtils.toHexString( new byte[] { -1 } ) );
         assertEquals( "00017f", ChecksumUtils.toHexString( new byte[] { 0, 1, 127 } ) );
     }
+    
+    @Test
+    public void testCalcWithByteArray() throws Throwable
+    {
+        Map<String, Object> checksums = null;
+
+        for ( Map.Entry<String, byte[]> bytesEntry : bytes.entrySet() )
+        {
+            checksums = ChecksumUtils.calc( bytesEntry.getValue(), Arrays.asList( "SHA-1", "MD5" ) );
+
+            for ( Entry<String, Object> entry : checksums.entrySet() )
+            {
+                if ( entry.getValue() instanceof Throwable )
+                {
+                    throw (Throwable) entry.getValue();
+                }
+                String actual = entry.getValue().toString();
+                String expected = sums.get( bytesEntry.getKey() ).get( entry.getKey() );
+                assertEquals( String.format( "checksums do not match for '%s', algorithm '%s'", bytesEntry.getKey(),
+                                             entry.getKey() ), expected, actual );
+            }
+        }
+    }
 
+    private byte[] writeBytes( byte[] pattern, int repeat )
+    {
+        byte[] result = new byte[pattern.length * repeat];
+        for ( int i = 0; i < repeat; i++ )
+        {
+            System.arraycopy( pattern, 0, result, i * pattern.length, pattern.length );
+        }
+        return result;
+    }
 }
+