diff --git a/maven-repository-proxy/pom.xml b/maven-repository-proxy/pom.xml
new file mode 100644
index 000000000..a6201714b
--- /dev/null
+++ b/maven-repository-proxy/pom.xml
@@ -0,0 +1,37 @@
+
+
+
+
+ org.apache.maven.repository
+ maven-repository-manager
+ 1.0-SNAPSHOT
+
+ 4.0.0
+ maven-repository-proxy
+ Maven Repository Proxy
+
+
+ org.apache.maven
+ maven-artifact
+
+
+ org.apache.maven
+ maven-artifact-manager
+
+
+
diff --git a/maven-repository-proxy/src/main/java/org/apache/maven/repository/proxy/DefaultProxyManager.java b/maven-repository-proxy/src/main/java/org/apache/maven/repository/proxy/DefaultProxyManager.java
new file mode 100644
index 000000000..b1a6e637c
--- /dev/null
+++ b/maven-repository-proxy/src/main/java/org/apache/maven/repository/proxy/DefaultProxyManager.java
@@ -0,0 +1,338 @@
+package org.apache.maven.repository.proxy;
+
+import org.apache.maven.artifact.Artifact;
+import org.apache.maven.artifact.manager.ChecksumFailedException;
+import org.apache.maven.artifact.manager.WagonManager;
+import org.apache.maven.artifact.repository.ArtifactRepository;
+import org.apache.maven.repository.proxy.configuration.ProxyConfiguration;
+import org.apache.maven.repository.proxy.files.DefaultRepositoryFileManager;
+import org.apache.maven.repository.proxy.files.Checksum;
+import org.apache.maven.repository.proxy.repository.ProxyRepository;
+import org.apache.maven.wagon.ConnectionException;
+import org.apache.maven.wagon.ResourceDoesNotExistException;
+import org.apache.maven.wagon.TransferFailedException;
+import org.apache.maven.wagon.UnsupportedProtocolException;
+import org.apache.maven.wagon.Wagon;
+import org.apache.maven.wagon.authentication.AuthenticationException;
+import org.apache.maven.wagon.authorization.AuthorizationException;
+import org.apache.maven.wagon.observers.ChecksumObserver;
+import org.codehaus.plexus.util.FileUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Iterator;
+
+/**
+ * @author Edwin Punzalan
+ */
+public class DefaultProxyManager
+ //implements ProxyManager
+{
+ /* @plexus.requirement */
+ private WagonManager wagon;
+
+ private ProxyConfiguration config;
+
+ public DefaultProxyManager( ProxyConfiguration configuration )
+ {
+ config = configuration;
+ }
+
+ public File get( String path )
+ throws ProxyException
+ {
+ String cachePath = config.getRepositoryCachePath();
+ File cachedFile = new File( cachePath, path );
+ if ( !cachedFile.exists() )
+ {
+ getRemoteFile( path );
+ }
+ return cachedFile;
+ }
+
+ public File getRemoteFile( String path )
+ throws ProxyException
+ {
+ try
+ {
+ if ( path.indexOf( "/jars/" ) >= 0 )
+ {
+ //@todo maven 1 repo request
+ throw new ProxyException( "Maven 1 repository requests not yet supported." );
+ }
+ else if ( path.indexOf( "/poms/" ) >= 0 )
+ {
+ //@todo maven 1 repo request
+ throw new ProxyException( "Maven 1 repository requests not yet supported." );
+ }
+ else
+ {
+ //maven 2 repo request
+ Object obj = new DefaultRepositoryFileManager().getRequestedObjectFromPath( path );
+
+ if ( obj == null )
+ {
+ //right now, only metadata is known to fit here
+ return getRepositoryFile( path );
+ }
+ else if ( obj instanceof Checksum )
+ {
+ return getRepositoryFile( path, false );
+ }
+ else if ( obj instanceof Artifact )
+ {
+ Artifact artifact = (Artifact) obj;
+ return getArtifactFile( artifact );
+ }
+ else
+ {
+ throw new ProxyException( "Could not hande repository object: " + obj.getClass() );
+ }
+ }
+ }
+ catch( TransferFailedException e )
+ {
+ throw new ProxyException( e.getMessage(), e );
+ }
+ catch( ResourceDoesNotExistException e)
+ {
+ throw new ProxyException( e.getMessage(), e );
+ }
+ }
+
+ private File getArtifactFile( Artifact artifact )
+ throws TransferFailedException, ResourceDoesNotExistException
+ {
+ ArtifactRepository repoCache = config.getRepositoryCache();
+
+ File artifactFile = new File( repoCache.getBasedir(), repoCache.pathOf( artifact ) );
+
+ if ( !artifactFile.exists() )
+ {
+ wagon.getArtifact( artifact, config.getRepositories() );
+ artifactFile = artifact.getFile();
+ }
+
+ return artifactFile;
+ }
+
+ private File getRepositoryFile( String path )
+ throws ProxyException
+ {
+ return getRepositoryFile( path, true );
+ }
+
+ private File getRepositoryFile( String path, boolean useChecksum )
+ throws ProxyException
+ {
+ ArtifactRepository cache = config.getRepositoryCache();
+ File target = new File( cache.getBasedir(), path );
+
+ for( Iterator repositories = config.getRepositories().iterator(); repositories.hasNext(); )
+ {
+ ProxyRepository repository = (ProxyRepository) repositories.next();
+
+ try
+ {
+ Wagon wagon = this.wagon.getWagon( repository.getProtocol() );
+
+ //@todo configure wagon
+
+ ChecksumObserver listener = null;
+ try
+ {
+ listener = repository.getChecksumObserver();
+
+ if ( listener != null )
+ {
+ wagon.addTransferListener( listener );
+ }
+ }
+ catch ( NoSuchAlgorithmException e )
+ {
+ System.out.println( "Skipping checksum validation for unsupported algorithm: "
+ + repository.getChecksum() );
+ }
+
+ if ( connectToRepository( wagon, repository ) )
+ {
+ File temp = new File( target.getAbsolutePath() + ".tmp" );
+ temp.deleteOnExit();
+
+ int tries = 0;
+ boolean success = false;
+
+ while( !success )
+ {
+ tries++;
+
+ wagon.get( path, temp );
+
+ if ( useChecksum )
+ {
+ success = doChecksumCheck( listener, repository, path, wagon );
+ }
+ else
+ {
+ success = true;
+ }
+
+ if ( tries > 1 && !success )
+ {
+ throw new ProxyException( "Checksum failures occurred while downloading " + path );
+ }
+ }
+ disconnectWagon( wagon );
+
+ return target;
+ }
+ //try next repository
+ }
+ catch ( TransferFailedException e )
+ {
+ System.out.println( "Skipping repository " + repository.getUrl() +
+ ": " + e.getMessage() );
+ }
+ catch ( ResourceDoesNotExistException e )
+ {
+ //do nothing, file not found in this repository
+ }
+ catch ( AuthorizationException e )
+ {
+ System.out.println( "Skipping repository " + repository.getUrl() +
+ ": " + e.getMessage() );
+ }
+ catch ( UnsupportedProtocolException e )
+ {
+ System.out.println( "Skipping repository " + repository.getUrl() +
+ ": no wagon configured for protocol " + repository.getProtocol() );
+ }
+ }
+
+ throw new ProxyException( "Could not find " + path + " in any of the repositories." );
+ }
+
+ private boolean connectToRepository( Wagon wagon, ProxyRepository repository )
+ {
+ boolean connected = false;
+ try
+ {
+ wagon.connect( repository );
+ connected = true;
+ }
+ catch ( ConnectionException e )
+ {
+ System.out.println( "Could not connect to " + repository.getId() + ": " + e.getMessage() );
+ }
+ catch ( AuthenticationException e )
+ {
+ System.out.println( "Could not connect to " + repository.getId() + ": " + e.getMessage() );
+ }
+
+ return connected;
+ }
+
+ private boolean doChecksumCheck( ChecksumObserver listener, ProxyRepository repository, String path, Wagon wagon )
+ //throws ChecksumFailedException
+ {
+ boolean success = false;
+
+ try
+ {
+ String checksumExt = repository.getChecksum().getFileExtension();
+ String remotePath = path + "." + checksumExt;
+ File checksumFile = new File( config.getRepositoryCache().getBasedir(), remotePath );
+
+ verifyChecksum( listener.getActualChecksum(), checksumFile, remotePath, checksumExt, wagon );
+
+ wagon.removeTransferListener( listener );
+
+ success = true;
+ }
+ catch ( ChecksumFailedException e )
+ {
+ System.out.println( "*** CHECKSUM FAILED - " + e.getMessage() + " - RETRYING" );
+ }
+
+ return success;
+ }
+
+ private void verifyChecksum( String actualChecksum, File destination, String remotePath,
+ String checksumFileExtension, Wagon wagon )
+ throws ChecksumFailedException
+ {
+ try
+ {
+ File tempDestination = new File( destination.getAbsolutePath() + ".tmp" );
+ tempDestination.deleteOnExit();
+
+ File tempChecksumFile = new File( tempDestination + checksumFileExtension + ".tmp" );
+ tempChecksumFile.deleteOnExit();
+
+ wagon.get( remotePath + checksumFileExtension, tempChecksumFile );
+
+ String expectedChecksum = FileUtils.fileRead( tempChecksumFile );
+
+ // remove whitespaces at the end
+ expectedChecksum = expectedChecksum.trim();
+
+ // check for 'MD5 (name) = CHECKSUM'
+ if ( expectedChecksum.startsWith( "MD5" ) )
+ {
+ int lastSpacePos = expectedChecksum.lastIndexOf( ' ' );
+ expectedChecksum = expectedChecksum.substring( lastSpacePos + 1 );
+ }
+ else
+ {
+ // remove everything after the first space (if available)
+ int spacePos = expectedChecksum.indexOf( ' ' );
+
+ if ( spacePos != -1 )
+ {
+ expectedChecksum = expectedChecksum.substring( 0, spacePos );
+ }
+ }
+
+ if ( expectedChecksum.equals( actualChecksum ) )
+ {
+ File checksumFile = new File( destination + checksumFileExtension );
+ if ( checksumFile.exists() ) checksumFile.delete();
+ FileUtils.copyFile( tempChecksumFile, checksumFile );
+ }
+ else
+ {
+ throw new ChecksumFailedException( "Checksum failed on download: local = '" + actualChecksum +
+ "'; remote = '" + expectedChecksum + "'" );
+ }
+ }
+ catch ( TransferFailedException e )
+ {
+ System.out.println( "Skipping checksum validation for " + remotePath + ": " + e.getMessage() );
+ }
+ catch ( ResourceDoesNotExistException e )
+ {
+ System.out.println( "Skipping checksum validation for " + remotePath + ": " + e.getMessage() );
+ }
+ catch ( AuthorizationException e )
+ {
+ System.out.println( "Skipping checksum validation for " + remotePath + ": " + e.getMessage() );
+ }
+ catch ( IOException e )
+ {
+ throw new ChecksumFailedException( "Invalid checksum file", e );
+ }
+ }
+
+ private void disconnectWagon( Wagon wagon )
+ {
+ try
+ {
+ wagon.disconnect();
+ }
+ catch ( ConnectionException e )
+ {
+ System.err.println( "Problem disconnecting from wagon - ignoring: " + e.getMessage() );
+ }
+ }
+}
diff --git a/maven-repository-proxy/src/main/java/org/apache/maven/repository/proxy/ProxyException.java b/maven-repository-proxy/src/main/java/org/apache/maven/repository/proxy/ProxyException.java
new file mode 100644
index 000000000..92ac871c4
--- /dev/null
+++ b/maven-repository-proxy/src/main/java/org/apache/maven/repository/proxy/ProxyException.java
@@ -0,0 +1,18 @@
+package org.apache.maven.repository.proxy;
+
+/**
+ * @author Edwin Punzalan
+ */
+public class ProxyException
+ extends Exception
+{
+ public ProxyException( String message )
+ {
+ super( message );
+ }
+
+ public ProxyException( String message, Throwable t )
+ {
+ super( message, t );
+ }
+}
diff --git a/maven-repository-proxy/src/main/java/org/apache/maven/repository/proxy/ProxyManager.java b/maven-repository-proxy/src/main/java/org/apache/maven/repository/proxy/ProxyManager.java
new file mode 100644
index 000000000..3c582aa4c
--- /dev/null
+++ b/maven-repository-proxy/src/main/java/org/apache/maven/repository/proxy/ProxyManager.java
@@ -0,0 +1,20 @@
+package org.apache.maven.repository.proxy;
+
+import org.apache.maven.artifact.Artifact;
+import org.apache.maven.wagon.ResourceDoesNotExistException;
+import org.apache.maven.wagon.TransferFailedException;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.File;
+
+/**
+ * @author Edwin Punzalan
+ */
+public interface ProxyManager
+{
+ File getArtifactFile( Artifact artifact )
+ throws TransferFailedException, ResourceDoesNotExistException, IOException;
+ InputStream getArtifactAsStream( Artifact artifact )
+ throws TransferFailedException, ResourceDoesNotExistException, IOException;
+}
diff --git a/maven-repository-proxy/src/main/java/org/apache/maven/repository/proxy/configuration/ProxyConfiguration.java b/maven-repository-proxy/src/main/java/org/apache/maven/repository/proxy/configuration/ProxyConfiguration.java
new file mode 100644
index 000000000..990e15480
--- /dev/null
+++ b/maven-repository-proxy/src/main/java/org/apache/maven/repository/proxy/configuration/ProxyConfiguration.java
@@ -0,0 +1,76 @@
+package org.apache.maven.repository.proxy.configuration;
+
+import org.apache.maven.artifact.repository.ArtifactRepository;
+import org.apache.maven.artifact.repository.ArtifactRepositoryPolicy;
+import org.apache.maven.artifact.repository.ArtifactRepositoryFactory;
+import org.apache.maven.artifact.repository.layout.ArtifactRepositoryLayout;
+import org.apache.maven.artifact.repository.layout.DefaultRepositoryLayout;
+import org.apache.maven.repository.proxy.repository.ProxyRepository;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Collections;
+
+/**
+ * @plexus.component role="org.apache.maven.repository.proxy.configuration.ProxyConfiguration"
+ *
+ * @author Edwin Punzalan
+ */
+public class ProxyConfiguration
+{
+ public static final String ROLE = ProxyConfiguration.class.getName();
+
+ /** @plexus.requirement */
+ private ArtifactRepositoryFactory artifactRepositoryFactory;
+
+ private boolean browsable;
+ private ArtifactRepository repoCache;
+ private ArrayList repositories = new ArrayList();
+
+ public void setBrowsable( boolean browsable )
+ {
+ this.browsable = browsable;
+ }
+
+ public boolean isBrowsable()
+ {
+ return browsable;
+ }
+
+ public void setRepositoryCachePath( String repoCachePath )
+ {
+ ArtifactRepositoryPolicy standardPolicy;
+ standardPolicy = new ArtifactRepositoryPolicy( true, ArtifactRepositoryPolicy.UPDATE_POLICY_ALWAYS,
+ ArtifactRepositoryPolicy.CHECKSUM_POLICY_IGNORE );
+
+ ArtifactRepositoryLayout layout = new DefaultRepositoryLayout();
+
+ repoCache = artifactRepositoryFactory.createArtifactRepository( "localCache", repoCachePath, layout,
+ standardPolicy, standardPolicy );
+ }
+
+ public ArtifactRepository getRepositoryCache( )
+ {
+ return repoCache;
+ }
+
+ public String getRepositoryCachePath()
+ {
+ return repoCache.getBasedir();
+ }
+
+ public void addRepository( ProxyRepository repository )
+ {
+ repositories.add( repository );
+ }
+
+ public List getRepositories()
+ {
+ return Collections.unmodifiableList( repositories );
+ }
+
+ public void setRepositories( ArrayList repositories )
+ {
+ this.repositories = repositories;
+ }
+}
diff --git a/maven-repository-proxy/src/main/java/org/apache/maven/repository/proxy/files/Checksum.java b/maven-repository-proxy/src/main/java/org/apache/maven/repository/proxy/files/Checksum.java
new file mode 100644
index 000000000..458848899
--- /dev/null
+++ b/maven-repository-proxy/src/main/java/org/apache/maven/repository/proxy/files/Checksum.java
@@ -0,0 +1,36 @@
+package org.apache.maven.repository.proxy.files;
+
+/**
+ * @author Edwin Punzalan
+ */
+public class Checksum
+{
+ private String algorithm;
+
+ public Checksum( String algorithm )
+ {
+ this.setAlgorithm( algorithm );
+ }
+
+ public String getFileExtension()
+ {
+ if ( "MD5".equals( algorithm ) )
+ {
+ return "md5";
+ }
+ else
+ {
+ return "sha1";
+ }
+ }
+
+ public String getAlgorithm()
+ {
+ return algorithm;
+ }
+
+ public void setAlgorithm( String algorithm )
+ {
+ this.algorithm = algorithm;
+ }
+}
diff --git a/maven-repository-proxy/src/main/java/org/apache/maven/repository/proxy/files/DefaultRepositoryFileManager.java b/maven-repository-proxy/src/main/java/org/apache/maven/repository/proxy/files/DefaultRepositoryFileManager.java
new file mode 100644
index 000000000..f3317735c
--- /dev/null
+++ b/maven-repository-proxy/src/main/java/org/apache/maven/repository/proxy/files/DefaultRepositoryFileManager.java
@@ -0,0 +1,95 @@
+package org.apache.maven.repository.proxy.files;
+
+import org.apache.maven.artifact.Artifact;
+import org.apache.maven.artifact.factory.ArtifactFactory;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.StringTokenizer;
+
+/**
+ * @author Edwin Punzalan
+ */
+public class DefaultRepositoryFileManager
+{
+ /* @plexus.requirement */
+ private ArtifactFactory factory;
+
+ public Object getRequestedObjectFromPath( String path )
+ {
+ if ( path.endsWith( ".pom" ) )
+ {
+ return getArtifactFromPath( path );
+ }
+ else if ( path.endsWith( "ar" ) )
+ {
+ return getArtifactFromPath( path );
+ }
+ else if ( path.endsWith( ".md5" ) )
+ {
+ return new Checksum( "MD5" );
+ }
+ else if ( path.endsWith( ".sha1" ) )
+ {
+ return new Checksum( "SHA-1" );
+ }
+ else
+ {
+ //@todo handle metadata file requests
+ return null;
+ }
+ }
+
+ private Artifact getArtifactFromPath( String path )
+ {
+ List pathInfo = getReversedPathInfo( path );
+ String filename = getPathToken( pathInfo );
+ String version = getPathToken( pathInfo );
+ String artifactId = getPathToken( pathInfo );
+ String groupId = "";
+ while ( pathInfo.size() > 0 )
+ {
+ if ( groupId.length() == 0 )
+ {
+ groupId = "." + groupId;
+ }
+ else
+ {
+ groupId = getPathToken( pathInfo ) + "." + groupId;
+ }
+ }
+
+ return factory.createBuildArtifact( groupId, artifactId, version, getFileExtension( filename ) );
+ }
+
+ private List getReversedPathInfo( String path )
+ {
+ List reversedPath = new ArrayList();
+
+ StringTokenizer tokenizer = new StringTokenizer( path, "/" );
+ while( tokenizer.hasMoreTokens() )
+ {
+ reversedPath.add( 0, tokenizer.nextToken() );
+ }
+
+ return reversedPath;
+ }
+
+ private String getPathToken( List path )
+ {
+ if ( path.size() > 0 )
+ {
+ return (String) path.remove( 0 );
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ private String getFileExtension( String filename )
+ {
+ int idx = filename.lastIndexOf( '.' );
+ return filename.substring( idx + 1 );
+ }
+}
diff --git a/maven-repository-proxy/src/main/java/org/apache/maven/repository/proxy/repository/ProxyRepository.java b/maven-repository-proxy/src/main/java/org/apache/maven/repository/proxy/repository/ProxyRepository.java
new file mode 100644
index 000000000..d0e833952
--- /dev/null
+++ b/maven-repository-proxy/src/main/java/org/apache/maven/repository/proxy/repository/ProxyRepository.java
@@ -0,0 +1,38 @@
+package org.apache.maven.repository.proxy.repository;
+
+import org.apache.maven.artifact.repository.DefaultArtifactRepository;
+import org.apache.maven.artifact.repository.layout.ArtifactRepositoryLayout;
+import org.apache.maven.wagon.observers.ChecksumObserver;
+import org.apache.maven.repository.proxy.files.Checksum;
+
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * @author Edwin Punzalan
+ */
+public class ProxyRepository
+ extends DefaultArtifactRepository
+{
+ private Checksum checksum;
+
+ public ProxyRepository( String id, String url, ArtifactRepositoryLayout layout )
+ {
+ super( id, url, layout );
+ }
+
+ public void setChecksum( String algorithm )
+ {
+ this.checksum = new Checksum( algorithm );
+ }
+
+ public Checksum getChecksum()
+ {
+ return checksum;
+ }
+
+ public ChecksumObserver getChecksumObserver()
+ throws NoSuchAlgorithmException
+ {
+ return new ChecksumObserver( checksum.getAlgorithm() );
+ }
+}
diff --git a/pom.xml b/pom.xml
index 7358f2a29..21b792983 100644
--- a/pom.xml
+++ b/pom.xml
@@ -137,6 +137,7 @@
maven-repository-indexer
maven-repository-utils
maven-repository-webapp
+ maven-repository-proxy