]> source.dussan.org Git - archiva.git/blob
c0599799def22313ba2393e1da723c645095abfe
[archiva.git] /
1 package org.apache.maven.repository.proxy;
2
3 /*
4  * Copyright 2005-2006 The Apache Software Foundation.
5  *
6  * Licensed under the Apache License, Version 2.0 (the "License");
7  * you may not use this file except in compliance with the License.
8  * You may obtain a copy of the License at
9  *
10  *     http://www.apache.org/licenses/LICENSE-2.0
11  *
12  * Unless required by applicable law or agreed to in writing, software
13  * distributed under the License is distributed on an "AS IS" BASIS,
14  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15  * See the License for the specific language governing permissions and
16  * limitations under the License.
17  */
18
19 import org.apache.maven.artifact.Artifact;
20 import org.apache.maven.artifact.repository.ArtifactRepository;
21 import org.apache.maven.artifact.repository.ArtifactRepositoryPolicy;
22 import org.apache.maven.artifact.repository.metadata.Metadata;
23 import org.apache.maven.artifact.repository.metadata.io.xpp3.MetadataXpp3Reader;
24 import org.apache.maven.artifact.repository.metadata.io.xpp3.MetadataXpp3Writer;
25 import org.apache.maven.repository.digest.DigestUtils;
26 import org.apache.maven.repository.digest.DigesterException;
27 import org.apache.maven.repository.discovery.ArtifactDiscoverer;
28 import org.apache.maven.repository.discovery.DiscovererException;
29 import org.apache.maven.wagon.ConnectionException;
30 import org.apache.maven.wagon.ResourceDoesNotExistException;
31 import org.apache.maven.wagon.TransferFailedException;
32 import org.apache.maven.wagon.Wagon;
33 import org.apache.maven.wagon.authentication.AuthenticationException;
34 import org.apache.maven.wagon.authorization.AuthorizationException;
35 import org.apache.maven.wagon.observers.ChecksumObserver;
36 import org.apache.maven.wagon.proxy.ProxyInfo;
37 import org.apache.maven.wagon.repository.Repository;
38 import org.codehaus.plexus.logging.AbstractLogEnabled;
39 import org.codehaus.plexus.util.FileUtils;
40 import org.codehaus.plexus.util.IOUtil;
41 import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
42
43 import java.io.File;
44 import java.io.FileReader;
45 import java.io.FileWriter;
46 import java.io.IOException;
47 import java.security.NoSuchAlgorithmException;
48 import java.util.Date;
49 import java.util.Iterator;
50 import java.util.LinkedHashMap;
51 import java.util.List;
52 import java.util.Map;
53
54 /**
55  * An implementation of the proxy handler.
56  *
57  * @author <a href="mailto:brett@apache.org">Brett Porter</a>
58  * @plexus.component
59  * @todo use wagonManager for cache use file:// as URL
60  * @todo this currently duplicates a lot of the wagon manager, and doesn't do things like snapshot resolution, etc.
61  * The checksum handling is inconsistent with that of the wagon manager.
62  * Should we have a more artifact based one? This will merge metadata so should behave correctly, and it is able to
63  * correct some limitations of the wagon manager (eg, it can retrieve newer SNAPSHOT files without metadata)
64  */
65 public class DefaultProxyRequestHandler
66     extends AbstractLogEnabled
67     implements ProxyRequestHandler
68 {
69     /**
70      * @plexus.requirement role-hint="default"
71      * @todo use a map, and have priorities in them
72      */
73     private ArtifactDiscoverer defaultArtifactDiscoverer;
74
75     /**
76      * @plexus.requirement role-hint="legacy"
77      */
78     private ArtifactDiscoverer legacyArtifactDiscoverer;
79
80     /**
81      * @plexus.requirement role="org.apache.maven.wagon.Wagon"
82      */
83     private Map/*<String,Wagon>*/ wagons;
84
85     /**
86      * @plexus.requirement role="org.apache.maven.repository.digest.Digester"
87      */
88     private Map/*<String,Digester>*/ digesters;
89
90     public File get( String path, List proxiedRepositories, ArtifactRepository managedRepository )
91         throws ProxyException, ResourceDoesNotExistException
92     {
93         return get( path, proxiedRepositories, managedRepository, null );
94     }
95
96     public File get( String path, List proxiedRepositories, ArtifactRepository managedRepository, ProxyInfo wagonProxy )
97         throws ProxyException, ResourceDoesNotExistException
98     {
99         return get( managedRepository, path, proxiedRepositories, wagonProxy, false );
100     }
101
102     public File getAlways( String path, List proxiedRepositories, ArtifactRepository managedRepository )
103         throws ProxyException, ResourceDoesNotExistException
104     {
105         return getAlways( path, proxiedRepositories, managedRepository, null );
106     }
107
108     public File getAlways( String path, List proxiedRepositories, ArtifactRepository managedRepository,
109                            ProxyInfo wagonProxy )
110         throws ResourceDoesNotExistException, ProxyException
111     {
112         return get( managedRepository, path, proxiedRepositories, wagonProxy, true );
113     }
114
115     private File get( ArtifactRepository managedRepository, String path, List proxiedRepositories, ProxyInfo wagonProxy,
116                       boolean force )
117         throws ProxyException, ResourceDoesNotExistException
118     {
119         File target = new File( managedRepository.getBasedir(), path );
120
121         for ( Iterator i = proxiedRepositories.iterator(); i.hasNext(); )
122         {
123             ProxiedArtifactRepository repository = (ProxiedArtifactRepository) i.next();
124
125             if ( !force && repository.isCachedFailure( path ) )
126             {
127                 processCachedRepositoryFailure( repository, "Cached failure found for: " + path );
128             }
129             else
130             {
131                 get( path, target, repository, managedRepository, wagonProxy, force );
132             }
133         }
134
135         if ( !target.exists() )
136         {
137             throw new ResourceDoesNotExistException( "Could not find " + path + " in any of the repositories." );
138         }
139
140         return target;
141     }
142
143     private void get( String path, File target, ProxiedArtifactRepository repository,
144                       ArtifactRepository managedRepository, ProxyInfo wagonProxy, boolean force )
145         throws ProxyException
146     {
147         ArtifactRepositoryPolicy policy;
148
149         if ( path.endsWith( ".md5" ) || path.endsWith( ".sha1" ) )
150         {
151             // always read from the managed repository, no need to make remote request
152         }
153         else if ( path.endsWith( "maven-metadata.xml" ) )
154         {
155             File metadataFile = new File( target.getParentFile(), ".metadata-" + repository.getRepository().getId() );
156
157             policy = repository.getRepository().getReleases();
158
159             // if it is snapshot metadata, use a different policy
160             if ( path.endsWith( "-SNAPSHOT/maven-metadata.xml" ) )
161             {
162                 policy = repository.getRepository().getSnapshots();
163             }
164
165             if ( force || !metadataFile.exists() || isOutOfDate( policy, metadataFile ) )
166             {
167                 getFileFromRepository( path, repository, managedRepository.getBasedir(), wagonProxy, metadataFile,
168                                        policy, force );
169
170                 mergeMetadataFiles( target, metadataFile );
171             }
172         }
173         else
174         {
175             Artifact artifact = null;
176             try
177             {
178                 artifact = defaultArtifactDiscoverer.buildArtifact( path );
179             }
180             catch ( DiscovererException e )
181             {
182                 getLogger().debug( "Failed to build artifact using default layout with message: " + e.getMessage() );
183             }
184
185             if ( artifact == null )
186             {
187                 try
188                 {
189                     artifact = legacyArtifactDiscoverer.buildArtifact( path );
190                 }
191                 catch ( DiscovererException e )
192                 {
193                     getLogger().debug( "Failed to build artifact using legacy layout with message: " + e.getMessage() );
194                 }
195             }
196
197             if ( artifact != null )
198             {
199                 ArtifactRepository artifactRepository = repository.getRepository();
200
201                 // we use the release policy for tracking failures, but only check for updates on snapshots
202                 // also, we don't look for updates on timestamp snapshot files, only non-unique-version ones
203                 policy = artifact.isSnapshot() ? artifactRepository.getSnapshots() : artifactRepository.getReleases();
204
205                 boolean needsUpdate = false;
206                 if ( artifact.getVersion().endsWith( "-SNAPSHOT" ) && isOutOfDate( policy, target ) )
207                 {
208                     needsUpdate = true;
209                 }
210
211                 if ( needsUpdate || force || !target.exists() )
212                 {
213                     getFileFromRepository( artifactRepository.pathOf( artifact ), repository,
214                                            managedRepository.getBasedir(), wagonProxy, target, policy, force );
215                 }
216             }
217             else
218             {
219                 // Some other unknown file in the repository, proxy as is
220                 if ( force || !target.exists() )
221                 {
222                     policy = repository.getRepository().getReleases();
223                     getFileFromRepository( path, repository, managedRepository.getBasedir(), wagonProxy, target, policy,
224                                            force );
225                 }
226             }
227         }
228
229         if ( target.exists() )
230         {
231             // in case it previously failed and we've since found it
232             repository.clearFailure( path );
233         }
234     }
235
236     private void mergeMetadataFiles( File target, File metadataFile )
237         throws ProxyException
238     {
239         if ( target.exists() )
240         {
241             MetadataXpp3Reader reader = new MetadataXpp3Reader();
242             Metadata metadata;
243             FileReader fileReader = null;
244             try
245             {
246                 fileReader = new FileReader( target );
247                 metadata = reader.read( fileReader );
248             }
249             catch ( XmlPullParserException e )
250             {
251                 throw new ProxyException( "Unable to parse existing metadata: " + e.getMessage(), e );
252             }
253             catch ( IOException e )
254             {
255                 throw new ProxyException( "Unable to read existing metadata: " + e.getMessage(), e );
256             }
257             finally
258             {
259                 IOUtil.close( fileReader );
260             }
261
262             fileReader = null;
263             boolean changed = false;
264             try
265             {
266                 fileReader = new FileReader( metadataFile );
267                 Metadata newMetadata = reader.read( fileReader );
268
269                 changed = metadata.merge( newMetadata );
270             }
271             catch ( IOException e )
272             {
273                 // ignore the merged file
274                 getLogger().warn( "Unable to read new metadata: " + e.getMessage() );
275             }
276             catch ( XmlPullParserException e )
277             {
278                 // ignore the merged file
279                 getLogger().warn( "Unable to parse new metadata: " + e.getMessage() );
280             }
281             finally
282             {
283                 IOUtil.close( fileReader );
284             }
285
286             if ( changed )
287             {
288                 FileWriter fileWriter = null;
289                 try
290                 {
291                     fileWriter = new FileWriter( target );
292                     new MetadataXpp3Writer().write( fileWriter, metadata );
293                 }
294                 catch ( IOException e )
295                 {
296                     getLogger().warn( "Unable to store new metadata: " + e.getMessage() );
297                 }
298                 finally
299                 {
300                     IOUtil.close( fileWriter );
301                 }
302             }
303         }
304         else
305         {
306             try
307             {
308                 FileUtils.copyFile( metadataFile, target );
309             }
310             catch ( IOException e )
311             {
312                 // warn, but ignore
313                 getLogger().warn( "Unable to copy metadata: " + metadataFile + " to " + target );
314             }
315         }
316     }
317
318     private void getFileFromRepository( String path, ProxiedArtifactRepository repository, String repositoryCachePath,
319                                         ProxyInfo httpProxy, File target, ArtifactRepositoryPolicy policy,
320                                         boolean force )
321         throws ProxyException
322     {
323         if ( !policy.isEnabled() )
324         {
325             getLogger().debug( "Skipping disabled repository " + repository.getName() );
326             return;
327         }
328
329         Map checksums = null;
330         Wagon wagon = null;
331
332         File temp = new File( target.getAbsolutePath() + ".tmp" );
333         temp.deleteOnExit();
334
335         boolean connected = false;
336         try
337         {
338             String protocol = repository.getRepository().getProtocol();
339             wagon = (Wagon) wagons.get( protocol );
340             if ( wagon == null )
341             {
342                 throw new ProxyException( "Unsupported remote protocol: " + protocol );
343             }
344
345             //@todo configure wagon (ssh settings, etc)
346
347             checksums = prepareChecksumListeners( wagon );
348
349             connected = connectToRepository( wagon, repository, httpProxy );
350             if ( connected )
351             {
352                 int tries = 0;
353                 boolean success;
354
355                 do
356                 {
357                     tries++;
358
359                     getLogger().debug( "Trying " + path + " from " + repository.getName() + "..." );
360
361                     boolean downloaded = true;
362                     if ( force || !target.exists() )
363                     {
364                         wagon.get( path, temp );
365                     }
366                     else
367                     {
368                         downloaded = wagon.getIfNewer( path, temp, target.lastModified() );
369                     }
370
371                     if ( downloaded )
372                     {
373                         success = checkChecksum( checksums, path, wagon, repositoryCachePath );
374
375                         if ( tries > 1 && !success )
376                         {
377                             processRepositoryFailure( repository,
378                                                       "Checksum failures occurred while downloading " + path, path,
379                                                       policy );
380                             return;
381                         }
382                     }
383                     else
384                     {
385                         // getIfNewer determined we were up to date
386                         success = true;
387                     }
388                 }
389                 while ( !success );
390
391                 // temp won't exist if we called getIfNewer and it was older, but its still a successful return
392                 if ( temp.exists() )
393                 {
394                     moveTempToTarget( temp, target );
395                 }
396             }
397             //try next repository
398         }
399         catch ( TransferFailedException e )
400         {
401             processRepositoryFailure( repository, e, path, policy );
402         }
403         catch ( AuthorizationException e )
404         {
405             processRepositoryFailure( repository, e, path, policy );
406         }
407         catch ( ResourceDoesNotExistException e )
408         {
409             // hard failure setting doesn't affect "not found".
410             getLogger().debug( "Artifact not found in repository: " + repository.getName() + ": " + e.getMessage() );
411         }
412         finally
413         {
414             temp.delete();
415
416             if ( wagon != null && checksums != null )
417             {
418                 releaseChecksumListeners( wagon, checksums );
419             }
420
421             if ( connected )
422             {
423                 disconnectWagon( wagon );
424             }
425         }
426     }
427
428     private static boolean isOutOfDate( ArtifactRepositoryPolicy policy, File target )
429     {
430         return policy != null && policy.checkOutOfDate( new Date( target.lastModified() ) );
431     }
432
433     /**
434      * Used to add checksum observers as transfer listeners to the wagonManager object
435      *
436      * @param wagon the wagonManager object to use the checksum with
437      * @return map of ChecksumObservers added into the wagonManager transfer listeners
438      */
439     private Map prepareChecksumListeners( Wagon wagon )
440     {
441         Map checksums = new LinkedHashMap();
442         try
443         {
444             ChecksumObserver checksum = new ChecksumObserver( "SHA-1" );
445             wagon.addTransferListener( checksum );
446             checksums.put( "sha1", checksum );
447
448             checksum = new ChecksumObserver( "MD5" );
449             wagon.addTransferListener( checksum );
450             checksums.put( "md5", checksum );
451         }
452         catch ( NoSuchAlgorithmException e )
453         {
454             getLogger().info( "An error occurred while preparing checksum observers", e );
455         }
456         return checksums;
457     }
458
459     private void releaseChecksumListeners( Wagon wagon, Map checksumMap )
460     {
461         for ( Iterator checksums = checksumMap.values().iterator(); checksums.hasNext(); )
462         {
463             ChecksumObserver listener = (ChecksumObserver) checksums.next();
464             wagon.removeTransferListener( listener );
465         }
466     }
467
468     private boolean connectToRepository( Wagon wagon, ProxiedArtifactRepository repository, ProxyInfo httpProxy )
469     {
470         boolean connected = false;
471         try
472         {
473             ArtifactRepository artifactRepository = repository.getRepository();
474             Repository wagonRepository = new Repository( artifactRepository.getId(), artifactRepository.getUrl() );
475             if ( repository.isUseNetworkProxy() && httpProxy != null )
476             {
477                 wagon.connect( wagonRepository, httpProxy );
478             }
479             else
480             {
481                 wagon.connect( wagonRepository );
482             }
483             connected = true;
484         }
485         catch ( ConnectionException e )
486         {
487             getLogger().info( "Could not connect to " + repository.getName() + ": " + e.getMessage() );
488         }
489         catch ( AuthenticationException e )
490         {
491             getLogger().info( "Could not connect to " + repository.getName() + ": " + e.getMessage() );
492         }
493
494         return connected;
495     }
496
497     private boolean checkChecksum( Map checksumMap, String path, Wagon wagon, String repositoryCachePath )
498         throws ProxyException
499     {
500         releaseChecksumListeners( wagon, checksumMap );
501
502         boolean correctChecksum = false;
503
504         boolean allNotFound = true;
505
506         for ( Iterator i = checksumMap.keySet().iterator(); i.hasNext() && !correctChecksum; )
507         {
508             String checksumExt = (String) i.next();
509             ChecksumObserver checksum = (ChecksumObserver) checksumMap.get( checksumExt );
510             String checksumPath = path + "." + checksumExt;
511             File checksumFile = new File( repositoryCachePath, checksumPath );
512
513             File tempChecksumFile = new File( checksumFile.getAbsolutePath() + ".tmp" );
514             tempChecksumFile.deleteOnExit();
515
516             try
517             {
518                 wagon.get( checksumPath, tempChecksumFile );
519
520                 allNotFound = false;
521
522                 String remoteChecksum = DigestUtils.cleanChecksum( FileUtils.fileRead( tempChecksumFile ),
523                                                                    checksumExt.toUpperCase(),
524                                                                    path.substring( path.lastIndexOf( '/' ) ) );
525
526                 String actualChecksum = checksum.getActualChecksum().toUpperCase();
527                 remoteChecksum = remoteChecksum.toUpperCase();
528
529                 if ( remoteChecksum.equals( actualChecksum ) )
530                 {
531                     moveTempToTarget( tempChecksumFile, checksumFile );
532
533                     correctChecksum = true;
534                 }
535                 else
536                 {
537                     getLogger().warn(
538                         "The checksum '" + actualChecksum + "' did not match the remote value: " + remoteChecksum );
539                 }
540             }
541             catch ( TransferFailedException e )
542             {
543                 getLogger().warn( "An error occurred during the download of " + checksumPath + ": " + e.getMessage(),
544                                   e );
545                 // do nothing try the next checksum
546
547                 allNotFound = false;
548             }
549             catch ( ResourceDoesNotExistException e )
550             {
551                 getLogger().debug( "The checksum did not exist: " + checksumPath, e );
552                 // do nothing try the next checksum
553                 // remove it if it is present locally in case there is an old incorrect one
554                 if ( checksumFile.exists() )
555                 {
556                     checksumFile.delete();
557                 }
558             }
559             catch ( AuthorizationException e )
560             {
561                 getLogger().warn( "An error occurred during the download of " + checksumPath + ": " + e.getMessage(),
562                                   e );
563                 // do nothing try the next checksum
564
565                 allNotFound = false;
566             }
567             catch ( IOException e )
568             {
569                 getLogger().warn( "An error occurred while reading the temporary checksum file.", e );
570                 // do nothing try the next checksum
571
572                 allNotFound = false;
573             }
574             catch ( DigesterException e )
575             {
576                 getLogger().warn( "The checksum was invalid: " + checksumPath + ": " + e.getMessage(), e );
577                 // do nothing try the next checksum
578
579                 allNotFound = false;
580             }
581             finally
582             {
583                 tempChecksumFile.delete();
584             }
585         }
586         return correctChecksum || allNotFound;
587     }
588
589     /**
590      * Used to move the temporary file to its real destination.  This is patterned from the way WagonManager handles
591      * its downloaded files.
592      *
593      * @param temp   The completed download file
594      * @param target The final location of the downloaded file
595      * @throws ProxyException when the temp file cannot replace the target file
596      */
597     private void moveTempToTarget( File temp, File target )
598         throws ProxyException
599     {
600         if ( target.exists() && !target.delete() )
601         {
602             throw new ProxyException( "Unable to overwrite existing target file: " + target.getAbsolutePath() );
603         }
604
605         if ( !temp.renameTo( target ) )
606         {
607             getLogger().warn( "Unable to rename tmp file to its final name... resorting to copy command." );
608
609             try
610             {
611                 FileUtils.copyFile( temp, target );
612             }
613             catch ( IOException e )
614             {
615                 throw new ProxyException( "Cannot copy tmp file to its final location", e );
616             }
617             finally
618             {
619                 temp.delete();
620             }
621         }
622     }
623
624     /**
625      * Used to disconnect the wagonManager from its repository
626      *
627      * @param wagon the connected wagonManager object
628      */
629     private void disconnectWagon( Wagon wagon )
630     {
631         try
632         {
633             wagon.disconnect();
634         }
635         catch ( ConnectionException e )
636         {
637             getLogger().error( "Problem disconnecting from wagonManager - ignoring: " + e.getMessage() );
638         }
639     }
640
641     private void processRepositoryFailure( ProxiedArtifactRepository repository, Throwable t, String path,
642                                            ArtifactRepositoryPolicy policy )
643         throws ProxyException
644     {
645         repository.addFailure( path, policy );
646
647         String message = t.getMessage();
648         if ( repository.isHardFail() )
649         {
650             repository.addFailure( path, policy );
651             throw new ProxyException(
652                 "An error occurred in hardfailing repository " + repository.getName() + "...\n    " + message, t );
653         }
654
655         getLogger().warn( "Skipping repository " + repository.getName() + ": " + message );
656         getLogger().debug( "Cause", t );
657     }
658
659     private void processRepositoryFailure( ProxiedArtifactRepository repository, String message, String path,
660                                            ArtifactRepositoryPolicy policy )
661         throws ProxyException
662     {
663         repository.addFailure( path, policy );
664
665         processCachedRepositoryFailure( repository, message );
666     }
667
668     private void processCachedRepositoryFailure( ProxiedArtifactRepository repository, String message )
669         throws ProxyException
670     {
671         if ( repository.isHardFail() )
672         {
673             throw new ProxyException(
674                 "An error occurred in hardfailing repository " + repository.getName() + "...\n    " + message );
675         }
676
677         getLogger().warn( "Skipping repository " + repository.getName() + ": " + message );
678     }
679 }