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:12 UTC

[maven-resolver] branch MRESOLVER-46 created (now 55658db)

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

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


      at 55658db  [MRESOLVER-46] Add support InputStream/OutputStream transformers

This branch includes the following new commits:

     new 55658db  [MRESOLVER-46] Add support InputStream/OutputStream transformers

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



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

Posted by rf...@apache.org.
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;
+    }
 }
+