1 package org.apache.maven.archiva.proxy;
4 * Licensed to the Apache Software Foundation (ASF) under one
5 * or more contributor license agreements. See the NOTICE file
6 * distributed with this work for additional information
7 * regarding copyright ownership. The ASF licenses this file
8 * to you under the Apache License, Version 2.0 (the
9 * "License"); you may not use this file except in compliance
10 * with the License. You may obtain a copy of the License at
12 * http://www.apache.org/licenses/LICENSE-2.0
14 * Unless required by applicable law or agreed to in writing,
15 * software distributed under the License is distributed on an
16 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17 * KIND, either express or implied. See the License for the
18 * specific language governing permissions and limitations
22 import org.apache.commons.io.FileUtils;
23 import org.apache.commons.io.IOUtils;
24 import org.apache.maven.archiva.common.artifact.builder.BuilderException;
25 import org.apache.maven.archiva.common.artifact.builder.LayoutArtifactBuilder;
26 import org.apache.maven.artifact.Artifact;
27 import org.apache.maven.artifact.factory.ArtifactFactory;
28 import org.apache.maven.artifact.repository.ArtifactRepository;
29 import org.apache.maven.artifact.repository.ArtifactRepositoryPolicy;
30 import org.apache.maven.artifact.repository.metadata.Metadata;
31 import org.apache.maven.artifact.repository.metadata.Versioning;
32 import org.apache.maven.artifact.repository.metadata.io.xpp3.MetadataXpp3Reader;
33 import org.apache.maven.artifact.repository.metadata.io.xpp3.MetadataXpp3Writer;
34 import org.apache.maven.model.DistributionManagement;
35 import org.apache.maven.model.Model;
36 import org.apache.maven.model.Relocation;
37 import org.apache.maven.model.io.xpp3.MavenXpp3Reader;
38 import org.apache.maven.wagon.ConnectionException;
39 import org.apache.maven.wagon.ResourceDoesNotExistException;
40 import org.apache.maven.wagon.TransferFailedException;
41 import org.apache.maven.wagon.Wagon;
42 import org.apache.maven.wagon.authentication.AuthenticationException;
43 import org.apache.maven.wagon.authorization.AuthorizationException;
44 import org.apache.maven.wagon.observers.ChecksumObserver;
45 import org.apache.maven.wagon.proxy.ProxyInfo;
46 import org.apache.maven.wagon.repository.Repository;
47 import org.codehaus.plexus.digest.DigestUtils;
48 import org.codehaus.plexus.digest.DigesterException;
49 import org.codehaus.plexus.logging.AbstractLogEnabled;
50 import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
53 import java.io.FileReader;
54 import java.io.FileWriter;
55 import java.io.IOException;
56 import java.io.Reader;
57 import java.security.NoSuchAlgorithmException;
58 import java.text.DateFormat;
59 import java.text.SimpleDateFormat;
60 import java.util.Date;
61 import java.util.Iterator;
62 import java.util.LinkedHashMap;
63 import java.util.List;
64 import java.util.Locale;
66 import java.util.TimeZone;
69 * An implementation of the proxy handler. This class is not thread safe (the class itself is, but the wagons it uses
70 * are not) - it is declared <code>per-lookup</code> for that reason.
72 * @author <a href="mailto:brett@apache.org">Brett Porter</a>
73 * @plexus.component instantiation-strategy="per-lookup"
74 * @todo use wagonManager for cache use file:// as URL
75 * @todo this currently duplicates a lot of the wagon manager, and doesn't do things like snapshot resolution, etc.
76 * The checksum handling is inconsistent with that of the wagon manager.
77 * Should we have a more artifact based one? This will merge metadata so should behave correctly, and it is able to
78 * correct some limitations of the wagon manager (eg, it can retrieve newer SNAPSHOT files without metadata)
80 public class DefaultProxyRequestHandler
81 extends AbstractLogEnabled
82 implements ProxyRequestHandler
87 private ArtifactFactory factory;
90 * @plexus.requirement role-hint="default"
91 * @todo use a map, and have priorities in them.
93 private LayoutArtifactBuilder defaultArtifactBuilder;
96 * @plexus.requirement role-hint="legacy"
98 private LayoutArtifactBuilder legacyArtifactBuilder;
101 * @plexus.requirement role="org.apache.maven.wagon.Wagon"
103 private Map/*<String,Wagon>*/ wagons;
105 private static final TimeZone UTC_TIMEZONE = TimeZone.getTimeZone( "UTC" );
107 public File get( String path, List proxiedRepositories, ArtifactRepository managedRepository )
108 throws ProxyException, ResourceDoesNotExistException
110 return get( path, proxiedRepositories, managedRepository, null );
113 public File get( String path, List proxiedRepositories, ArtifactRepository managedRepository, ProxyInfo wagonProxy )
114 throws ProxyException, ResourceDoesNotExistException
116 return get( managedRepository, path, proxiedRepositories, wagonProxy, false );
119 public File getAlways( String path, List proxiedRepositories, ArtifactRepository managedRepository )
120 throws ProxyException, ResourceDoesNotExistException
122 return getAlways( path, proxiedRepositories, managedRepository, null );
125 public File getAlways( String path, List proxiedRepositories, ArtifactRepository managedRepository,
126 ProxyInfo wagonProxy )
127 throws ResourceDoesNotExistException, ProxyException
129 return get( managedRepository, path, proxiedRepositories, wagonProxy, true );
132 private File get( ArtifactRepository managedRepository, String path, List proxiedRepositories, ProxyInfo wagonProxy,
134 throws ProxyException, ResourceDoesNotExistException
136 File target = new File( managedRepository.getBasedir(), path );
138 if ( path.endsWith( "maven-metadata.xml" ) )
140 // Request for managed repository metadatas
141 getMetadata( path, target, proxiedRepositories, managedRepository, wagonProxy, force );
145 boolean checksum = false;
146 String checksumExtension = null;
147 String artifactPath = path;
148 if ( path.endsWith( ".md5" ) || path.endsWith( ".sha1" ) )
150 int index = path.lastIndexOf( '.' );
151 checksumExtension = path.substring( index + 1 );
153 artifactPath = path.substring( 0, index );
158 // Request for artifact: parse the requested path to build an Artifact.
159 Artifact artifact = null;
162 artifact = defaultArtifactBuilder.build( artifactPath );
163 getLogger().debug( "Artifact requested is: " + artifact );
165 catch ( BuilderException e )
167 msg = "Failed to build artifact from path:\n\tfrom default: " + e.getMessage();
170 if ( artifact == null )
174 artifact = legacyArtifactBuilder.build( artifactPath );
175 getLogger().debug( "Artifact requested is: " + artifact );
177 catch ( BuilderException e )
179 getLogger().debug( msg + "\n\tfrom legacy: " + e.getMessage() );
183 if ( artifact != null )
185 applyRelocation( managedRepository, artifact, proxiedRepositories, wagonProxy, force );
189 // Build the target file name
190 target = new File( managedRepository.getBasedir(), managedRepository.pathOf( artifact ) );
192 // Get the requested artifact from proxiedRepositories
193 getArtifactFromRepository( managedRepository, target, artifact, proxiedRepositories, wagonProxy,
198 // Just adjust the filename for relocation, don't actualy get it
199 target = new File( managedRepository.getBasedir(),
200 managedRepository.pathOf( artifact ) + "." + checksumExtension );
203 else if ( !checksum )
205 // Some other unknown file in the repository, proxy as is, unless it was a checksum
206 if ( force || !target.exists() )
208 getFileFromRepository( managedRepository, target, path, proxiedRepositories, wagonProxy, force );
213 if ( !target.exists() )
215 throw new ResourceDoesNotExistException( "Could not find " + path + " in any of the repositories." );
221 private void getFileFromRepository( ArtifactRepository managedRepository, File target, String path,
222 List proxiedRepositories, ProxyInfo wagonProxy, boolean force )
223 throws ProxyException, ResourceDoesNotExistException
225 for ( Iterator i = proxiedRepositories.iterator(); i.hasNext(); )
227 ProxiedArtifactRepository repository = (ProxiedArtifactRepository) i.next();
229 if ( !force && repository.isCachedFailure( path ) )
231 processCachedRepositoryFailure( repository, "Cached failure found for: " + path );
235 ArtifactRepositoryPolicy policy = repository.getRepository().getReleases();
236 getFileFromRepository( path, repository, managedRepository.getBasedir(), wagonProxy, target, policy,
242 private void getArtifactFromRepository( ArtifactRepository managedRepository, File target, Artifact artifact,
243 List proxiedRepositories, ProxyInfo wagonProxy, boolean force )
244 throws ProxyException, ResourceDoesNotExistException
246 for ( Iterator i = proxiedRepositories.iterator(); i.hasNext(); )
248 ProxiedArtifactRepository repository = (ProxiedArtifactRepository) i.next();
249 String path = repository.getRepository().getLayout().pathOf( artifact );
251 if ( !force && repository.isCachedFailure( path ) )
253 processCachedRepositoryFailure( repository, "Cached failure found for: " + path );
257 get( artifact, target, repository, managedRepository, wagonProxy, force );
262 private void applyRelocation( ArtifactRepository managedRepository, Artifact artifact, List proxiedRepositories,
263 ProxyInfo wagonProxy, boolean force )
265 Artifact pomArtifact =
266 factory.createProjectArtifact( artifact.getGroupId(), artifact.getArtifactId(), artifact.getVersion() );
268 File pomFile = new File( managedRepository.getBasedir(), managedRepository.pathOf( pomArtifact ) );
271 getArtifactFromRepository( managedRepository, pomFile, pomArtifact, proxiedRepositories, wagonProxy,
274 catch ( ProxyException e )
276 getLogger().warn( "Error getting POM for artifact - not relocating: " + e.getMessage() );
277 getLogger().debug( "Cause", e );
279 catch ( ResourceDoesNotExistException e )
281 getLogger().debug( "Remote POM not found for artifact - not relocating" );
284 if ( pomFile.exists() )
289 // Parse the pom and look at relocation metadata
290 Reader reader = new FileReader( pomFile );
291 model = new MavenXpp3Reader().read( reader );
293 catch ( IOException e )
295 getLogger().warn( "Error reading POM for artifact - not relocating: " + e.getMessage() );
296 getLogger().debug( "Cause", e );
298 catch ( XmlPullParserException e )
300 getLogger().warn( "Error parsing POM for artifact - not relocating: " + e.getMessage() );
301 getLogger().debug( "Cause", e );
306 DistributionManagement dist;
307 dist = model.getDistributionManagement();
311 Relocation relocation = dist.getRelocation();
312 if ( relocation != null )
315 artifact.getGroupId() + ":" + artifact.getArtifactId() + ":" + artifact.getVersion();
317 // artifact is relocated : update the artifact
318 if ( relocation.getGroupId() != null )
320 artifact.setGroupId( relocation.getGroupId() );
322 if ( relocation.getArtifactId() != null )
324 artifact.setArtifactId( relocation.getArtifactId() );
326 if ( relocation.getVersion() != null )
328 artifact.setVersion( relocation.getVersion() );
332 artifact.getGroupId() + ":" + artifact.getArtifactId() + ":" + artifact.getVersion();
334 getLogger().debug( "Artifact " + requestedId + " has been relocated to " + relocatedId +
335 ( relocation.getMessage() != null ? ": " + relocation.getMessage() : "" ) );
337 applyRelocation( managedRepository, artifact, proxiedRepositories, wagonProxy, force );
344 private void getMetadata( String path, File target, List proxiedRepositories, ArtifactRepository managedRepository,
345 ProxyInfo wagonProxy, boolean force )
346 throws ProxyException
348 for ( Iterator i = proxiedRepositories.iterator(); i.hasNext(); )
350 ProxiedArtifactRepository repository = (ProxiedArtifactRepository) i.next();
351 File metadataFile = new File( target.getParentFile(), ".metadata-" + repository.getRepository().getId() );
353 ArtifactRepositoryPolicy policy = repository.getRepository().getReleases();
355 // if it is snapshot metadata, use a different policy
356 if ( path.endsWith( "-SNAPSHOT/maven-metadata.xml" ) )
358 policy = repository.getRepository().getSnapshots();
361 if ( force || !metadataFile.exists() || isOutOfDate( policy, metadataFile ) )
363 getFileFromRepository( path, repository, managedRepository.getBasedir(), wagonProxy, metadataFile,
366 mergeMetadataFiles( target, metadataFile );
371 private void get( Artifact artifact, File target, ProxiedArtifactRepository repository,
372 ArtifactRepository managedRepository, ProxyInfo wagonProxy, boolean force )
373 throws ProxyException
375 ArtifactRepository artifactRepository = repository.getRepository();
377 // we use the release policy for tracking failures, but only check for updates on snapshots
378 // also, we don't look for updates on timestamp snapshot files, only non-unique-version ones
379 ArtifactRepositoryPolicy policy =
380 artifact.isSnapshot() ? artifactRepository.getSnapshots() : artifactRepository.getReleases();
382 boolean needsUpdate = false;
383 if ( artifact.getVersion().endsWith( "-SNAPSHOT" ) && isOutOfDate( policy, target ) )
388 if ( needsUpdate || force || !target.exists() )
390 getFileFromRepository( artifactRepository.pathOf( artifact ), repository, managedRepository.getBasedir(),
391 wagonProxy, target, policy, force );
395 private void mergeMetadataFiles( File target, File metadataFile )
396 throws ProxyException
398 MetadataXpp3Reader reader = new MetadataXpp3Reader();
399 if ( metadataFile.exists() )
401 Metadata metadata = null;
402 if ( target.exists() )
404 FileReader fileReader = null;
407 fileReader = new FileReader( target );
408 metadata = reader.read( fileReader );
410 catch ( XmlPullParserException e )
412 throw new ProxyException( "Unable to parse existing metadata: " + e.getMessage(), e );
414 catch ( IOException e )
416 throw new ProxyException( "Unable to read existing metadata: " + e.getMessage(), e );
420 IOUtils.closeQuietly( fileReader );
424 FileReader fileReader = null;
425 boolean changed = false;
428 fileReader = new FileReader( metadataFile );
429 Metadata newMetadata = reader.read( fileReader );
431 if ( metadata != null )
433 setLastUpdatedIfEmpty( newMetadata, metadataFile );
434 setLastUpdatedIfEmpty( metadata, target );
436 changed = metadata.merge( newMetadata );
440 metadata = newMetadata;
444 catch ( IOException e )
446 // ignore the merged file
447 getLogger().warn( "Unable to read new metadata: " + e.getMessage() );
449 catch ( XmlPullParserException e )
451 // ignore the merged file
452 getLogger().warn( "Unable to parse new metadata: " + e.getMessage() );
456 IOUtils.closeQuietly( fileReader );
461 FileWriter fileWriter = null;
464 fileWriter = new FileWriter( target );
465 new MetadataXpp3Writer().write( fileWriter, metadata );
467 catch ( IOException e )
469 getLogger().warn( "Unable to store new metadata: " + e.getMessage() );
473 IOUtils.closeQuietly( fileWriter );
479 private void setLastUpdatedIfEmpty( Metadata metadata, File metadataFile )
481 if ( metadata.getVersioning() == null )
483 metadata.setVersioning( new Versioning() );
485 if ( metadata.getVersioning().getLastUpdated() == null )
487 DateFormat fmt = new SimpleDateFormat( "yyyyMMddHHmmss", Locale.US );
488 fmt.setTimeZone( UTC_TIMEZONE );
489 metadata.getVersioning().setLastUpdated( fmt.format( new Date( metadataFile.lastModified() ) ) );
493 private void getFileFromRepository( String path, ProxiedArtifactRepository repository, String repositoryCachePath,
494 ProxyInfo httpProxy, File target, ArtifactRepositoryPolicy policy,
496 throws ProxyException
498 if ( !policy.isEnabled() )
500 getLogger().debug( "Skipping disabled repository " + repository.getName() );
504 Map checksums = null;
507 File temp = new File( target.getAbsolutePath() + ".tmp" );
510 boolean connected = false;
513 String protocol = repository.getRepository().getProtocol();
514 wagon = (Wagon) wagons.get( protocol );
517 throw new ProxyException( "Unsupported remote protocol: " + protocol );
520 //@todo configure wagon (ssh settings, etc)
522 checksums = prepareChecksumListeners( wagon );
524 connected = connectToRepository( wagon, repository, httpProxy );
534 boolean downloaded = true;
535 if ( force || !target.exists() )
537 getLogger().debug( "Retrieving " + path + " from " + repository.getName() );
538 wagon.get( path, temp );
542 getLogger().debug( "Retrieving " + path + " from " + repository.getName() + " if updated" );
543 downloaded = wagon.getIfNewer( path, temp, target.lastModified() );
548 success = checkChecksum( checksums, path, wagon, repositoryCachePath );
550 if ( tries > 1 && !success )
552 processRepositoryFailure( repository,
553 "Checksum failures occurred while downloading " + path, path,
560 // getIfNewer determined we were up to date
566 // temp won't exist if we called getIfNewer and it was older, but its still a successful return
569 moveTempToTarget( temp, target );
572 getLogger().debug( "Successfully downloaded" );
574 //try next repository
576 catch ( TransferFailedException e )
578 processRepositoryFailure( repository, e, path, policy );
580 catch ( AuthorizationException e )
582 processRepositoryFailure( repository, e, path, policy );
584 catch ( ResourceDoesNotExistException e )
586 // hard failure setting doesn't affect "not found".
587 getLogger().debug( "Artifact not found in repository: " + repository.getName() + ": " + e.getMessage() );
593 if ( wagon != null && checksums != null )
595 releaseChecksumListeners( wagon, checksums );
600 disconnectWagon( wagon );
605 private static boolean isOutOfDate( ArtifactRepositoryPolicy policy, File target )
607 return policy != null && policy.checkOutOfDate( new Date( target.lastModified() ) );
611 * Used to add checksum observers as transfer listeners to the wagonManager object
613 * @param wagon the wagonManager object to use the checksum with
614 * @return map of ChecksumObservers added into the wagonManager transfer listeners
616 private Map prepareChecksumListeners( Wagon wagon )
618 Map checksums = new LinkedHashMap();
621 ChecksumObserver checksum = new ChecksumObserver( "SHA-1" );
622 wagon.addTransferListener( checksum );
623 checksums.put( "sha1", checksum );
625 checksum = new ChecksumObserver( "MD5" );
626 wagon.addTransferListener( checksum );
627 checksums.put( "md5", checksum );
629 catch ( NoSuchAlgorithmException e )
631 getLogger().error( "An error occurred while preparing checksum observers: " + e.getMessage() );
636 private void releaseChecksumListeners( Wagon wagon, Map checksumMap )
638 for ( Iterator checksums = checksumMap.values().iterator(); checksums.hasNext(); )
640 ChecksumObserver listener = (ChecksumObserver) checksums.next();
641 wagon.removeTransferListener( listener );
645 private boolean connectToRepository( Wagon wagon, ProxiedArtifactRepository repository, ProxyInfo httpProxy )
647 boolean connected = false;
650 ArtifactRepository artifactRepository = repository.getRepository();
651 Repository wagonRepository = new Repository( artifactRepository.getId(), artifactRepository.getUrl() );
652 if ( repository.isUseNetworkProxy() && httpProxy != null )
654 wagon.connect( wagonRepository, httpProxy );
658 wagon.connect( wagonRepository );
662 catch ( ConnectionException e )
664 getLogger().info( "Could not connect to " + repository.getName() + ": " + e.getMessage() );
666 catch ( AuthenticationException e )
668 getLogger().info( "Could not connect to " + repository.getName() + ": " + e.getMessage() );
674 private boolean checkChecksum( Map checksumMap, String path, Wagon wagon, String repositoryCachePath )
675 throws ProxyException
677 releaseChecksumListeners( wagon, checksumMap );
679 boolean correctChecksum = false;
681 boolean allNotFound = true;
683 for ( Iterator i = checksumMap.keySet().iterator(); i.hasNext() && !correctChecksum; )
685 String checksumExt = (String) i.next();
686 ChecksumObserver checksum = (ChecksumObserver) checksumMap.get( checksumExt );
687 String checksumPath = path + "." + checksumExt;
688 File checksumFile = new File( repositoryCachePath, checksumPath );
690 File tempChecksumFile = new File( checksumFile.getAbsolutePath() + ".tmp" );
691 tempChecksumFile.deleteOnExit();
695 wagon.get( checksumPath, tempChecksumFile );
699 String remoteChecksum = DigestUtils.cleanChecksum( FileUtils.readFileToString( tempChecksumFile, null ),
700 checksumExt.toUpperCase(),
701 path.substring( path.lastIndexOf( '/' ) + 1 ) );
703 String actualChecksum = checksum.getActualChecksum();
705 remoteChecksum = remoteChecksum.toUpperCase();
707 if ( actualChecksum != null && remoteChecksum.equals( actualChecksum.toUpperCase() ) )
709 moveTempToTarget( tempChecksumFile, checksumFile );
711 correctChecksum = true;
716 "The checksum '" + actualChecksum + "' did not match the remote value: " + remoteChecksum );
719 catch ( TransferFailedException e )
721 getLogger().warn( "An error occurred during the download of " + checksumPath + ": " + e.getMessage() );
722 // do nothing try the next checksum
726 catch ( ResourceDoesNotExistException e )
728 getLogger().debug( "The checksum did not exist: " + checksumPath + "; " + e.getMessage() );
729 // do nothing try the next checksum
730 // remove it if it is present locally in case there is an old incorrect one
731 if ( checksumFile.exists() )
733 checksumFile.delete();
736 catch ( AuthorizationException e )
738 getLogger().warn( "An error occurred during the download of " + checksumPath + ": " + e.getMessage() );
739 // do nothing try the next checksum
743 catch ( IOException e )
745 getLogger().warn( "An error occurred while reading the temporary checksum file: " + e.getMessage() );
746 // do nothing try the next checksum
750 catch ( DigesterException e )
752 getLogger().warn( "The checksum was invalid: " + checksumPath + ": " + e.getMessage() );
753 // do nothing try the next checksum
759 tempChecksumFile.delete();
762 return correctChecksum || allNotFound;
766 * Used to move the temporary file to its real destination. This is patterned from the way WagonManager handles
767 * its downloaded files.
769 * @param temp The completed download file
770 * @param target The final location of the downloaded file
771 * @throws ProxyException when the temp file cannot replace the target file
773 private void moveTempToTarget( File temp, File target )
774 throws ProxyException
776 if ( target.exists() && !target.delete() )
778 throw new ProxyException( "Unable to overwrite existing target file: " + target.getAbsolutePath() );
781 if ( !temp.renameTo( target ) )
783 getLogger().warn( "Unable to rename tmp file to its final name... resorting to copy command." );
787 FileUtils.copyFile( temp, target );
789 catch ( IOException e )
791 throw new ProxyException( "Cannot copy tmp file to its final location", e );
801 * Used to disconnect the wagonManager from its repository
803 * @param wagon the connected wagonManager object
805 private void disconnectWagon( Wagon wagon )
811 catch ( ConnectionException e )
813 getLogger().error( "Problem disconnecting from wagonManager - ignoring: " + e.getMessage() );
817 private void processRepositoryFailure( ProxiedArtifactRepository repository, Throwable t, String path,
818 ArtifactRepositoryPolicy policy )
819 throws ProxyException
821 repository.addFailure( path, policy );
823 String message = t.getMessage();
824 if ( repository.isHardFail() )
826 repository.addFailure( path, policy );
827 throw new ProxyException(
828 "An error occurred in hardfailing repository " + repository.getName() + "...\n " + message, t );
831 getLogger().warn( "Skipping repository " + repository.getName() + ": " + message );
832 getLogger().debug( "Cause", t );
835 private void processRepositoryFailure( ProxiedArtifactRepository repository, String message, String path,
836 ArtifactRepositoryPolicy policy )
837 throws ProxyException
839 repository.addFailure( path, policy );
841 processCachedRepositoryFailure( repository, message );
844 private void processCachedRepositoryFailure( ProxiedArtifactRepository repository, String message )
845 throws ProxyException
847 if ( repository.isHardFail() )
849 throw new ProxyException(
850 "An error occurred in hardfailing repository " + repository.getName() + "...\n " + message );
853 getLogger().warn( "Skipping repository " + repository.getName() + ": " + message );