]> source.dussan.org Git - archiva.git/blob
fa7799e30483eb4a14de0abb9e644d55ea67ccf5
[archiva.git] /
1 package org.apache.maven.archiva.proxy;
2
3 /*
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
11  *
12  *  http://www.apache.org/licenses/LICENSE-2.0
13  *
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
19  * under the License.
20  */
21
22 import java.io.File;
23 import java.io.IOException;
24 import java.util.ArrayList;
25 import java.util.Collections;
26 import java.util.HashMap;
27 import java.util.LinkedHashMap;
28 import java.util.List;
29 import java.util.Map;
30 import java.util.Properties;
31 import java.util.Map.Entry;
32
33 import org.apache.commons.collections.CollectionUtils;
34 import org.apache.commons.io.FileUtils;
35 import org.apache.commons.lang.StringUtils;
36 import org.apache.maven.archiva.configuration.ArchivaConfiguration;
37 import org.apache.maven.archiva.configuration.ConfigurationNames;
38 import org.apache.maven.archiva.configuration.NetworkProxyConfiguration;
39 import org.apache.maven.archiva.configuration.ProxyConnectorConfiguration;
40 import org.apache.maven.archiva.model.ArtifactReference;
41 import org.apache.maven.archiva.model.Keys;
42 import org.apache.maven.archiva.model.RepositoryURL;
43 import org.apache.maven.archiva.policies.DownloadErrorPolicy;
44 import org.apache.maven.archiva.policies.DownloadPolicy;
45 import org.apache.maven.archiva.policies.PolicyConfigurationException;
46 import org.apache.maven.archiva.policies.PolicyViolationException;
47 import org.apache.maven.archiva.policies.PostDownloadPolicy;
48 import org.apache.maven.archiva.policies.PreDownloadPolicy;
49 import org.apache.maven.archiva.policies.ProxyDownloadException;
50 import org.apache.maven.archiva.policies.urlcache.UrlFailureCache;
51 import org.apache.maven.archiva.repository.ManagedRepositoryContent;
52 import org.apache.maven.archiva.repository.RemoteRepositoryContent;
53 import org.apache.maven.archiva.repository.RepositoryContentFactory;
54 import org.apache.maven.archiva.repository.RepositoryException;
55 import org.apache.maven.archiva.repository.RepositoryNotFoundException;
56 import org.apache.maven.archiva.repository.metadata.MetadataTools;
57 import org.apache.maven.archiva.repository.metadata.RepositoryMetadataException;
58 import org.apache.maven.archiva.repository.scanner.RepositoryContentConsumers;
59 import org.apache.maven.wagon.ConnectionException;
60 import org.apache.maven.wagon.ResourceDoesNotExistException;
61 import org.apache.maven.wagon.Wagon;
62 import org.apache.maven.wagon.WagonException;
63 import org.apache.maven.wagon.authentication.AuthenticationException;
64 import org.apache.maven.wagon.authentication.AuthenticationInfo;
65 import org.apache.maven.wagon.proxy.ProxyInfo;
66 import org.apache.maven.wagon.repository.Repository;
67 import org.codehaus.plexus.personality.plexus.lifecycle.phase.Initializable;
68 import org.codehaus.plexus.personality.plexus.lifecycle.phase.InitializationException;
69 import org.codehaus.plexus.registry.Registry;
70 import org.codehaus.plexus.registry.RegistryListener;
71 import org.codehaus.plexus.util.SelectorUtils;
72 import org.slf4j.Logger;
73 import org.slf4j.LoggerFactory;
74
75 /**
76  * DefaultRepositoryProxyConnectors
77  * 
78  * @version $Id$
79  * @todo exception handling needs work - "not modified" is not really an exceptional case, and it has more layers than
80  *       your average brown onion
81  * @plexus.component role-hint="default"
82  */
83 public class DefaultRepositoryProxyConnectors
84     implements RepositoryProxyConnectors, RegistryListener, Initializable
85 {
86     private Logger log = LoggerFactory.getLogger( DefaultRepositoryProxyConnectors.class );
87
88     /**
89      * @plexus.requirement
90      */
91     private ArchivaConfiguration archivaConfiguration;
92
93     /**
94      * @plexus.requirement
95      */
96     private RepositoryContentFactory repositoryFactory;
97
98     /**
99      * @plexus.requirement
100      */
101     private MetadataTools metadataTools;
102
103     /**
104      * @plexus.requirement role="org.apache.maven.archiva.policies.PreDownloadPolicy"
105      */
106     private Map<String, PreDownloadPolicy> preDownloadPolicies;
107
108     /**
109      * @plexus.requirement role="org.apache.maven.archiva.policies.PostDownloadPolicy"
110      */
111     private Map<String, PostDownloadPolicy> postDownloadPolicies;
112
113     /**
114      * @plexus.requirement role="org.apache.maven.archiva.policies.DownloadErrorPolicy"
115      */
116     private Map<String, DownloadErrorPolicy> downloadErrorPolicies;
117
118     /**
119      * @plexus.requirement role-hint="default"
120      */
121     private UrlFailureCache urlFailureCache;
122
123     private Map<String, List<ProxyConnector>> proxyConnectorMap = new HashMap<String, List<ProxyConnector>>();
124
125     private Map<String, ProxyInfo> networkProxyMap = new HashMap<String, ProxyInfo>();
126
127     /**
128      * @plexus.requirement
129      */
130     private RepositoryContentConsumers consumers;
131
132     /**
133      * @plexus.requirement
134      */
135     private WagonFactory wagonFactory;
136
137     public File fetchFromProxies( ManagedRepositoryContent repository, ArtifactReference artifact )
138         throws ProxyDownloadException
139     {
140         File localFile = toLocalFile( repository, artifact );
141
142         Properties requestProperties = new Properties();
143         requestProperties.setProperty( "filetype", "artifact" );
144         requestProperties.setProperty( "version", artifact.getVersion() );
145         requestProperties.setProperty( "managedRepositoryId", repository.getId() );
146
147         List<ProxyConnector> connectors = getProxyConnectors( repository );
148         Map<String, Exception> previousExceptions = new LinkedHashMap<String, Exception>();
149         for ( ProxyConnector connector : connectors )
150         {
151             if ( connector.isDisabled() )
152             {
153                 continue;
154             }
155
156             RemoteRepositoryContent targetRepository = connector.getTargetRepository();
157             requestProperties.setProperty( "remoteRepositoryId", targetRepository.getId() );
158
159             String targetPath = targetRepository.toPath( artifact );
160
161             try
162             {
163                 File downloadedFile =
164                     transferFile( connector, targetRepository, targetPath, repository, localFile, requestProperties,
165                                   true );
166
167                 if ( fileExists( downloadedFile ) )
168                 {
169                     log.debug( "Successfully transferred: " + downloadedFile.getAbsolutePath() );
170                     return downloadedFile;
171                 }
172             }
173             catch ( NotFoundException e )
174             {
175                 log.debug( "Artifact " + Keys.toKey( artifact ) + " not found on repository \""
176                     + targetRepository.getRepository().getId() + "\"." );
177             }
178             catch ( NotModifiedException e )
179             {
180                 log.debug( "Artifact " + Keys.toKey( artifact ) + " not updated on repository \""
181                     + targetRepository.getRepository().getId() + "\"." );
182             }
183             catch ( ProxyException e )
184             {
185                 validatePolicies( this.downloadErrorPolicies, connector.getPolicies(), requestProperties, artifact,
186                                   targetRepository, localFile, e, previousExceptions );
187             }
188         }
189
190         if ( !previousExceptions.isEmpty() )
191         {
192             throw new ProxyDownloadException( "Failures occurred downloading from some remote repositories",
193                                               previousExceptions );
194         }
195
196         log.debug( "Exhausted all target repositories, artifact " + Keys.toKey( artifact ) + " not found." );
197
198         return null;
199     }
200
201     public File fetchFromProxies( ManagedRepositoryContent repository, String path )
202     {
203         File localFile = new File( repository.getRepoRoot(), path );
204
205         // no update policies for these paths
206         if ( localFile.exists() )
207         {
208             return null;
209         }
210
211         Properties requestProperties = new Properties();
212         requestProperties.setProperty( "filetype", "resource" );
213         requestProperties.setProperty( "managedRepositoryId", repository.getId() );
214
215         List<ProxyConnector> connectors = getProxyConnectors( repository );
216         for ( ProxyConnector connector : connectors )
217         {
218             if ( connector.isDisabled() )
219             {
220                 continue;
221             }
222
223             RemoteRepositoryContent targetRepository = connector.getTargetRepository();
224             requestProperties.setProperty( "remoteRepositoryId", targetRepository.getId() );
225
226             String targetPath = path;
227
228             try
229             {
230                 File downloadedFile =
231                     transferFile( connector, targetRepository, targetPath, repository, localFile, requestProperties,
232                                   false );
233
234                 if ( fileExists( downloadedFile ) )
235                 {
236                     log.debug( "Successfully transferred: " + downloadedFile.getAbsolutePath() );
237                     return downloadedFile;
238                 }
239             }
240             catch ( NotFoundException e )
241             {
242                 log.debug( "Resource " + path + " not found on repository \""
243                     + targetRepository.getRepository().getId() + "\"." );
244             }
245             catch ( NotModifiedException e )
246             {
247                 log.debug( "Resource " + path + " not updated on repository \""
248                     + targetRepository.getRepository().getId() + "\"." );
249             }
250             catch ( ProxyException e )
251             {
252                 log.warn( "Transfer error from repository \"" + targetRepository.getRepository().getId()
253                     + "\" for resource " + path + ", continuing to next repository. Error message: " + e.getMessage() );
254                 log.debug( "Full stack trace", e );
255             }
256         }
257
258         log.debug( "Exhausted all target repositories, resource " + path + " not found." );
259
260         return null;
261     }
262
263     public File fetchMetatadaFromProxies( ManagedRepositoryContent repository, String logicalPath )
264     {
265         File localFile = new File( repository.getRepoRoot(), logicalPath );
266
267         Properties requestProperties = new Properties();
268         requestProperties.setProperty( "filetype", "metadata" );
269         boolean metadataNeedsUpdating = false;
270         long originalTimestamp = getLastModified( localFile );
271
272         List<ProxyConnector> connectors = getProxyConnectors( repository );
273         for ( ProxyConnector connector : connectors )
274         {
275             if ( connector.isDisabled() )
276             {
277                 continue;
278             }
279
280             RemoteRepositoryContent targetRepository = connector.getTargetRepository();
281
282             File localRepoFile = toLocalRepoFile( repository, targetRepository, logicalPath );
283             long originalMetadataTimestamp = getLastModified( localRepoFile );
284
285             try
286             {
287                 transferFile( connector, targetRepository, logicalPath, repository, localRepoFile, requestProperties,
288                               true );
289
290                 if ( hasBeenUpdated( localRepoFile, originalMetadataTimestamp ) )
291                 {
292                     metadataNeedsUpdating = true;
293                 }
294             }
295             catch ( NotFoundException e )
296             {
297                 log.debug( "Metadata " + logicalPath + " not found on remote repository \""
298                     + targetRepository.getRepository().getId() + "\".", e );
299             }
300             catch ( NotModifiedException e )
301             {
302                 log.debug( "Metadata " + logicalPath + " not updated on remote repository \""
303                     + targetRepository.getRepository().getId() + "\".", e );
304             }
305             catch ( ProxyException e )
306             {
307                 log.warn( "Transfer error from repository \"" + targetRepository.getRepository().getId()
308                     + "\" for versioned Metadata " + logicalPath + ", continuing to next repository. Error message: "
309                     + e.getMessage() );
310                 log.debug( "Full stack trace", e );
311             }
312         }
313
314         if ( hasBeenUpdated( localFile, originalTimestamp ) )
315         {
316             metadataNeedsUpdating = true;
317         }
318
319         if ( metadataNeedsUpdating || !localFile.exists() )
320         {
321             try
322             {
323                 metadataTools.updateMetadata( repository, logicalPath );
324             }
325             catch ( RepositoryMetadataException e )
326             {
327                 log.warn( "Unable to update metadata " + localFile.getAbsolutePath() + ": " + e.getMessage(), e );
328             }
329         }
330
331         if ( fileExists( localFile ) )
332         {
333             return localFile;
334         }
335
336         return null;
337     }
338
339     private long getLastModified( File file )
340     {
341         if ( !file.exists() || !file.isFile() )
342         {
343             return 0;
344         }
345
346         return file.lastModified();
347     }
348
349     private boolean hasBeenUpdated( File file, long originalLastModified )
350     {
351         if ( !file.exists() || !file.isFile() )
352         {
353             return false;
354         }
355
356         long currentLastModified = getLastModified( file );
357         return ( currentLastModified > originalLastModified );
358     }
359
360     private File toLocalRepoFile( ManagedRepositoryContent repository, RemoteRepositoryContent targetRepository,
361                                   String targetPath )
362     {
363         String repoPath = metadataTools.getRepositorySpecificName( targetRepository, targetPath );
364         return new File( repository.getRepoRoot(), repoPath );
365     }
366
367     /**
368      * Test if the provided ManagedRepositoryContent has any proxies configured for it.
369      */
370     public boolean hasProxies( ManagedRepositoryContent repository )
371     {
372         synchronized ( this.proxyConnectorMap )
373         {
374             return this.proxyConnectorMap.containsKey( repository.getId() );
375         }
376     }
377
378     private File toLocalFile( ManagedRepositoryContent repository, ArtifactReference artifact )
379     {
380         return repository.toFile( artifact );
381     }
382
383     /**
384      * Simple method to test if the file exists on the local disk.
385      * 
386      * @param file the file to test. (may be null)
387      * @return true if file exists. false if the file param is null, doesn't exist, or is not of type File.
388      */
389     private boolean fileExists( File file )
390     {
391         if ( file == null )
392         {
393             return false;
394         }
395
396         if ( !file.exists() )
397         {
398             return false;
399         }
400
401         if ( !file.isFile() )
402         {
403             return false;
404         }
405
406         return true;
407     }
408
409     /**
410      * Perform the transfer of the file.
411      * 
412      * @param connector the connector configuration to use.
413      * @param remoteRepository the remote repository get the resource from.
414      * @param remotePath the path in the remote repository to the resource to get.
415      * @param repository the managed repository that will hold the file
416      * @param resource the local file to place the downloaded resource into
417      * @param requestProperties the request properties to utilize for policy handling.
418      * @param executeConsumers whether to execute the consumers after proxying
419      * @return the local file that was downloaded, or null if not downloaded.
420      * @throws NotFoundException if the file was not found on the remote repository.
421      * @throws NotModifiedException if the localFile was present, and the resource was present on remote repository, but
422      *             the remote resource is not newer than the local File.
423      * @throws ProxyException if transfer was unsuccessful.
424      */
425     private File transferFile( ProxyConnector connector, RemoteRepositoryContent remoteRepository, String remotePath,
426                                ManagedRepositoryContent repository, File resource, Properties requestProperties,
427                                boolean executeConsumers )
428         throws ProxyException, NotModifiedException
429     {
430         String url = remoteRepository.getURL().getUrl();
431         if ( !url.endsWith( "/" ) )
432         {
433             url = url + "/";
434         }
435         url = url + remotePath;
436         requestProperties.setProperty( "url", url );
437
438         // Is a whitelist defined?
439         if ( CollectionUtils.isNotEmpty( connector.getWhitelist() ) )
440         {
441             // Path must belong to whitelist.
442             if ( !matchesPattern( remotePath, connector.getWhitelist() ) )
443             {
444                 log.debug( "Path [" + remotePath
445                     + "] is not part of defined whitelist (skipping transfer from repository ["
446                     + remoteRepository.getRepository().getName() + "])." );
447                 return null;
448             }
449         }
450
451         // Is target path part of blacklist?
452         if ( matchesPattern( remotePath, connector.getBlacklist() ) )
453         {
454             log.debug( "Path [" + remotePath + "] is part of blacklist (skipping transfer from repository ["
455                 + remoteRepository.getRepository().getName() + "])." );
456             return null;
457         }
458
459         // Handle pre-download policy
460         try
461         {
462             validatePolicies( this.preDownloadPolicies, connector.getPolicies(), requestProperties, resource );
463         }
464         catch ( PolicyViolationException e )
465         {
466             String emsg = "Transfer not attempted on " + url + " : " + e.getMessage();
467             if ( fileExists( resource ) )
468             {
469                 log.info( emsg + ": using already present local file." );
470                 return resource;
471             }
472
473             log.info( emsg );
474             return null;
475         }
476
477         File tmpMd5 = null;
478         File tmpSha1 = null;
479         File tmpResource = null;
480
481         File workingDirectory = createWorkingDirectory( repository );
482         try
483         {
484             Wagon wagon = null;
485             try
486             {
487                 RepositoryURL repoUrl = remoteRepository.getURL();
488                 String protocol = repoUrl.getProtocol();
489                 wagon = (Wagon) wagonFactory.getWagon( "wagon#" + protocol );
490                 if ( wagon == null )
491                 {
492                     throw new ProxyException( "Unsupported target repository protocol: " + protocol );
493                 }
494
495                 boolean connected = connectToRepository( connector, wagon, remoteRepository );
496                 if ( connected )
497                 {
498                     tmpResource = new File( workingDirectory, resource.getName() );
499                     transferSimpleFile( wagon, remoteRepository, remotePath, repository, resource, tmpResource );
500
501                     // TODO: these should be used to validate the download based on the policies, not always downloaded
502                     // to
503                     // save on connections since md5 is rarely used
504                     tmpSha1 =
505                         transferChecksum( wagon, remoteRepository, remotePath, repository, resource, workingDirectory,
506                                           ".sha1" );
507                     tmpMd5 =
508                         transferChecksum( wagon, remoteRepository, remotePath, repository, resource, workingDirectory,
509                                           ".md5" );
510                 }
511             }
512             catch ( NotFoundException e )
513             {
514                 urlFailureCache.cacheFailure( url );
515                 throw e;
516             }
517             catch ( NotModifiedException e )
518             {
519                 // Do not cache url here.
520                 throw e;
521             }
522             catch ( ProxyException e )
523             {
524                 urlFailureCache.cacheFailure( url );
525                 throw e;
526             }
527             finally
528             {
529                 if ( wagon != null )
530                 {
531                     try
532                     {
533                         wagon.disconnect();
534                     }
535                     catch ( ConnectionException e )
536                     {
537                         log.warn( "Unable to disconnect wagon.", e );
538                     }
539                 }
540             }
541
542             // Handle post-download policies.
543             try
544             {
545                 validatePolicies( this.postDownloadPolicies, connector.getPolicies(), requestProperties, tmpResource );
546             }
547             catch ( PolicyViolationException e )
548             {
549                 log.info( "Transfer invalidated from " + url + " : " + e.getMessage() );
550                 executeConsumers = false;
551                 if ( !fileExists( tmpResource ) )
552                 {
553                     resource = null;
554                 }
555             }
556
557             if ( resource != null )
558             {
559                 synchronized ( resource.getAbsolutePath().intern() )
560                 {
561                     File directory = resource.getParentFile();
562                     moveFileIfExists( tmpMd5, directory );
563                     moveFileIfExists( tmpSha1, directory );
564                     moveFileIfExists( tmpResource, directory );
565                 }
566             }
567         }
568         finally
569         {
570             FileUtils.deleteQuietly( workingDirectory );
571         }
572
573         if ( executeConsumers )
574         {
575             // Just-in-time update of the index and database by executing the consumers for this artifact
576             consumers.executeConsumers( connector.getSourceRepository().getRepository(), resource );
577         }
578
579         return resource;
580     }
581
582     /**
583      * Moves the file into repository location if it exists
584      * 
585      * @param fileToMove this could be either the main artifact, sha1 or md5 checksum file.
586      * @param directory directory to write files to
587      */
588     private void moveFileIfExists( File fileToMove, File directory )
589         throws ProxyException
590     {
591         if ( fileToMove != null && fileToMove.exists() )
592         {
593             File newLocation = new File( directory, fileToMove.getName() );
594             moveTempToTarget( fileToMove, newLocation );
595         }
596     }
597
598     /**
599      * <p>
600      * Quietly transfer the checksum file from the remote repository to the local file.
601      * </p>
602      * 
603      * @param wagon the wagon instance (should already be connected) to use.
604      * @param remoteRepository the remote repository to transfer from.
605      * @param remotePath the remote path to the resource to get.
606      * @param repository the managed repository that will hold the file
607      * @param localFile the local file that should contain the downloaded contents
608      * @param type the type of checksum to transfer (example: ".md5" or ".sha1")
609      * @throws ProxyException if copying the downloaded file into place did not succeed.
610      */
611     private File transferChecksum( Wagon wagon, RemoteRepositoryContent remoteRepository, String remotePath,
612                                    ManagedRepositoryContent repository, File resource, File tmpDirectory, String ext )
613         throws ProxyException
614     {
615         String url = remoteRepository.getURL().getUrl() + remotePath + ext;
616
617         // Transfer checksum does not use the policy.
618         if ( urlFailureCache.hasFailedBefore( url ) )
619         {
620             return null;
621         }
622
623         File destFile = new File( tmpDirectory, resource.getName() + ext );
624
625         try
626         {
627             transferSimpleFile( wagon, remoteRepository, remotePath + ext, repository, resource, destFile );
628             log.debug( "Checksum " + url + " Downloaded: " + destFile + " to move to " + resource );
629         }
630         catch ( NotFoundException e )
631         {
632             urlFailureCache.cacheFailure( url );
633             log.debug( "Transfer failed, checksum not found: " + url );
634             // Consume it, do not pass this on.
635         }
636         catch ( NotModifiedException e )
637         {
638             log.debug( "Transfer skipped, checksum not modified: " + url );
639             // Consume it, do not pass this on.
640         }
641         catch ( ProxyException e )
642         {
643             urlFailureCache.cacheFailure( url );
644             log.warn( "Transfer failed on checksum: " + url + " : " + e.getMessage(), e );
645             // Critical issue, pass it on.
646             throw e;
647         }
648         return destFile;
649     }
650
651     /**
652      * Perform the transfer of the remote file to the local file specified.
653      * 
654      * @param wagon the wagon instance to use.
655      * @param remoteRepository the remote repository to use
656      * @param remotePath the remote path to attempt to get
657      * @param repository the managed repository that will hold the file
658      * @param origFile the local file to save to
659      * @return The local file that was transfered.
660      * @throws ProxyException if there was a problem moving the downloaded file into place.
661      * @throws WagonException if there was a problem tranfering the file.
662      */
663     private void transferSimpleFile( Wagon wagon, RemoteRepositoryContent remoteRepository, String remotePath,
664                                      ManagedRepositoryContent repository, File origFile, File destFile )
665         throws ProxyException
666     {
667         assert ( remotePath != null );
668
669         // Transfer the file.
670         try
671         {
672             boolean success = false;
673
674             if ( !origFile.exists() )
675             {
676                 log.debug( "Retrieving " + remotePath + " from " + remoteRepository.getRepository().getName() );
677                 wagon.get( remotePath, destFile );
678                 success = true;
679
680                 // You wouldn't get here on failure, a WagonException would have been thrown.
681                 log.debug( "Downloaded successfully." );
682             }
683             else
684             {
685                 log.debug( "Retrieving " + remotePath + " from " + remoteRepository.getRepository().getName()
686                     + " if updated" );
687                 success = wagon.getIfNewer( remotePath, destFile, origFile.lastModified() );
688                 if ( !success )
689                 {
690                     throw new NotModifiedException( "Not downloaded, as local file is newer than remote side: "
691                         + origFile.getAbsolutePath() );
692                 }
693
694                 if ( destFile.exists() )
695                 {
696                     log.debug( "Downloaded successfully." );
697                 }
698             }
699         }
700         catch ( ResourceDoesNotExistException e )
701         {
702             throw new NotFoundException( "Resource [" + remoteRepository.getURL() + "/" + remotePath
703                 + "] does not exist: " + e.getMessage(), e );
704         }
705         catch ( WagonException e )
706         {
707             // TODO: shouldn't have to drill into the cause, but TransferFailedException is often not descriptive enough
708
709             String msg =
710                 "Download failure on resource [" + remoteRepository.getURL() + "/" + remotePath + "]:" + e.getMessage();
711             if ( e.getCause() != null )
712             {
713                 msg += " (cause: " + e.getCause() + ")";
714             }
715             throw new ProxyException( msg, e );
716         }
717     }
718
719     /**
720      * Apply the policies.
721      * 
722      * @param policies the map of policies to execute. (Map of String policy keys, to {@link DownloadPolicy} objects)
723      * @param settings the map of settings for the policies to execute. (Map of String policy keys, to String policy
724      *            setting)
725      * @param request the request properties (utilized by the {@link DownloadPolicy#applyPolicy(String,Properties,File)}
726      *            )
727      * @param localFile the local file (utilized by the {@link DownloadPolicy#applyPolicy(String,Properties,File)})
728      */
729     private void validatePolicies( Map<String, ? extends DownloadPolicy> policies, Map<String, String> settings,
730                                    Properties request, File localFile )
731         throws PolicyViolationException
732     {
733         for ( Entry<String, ? extends DownloadPolicy> entry : policies.entrySet() )
734         {
735             String key = entry.getKey();
736             DownloadPolicy policy = entry.getValue();
737             String defaultSetting = policy.getDefaultOption();
738             String setting = StringUtils.defaultString( settings.get( key ), defaultSetting );
739
740             log.debug( "Applying [" + key + "] policy with [" + setting + "]" );
741             try
742             {
743                 policy.applyPolicy( setting, request, localFile );
744             }
745             catch ( PolicyConfigurationException e )
746             {
747                 log.error( e.getMessage(), e );
748             }
749         }
750     }
751
752     private void validatePolicies( Map<String, DownloadErrorPolicy> policies, Map<String, String> settings,
753                                    Properties request, ArtifactReference artifact, RemoteRepositoryContent content,
754                                    File localFile, ProxyException exception, Map<String, Exception> previousExceptions )
755         throws ProxyDownloadException
756     {
757         boolean process = true;
758         for ( Entry<String, ? extends DownloadErrorPolicy> entry : policies.entrySet() )
759         {
760             String key = entry.getKey();
761             DownloadErrorPolicy policy = entry.getValue();
762             String defaultSetting = policy.getDefaultOption();
763             String setting = StringUtils.defaultString( settings.get( key ), defaultSetting );
764
765             log.debug( "Applying [" + key + "] policy with [" + setting + "]" );
766             try
767             {
768                 // all policies must approve the exception, any can cancel
769                 process = policy.applyPolicy( setting, request, localFile, exception, previousExceptions );
770                 if ( !process )
771                 {
772                     break;
773                 }
774             }
775             catch ( PolicyConfigurationException e )
776             {
777                 log.error( e.getMessage(), e );
778             }
779         }
780
781         if ( process )
782         {
783             // if the exception was queued, don't throw it
784             if ( !previousExceptions.containsKey( content.getId() ) )
785             {
786                 throw new ProxyDownloadException(
787                                                   "An error occurred in downloading from the remote repository, and the policy is to fail immediately",
788                                                   content.getId(), exception );
789             }
790         }
791         else
792         {
793             // if the exception was queued, but cancelled, remove it
794             previousExceptions.remove( content.getId() );
795         }
796
797         log.warn( "Transfer error from repository \"" + content.getRepository().getId() + "\" for artifact "
798             + Keys.toKey( artifact ) + ", continuing to next repository. Error message: " + exception.getMessage() );
799         log.debug( "Full stack trace", exception );
800     }
801
802     /**
803      * Creates a working directory in the repository root for this request
804      * 
805      * @param repository
806      * @return file location of working directory
807      * @throws IOException
808      */
809     private File createWorkingDirectory( ManagedRepositoryContent repository )
810     {
811         // TODO: This is ugly - lets actually clean this up when we get the new repository api
812         try
813         {
814             File tmpDir = File.createTempFile( ".workingdirectory", null, new File( repository.getRepoRoot() ) );
815             tmpDir.delete();
816             tmpDir.mkdirs();
817             return tmpDir;
818         }
819         catch ( IOException e )
820         {
821             throw new RuntimeException( "Could not create working directory for this request", e );
822         }
823     }
824
825     /**
826      * Used to move the temporary file to its real destination. This is patterned from the way WagonManager handles its
827      * downloaded files.
828      * 
829      * @param temp The completed download file
830      * @param target The final location of the downloaded file
831      * @throws ProxyException when the temp file cannot replace the target file
832      */
833     private void moveTempToTarget( File temp, File target )
834         throws ProxyException
835     {
836         if ( target.exists() && !target.delete() )
837         {
838             throw new ProxyException( "Unable to overwrite existing target file: " + target.getAbsolutePath() );
839         }
840
841         target.getParentFile().mkdirs();
842         if ( !temp.renameTo( target ) )
843         {
844             log.warn( "Unable to rename tmp file to its final name... resorting to copy command." );
845
846             try
847             {
848                 FileUtils.copyFile( temp, target );
849             }
850             catch ( IOException e )
851             {
852                 if ( target.exists() )
853                 {
854                     log.debug( "Tried to copy file " + temp.getName() + " to " + target.getAbsolutePath()
855                         + " but file with this name already exists." );
856                 }
857                 else
858                 {
859                     throw new ProxyException( "Cannot copy tmp file " + temp.getAbsolutePath()
860                         + " to its final location", e );
861                 }
862             }
863             finally
864             {
865                 FileUtils.deleteQuietly( temp );
866             }
867         }
868     }
869
870     /**
871      * Using wagon, connect to the remote repository.
872      * 
873      * @param connector the connector configuration to utilize (for obtaining network proxy configuration from)
874      * @param wagon the wagon instance to establish the connection on.
875      * @param remoteRepository the remote repository to connect to.
876      * @return true if the connection was successful. false if not connected.
877      */
878     private boolean connectToRepository( ProxyConnector connector, Wagon wagon, RemoteRepositoryContent remoteRepository )
879     {
880         boolean connected = false;
881
882         final ProxyInfo networkProxy;
883         synchronized ( this.networkProxyMap )
884         {
885             networkProxy = (ProxyInfo) this.networkProxyMap.get( connector.getProxyId() );
886         }
887
888         if ( log.isDebugEnabled() )
889         {
890             if ( networkProxy != null )
891             {
892                 // TODO: move to proxyInfo.toString()
893                 String msg =
894                     "Using network proxy " + networkProxy.getHost() + ":" + networkProxy.getPort()
895                         + " to connect to remote repository " + remoteRepository.getURL();
896                 if ( networkProxy.getNonProxyHosts() != null )
897                 {
898                     msg += "; excluding hosts: " + networkProxy.getNonProxyHosts();
899                 }
900                 if ( StringUtils.isNotBlank( networkProxy.getUserName() ) )
901                 {
902                     msg += "; as user: " + networkProxy.getUserName();
903                 }
904                 log.debug( msg );
905             }
906         }
907
908         AuthenticationInfo authInfo = null;
909         String username = remoteRepository.getRepository().getUsername();
910         String password = remoteRepository.getRepository().getPassword();
911
912         if ( StringUtils.isNotBlank( username ) && StringUtils.isNotBlank( password ) )
913         {
914             log.debug( "Using username " + username + " to connect to remote repository " + remoteRepository.getURL() );
915             authInfo = new AuthenticationInfo();
916             authInfo.setUserName( username );
917             authInfo.setPassword( password );
918         }
919
920         // Convert seconds to milliseconds
921         int timeoutInMilliseconds = remoteRepository.getRepository().getTimeout() * 1000;
922
923         // Set timeout
924         wagon.setTimeout( timeoutInMilliseconds );
925
926         try
927         {
928             Repository wagonRepository =
929                 new Repository( remoteRepository.getId(), remoteRepository.getURL().toString() );
930             wagon.connect( wagonRepository, authInfo, networkProxy );
931             connected = true;
932         }
933         catch ( ConnectionException e )
934         {
935             log.warn( "Could not connect to " + remoteRepository.getRepository().getName() + ": " + e.getMessage() );
936             connected = false;
937         }
938         catch ( AuthenticationException e )
939         {
940             log.warn( "Could not connect to " + remoteRepository.getRepository().getName() + ": " + e.getMessage() );
941             connected = false;
942         }
943
944         return connected;
945     }
946
947     /**
948      * Tests whitelist and blacklist patterns against path.
949      * 
950      * @param path the path to test.
951      * @param patterns the list of patterns to check.
952      * @return true if the path matches at least 1 pattern in the provided patterns list.
953      */
954     private boolean matchesPattern( String path, List<String> patterns )
955     {
956         if ( CollectionUtils.isEmpty( patterns ) )
957         {
958             return false;
959         }
960
961         if ( !path.startsWith( "/" ) )
962         {
963             path = "/" + path;
964         }
965         
966         for ( String pattern : patterns )
967         {
968             if ( !pattern.startsWith( "/" ) )
969             {
970                 pattern = "/" + pattern;
971             }
972             
973             if ( SelectorUtils.matchPath( pattern, path, false ) )
974             {
975                 return true;
976             }
977         }
978
979         return false;
980     }
981
982     /**
983      * TODO: Ensure that list is correctly ordered based on configuration. See MRM-477
984      */
985     public List<ProxyConnector> getProxyConnectors( ManagedRepositoryContent repository )
986     {
987         synchronized ( this.proxyConnectorMap )
988         {
989             List<ProxyConnector> ret = (List<ProxyConnector>) this.proxyConnectorMap.get( repository.getId() );
990             if ( ret == null )
991             {
992                 return Collections.emptyList();
993             }
994
995             Collections.sort( ret, ProxyConnectorOrderComparator.getInstance() );
996             return ret;
997         }
998     }
999
1000     public void afterConfigurationChange( Registry registry, String propertyName, Object propertyValue )
1001     {
1002         if ( ConfigurationNames.isNetworkProxy( propertyName )
1003             || ConfigurationNames.isManagedRepositories( propertyName )
1004             || ConfigurationNames.isRemoteRepositories( propertyName )
1005             || ConfigurationNames.isProxyConnector( propertyName ) )
1006         {
1007             initConnectorsAndNetworkProxies();
1008         }
1009     }
1010
1011     public void beforeConfigurationChange( Registry registry, String propertyName, Object propertyValue )
1012     {
1013         /* do nothing */
1014     }
1015
1016     @SuppressWarnings( "unchecked" )
1017     private void initConnectorsAndNetworkProxies()
1018     {
1019         synchronized ( this.proxyConnectorMap )
1020         {
1021             ProxyConnectorOrderComparator proxyOrderSorter = new ProxyConnectorOrderComparator();
1022             this.proxyConnectorMap.clear();
1023
1024             List<ProxyConnectorConfiguration> proxyConfigs =
1025                 archivaConfiguration.getConfiguration().getProxyConnectors();
1026             for ( ProxyConnectorConfiguration proxyConfig : proxyConfigs )
1027             {
1028                 String key = proxyConfig.getSourceRepoId();
1029
1030                 try
1031                 {
1032                     // Create connector object.
1033                     ProxyConnector connector = new ProxyConnector();
1034
1035                     connector.setSourceRepository( repositoryFactory.getManagedRepositoryContent( proxyConfig.getSourceRepoId() ) );
1036                     connector.setTargetRepository( repositoryFactory.getRemoteRepositoryContent( proxyConfig.getTargetRepoId() ) );
1037
1038                     connector.setProxyId( proxyConfig.getProxyId() );
1039                     connector.setPolicies( proxyConfig.getPolicies() );
1040                     connector.setOrder( proxyConfig.getOrder() );
1041                     connector.setDisabled( proxyConfig.isDisabled() );
1042
1043                     // Copy any blacklist patterns.
1044                     List<String> blacklist = new ArrayList<String>();
1045                     if ( CollectionUtils.isNotEmpty( proxyConfig.getBlackListPatterns() ) )
1046                     {
1047                         blacklist.addAll( proxyConfig.getBlackListPatterns() );
1048                     }
1049                     connector.setBlacklist( blacklist );
1050
1051                     // Copy any whitelist patterns.
1052                     List<String> whitelist = new ArrayList<String>();
1053                     if ( CollectionUtils.isNotEmpty( proxyConfig.getWhiteListPatterns() ) )
1054                     {
1055                         whitelist.addAll( proxyConfig.getWhiteListPatterns() );
1056                     }
1057                     connector.setWhitelist( whitelist );
1058
1059                     // Get other connectors
1060                     List<ProxyConnector> connectors = this.proxyConnectorMap.get( key );
1061                     if ( connectors == null )
1062                     {
1063                         // Create if we are the first.
1064                         connectors = new ArrayList<ProxyConnector>();
1065                     }
1066
1067                     // Add the connector.
1068                     connectors.add( connector );
1069
1070                     // Ensure the list is sorted.
1071                     Collections.sort( connectors, proxyOrderSorter );
1072
1073                     // Set the key to the list of connectors.
1074                     this.proxyConnectorMap.put( key, connectors );
1075                 }
1076                 catch ( RepositoryNotFoundException e )
1077                 {
1078                     log.warn( "Unable to use proxy connector: " + e.getMessage(), e );
1079                 }
1080                 catch ( RepositoryException e )
1081                 {
1082                     log.warn( "Unable to use proxy connector: " + e.getMessage(), e );
1083                 }
1084             }
1085
1086         }
1087
1088         synchronized ( this.networkProxyMap )
1089         {
1090             this.networkProxyMap.clear();
1091
1092             List<NetworkProxyConfiguration> networkProxies =
1093                 archivaConfiguration.getConfiguration().getNetworkProxies();
1094             for ( NetworkProxyConfiguration networkProxyConfig : networkProxies )
1095             {
1096                 String key = networkProxyConfig.getId();
1097
1098                 ProxyInfo proxy = new ProxyInfo();
1099
1100                 proxy.setType( networkProxyConfig.getProtocol() );
1101                 proxy.setHost( networkProxyConfig.getHost() );
1102                 proxy.setPort( networkProxyConfig.getPort() );
1103                 proxy.setUserName( networkProxyConfig.getUsername() );
1104                 proxy.setPassword( networkProxyConfig.getPassword() );
1105
1106                 this.networkProxyMap.put( key, proxy );
1107             }
1108         }
1109     }
1110
1111     public void initialize()
1112         throws InitializationException
1113     {
1114         initConnectorsAndNetworkProxies();
1115         archivaConfiguration.addChangeListener( this );
1116     }
1117 }