001 package org.apache.archiva.proxy; 002 003 /* 004 * Licensed to the Apache Software Foundation (ASF) under one 005 * or more contributor license agreements. See the NOTICE file 006 * distributed with this work for additional information 007 * regarding copyright ownership. The ASF licenses this file 008 * to you under the Apache License, Version 2.0 (the 009 * "License"); you may not use this file except in compliance 010 * with the License. You may obtain a copy of the License at 011 * 012 * http://www.apache.org/licenses/LICENSE-2.0 013 * 014 * Unless required by applicable law or agreed to in writing, 015 * software distributed under the License is distributed on an 016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 017 * KIND, either express or implied. See the License for the 018 * specific language governing permissions and limitations 019 * under the License. 020 */ 021 022 import com.google.common.io.Files; 023 import org.apache.archiva.admin.model.RepositoryAdminException; 024 import org.apache.archiva.admin.model.beans.NetworkProxy; 025 import org.apache.archiva.admin.model.beans.ProxyConnectorRuleType; 026 import org.apache.archiva.admin.model.beans.RemoteRepository; 027 import org.apache.archiva.admin.model.networkproxy.NetworkProxyAdmin; 028 import org.apache.archiva.configuration.ArchivaConfiguration; 029 import org.apache.archiva.configuration.Configuration; 030 import org.apache.archiva.configuration.ConfigurationNames; 031 import org.apache.archiva.configuration.NetworkProxyConfiguration; 032 import org.apache.archiva.configuration.ProxyConnectorConfiguration; 033 import org.apache.archiva.configuration.ProxyConnectorRuleConfiguration; 034 import org.apache.archiva.model.ArtifactReference; 035 import org.apache.archiva.model.Keys; 036 import org.apache.archiva.model.RepositoryURL; 037 import org.apache.archiva.policies.DownloadErrorPolicy; 038 import org.apache.archiva.policies.DownloadPolicy; 039 import org.apache.archiva.policies.PolicyConfigurationException; 040 import org.apache.archiva.policies.PolicyViolationException; 041 import org.apache.archiva.policies.PostDownloadPolicy; 042 import org.apache.archiva.policies.PreDownloadPolicy; 043 import org.apache.archiva.policies.ProxyDownloadException; 044 import org.apache.archiva.policies.urlcache.UrlFailureCache; 045 import org.apache.archiva.proxy.common.WagonFactory; 046 import org.apache.archiva.proxy.common.WagonFactoryException; 047 import org.apache.archiva.proxy.common.WagonFactoryRequest; 048 import org.apache.archiva.proxy.model.ProxyConnector; 049 import org.apache.archiva.proxy.model.RepositoryProxyConnectors; 050 import org.apache.archiva.redback.components.registry.Registry; 051 import org.apache.archiva.redback.components.registry.RegistryListener; 052 import org.apache.archiva.redback.components.taskqueue.TaskQueueException; 053 import org.apache.archiva.repository.ManagedRepositoryContent; 054 import org.apache.archiva.repository.RemoteRepositoryContent; 055 import org.apache.archiva.repository.RepositoryContentFactory; 056 import org.apache.archiva.repository.RepositoryException; 057 import org.apache.archiva.repository.RepositoryNotFoundException; 058 import org.apache.archiva.repository.metadata.MetadataTools; 059 import org.apache.archiva.repository.metadata.RepositoryMetadataException; 060 import org.apache.archiva.scheduler.ArchivaTaskScheduler; 061 import org.apache.archiva.scheduler.repository.model.RepositoryTask; 062 import org.apache.commons.collections.CollectionUtils; 063 import org.apache.commons.io.FileUtils; 064 import org.apache.commons.io.FilenameUtils; 065 import org.apache.commons.lang.StringUtils; 066 import org.apache.commons.lang.SystemUtils; 067 import org.apache.maven.wagon.ConnectionException; 068 import org.apache.maven.wagon.ResourceDoesNotExistException; 069 import org.apache.maven.wagon.Wagon; 070 import org.apache.maven.wagon.WagonException; 071 import org.apache.maven.wagon.authentication.AuthenticationException; 072 import org.apache.maven.wagon.authentication.AuthenticationInfo; 073 import org.apache.maven.wagon.proxy.ProxyInfo; 074 import org.apache.maven.wagon.repository.Repository; 075 import org.apache.tools.ant.types.selectors.SelectorUtils; 076 import org.slf4j.Logger; 077 import org.slf4j.LoggerFactory; 078 import org.slf4j.MarkerFactory; 079 import org.springframework.stereotype.Service; 080 081 import javax.annotation.PostConstruct; 082 import javax.inject.Inject; 083 import javax.inject.Named; 084 import java.io.File; 085 import java.io.IOException; 086 import java.util.ArrayList; 087 import java.util.Collections; 088 import java.util.HashMap; 089 import java.util.LinkedHashMap; 090 import java.util.List; 091 import java.util.Map; 092 import java.util.Map.Entry; 093 import java.util.Properties; 094 import java.util.concurrent.ConcurrentHashMap; 095 096 /** 097 * DefaultRepositoryProxyConnectors 098 * 099 * @todo exception handling needs work - "not modified" is not really an exceptional case, and it has more layers than 100 * your average brown onion 101 */ 102 @Service("repositoryProxyConnectors#default") 103 public class DefaultRepositoryProxyConnectors 104 implements RepositoryProxyConnectors, RegistryListener 105 { 106 private Logger log = LoggerFactory.getLogger( DefaultRepositoryProxyConnectors.class ); 107 108 /** 109 * 110 */ 111 @Inject 112 @Named(value = "archivaConfiguration#default") 113 private ArchivaConfiguration archivaConfiguration; 114 115 /** 116 * 117 */ 118 @Inject 119 @Named(value = "repositoryContentFactory#default") 120 private RepositoryContentFactory repositoryFactory; 121 122 /** 123 * 124 */ 125 @Inject 126 @Named(value = "metadataTools#default") 127 private MetadataTools metadataTools; 128 129 /** 130 * 131 */ 132 @Inject 133 private Map<String, PreDownloadPolicy> preDownloadPolicies; 134 135 /** 136 * 137 */ 138 @Inject 139 private Map<String, PostDownloadPolicy> postDownloadPolicies; 140 141 /** 142 * 143 */ 144 @Inject 145 private Map<String, DownloadErrorPolicy> downloadErrorPolicies; 146 147 /** 148 * 149 */ 150 @Inject 151 private UrlFailureCache urlFailureCache; 152 153 private Map<String, List<ProxyConnector>> proxyConnectorMap = new HashMap<String, List<ProxyConnector>>(); 154 155 private Map<String, ProxyInfo> networkProxyMap = new ConcurrentHashMap<String, ProxyInfo>(); 156 157 /** 158 * 159 */ 160 @Inject 161 private WagonFactory wagonFactory; 162 163 /** 164 * 165 */ 166 @Inject 167 @Named(value = "archivaTaskScheduler#repository") 168 private ArchivaTaskScheduler scheduler; 169 170 @Inject 171 private NetworkProxyAdmin networkProxyAdmin; 172 173 @PostConstruct 174 public void initialize() 175 { 176 initConnectorsAndNetworkProxies(); 177 archivaConfiguration.addChangeListener( this ); 178 179 } 180 181 @SuppressWarnings("unchecked") 182 private void initConnectorsAndNetworkProxies() 183 { 184 185 ProxyConnectorOrderComparator proxyOrderSorter = new ProxyConnectorOrderComparator(); 186 this.proxyConnectorMap.clear(); 187 188 Configuration configuration = archivaConfiguration.getConfiguration(); 189 190 List<ProxyConnectorRuleConfiguration> allProxyConnectorRuleConfigurations = 191 configuration.getProxyConnectorRuleConfigurations(); 192 193 List<ProxyConnectorConfiguration> proxyConfigs = configuration.getProxyConnectors(); 194 for ( ProxyConnectorConfiguration proxyConfig : proxyConfigs ) 195 { 196 String key = proxyConfig.getSourceRepoId(); 197 198 try 199 { 200 // Create connector object. 201 ProxyConnector connector = new ProxyConnector(); 202 203 connector.setSourceRepository( 204 repositoryFactory.getManagedRepositoryContent( proxyConfig.getSourceRepoId() ) ); 205 connector.setTargetRepository( 206 repositoryFactory.getRemoteRepositoryContent( proxyConfig.getTargetRepoId() ) ); 207 208 connector.setProxyId( proxyConfig.getProxyId() ); 209 connector.setPolicies( proxyConfig.getPolicies() ); 210 connector.setOrder( proxyConfig.getOrder() ); 211 connector.setDisabled( proxyConfig.isDisabled() ); 212 213 // Copy any blacklist patterns. 214 List<String> blacklist = new ArrayList<String>( 0 ); 215 if ( CollectionUtils.isNotEmpty( proxyConfig.getBlackListPatterns() ) ) 216 { 217 blacklist.addAll( proxyConfig.getBlackListPatterns() ); 218 } 219 connector.setBlacklist( blacklist ); 220 221 // Copy any whitelist patterns. 222 List<String> whitelist = new ArrayList<String>( 0 ); 223 if ( CollectionUtils.isNotEmpty( proxyConfig.getWhiteListPatterns() ) ) 224 { 225 whitelist.addAll( proxyConfig.getWhiteListPatterns() ); 226 } 227 connector.setWhitelist( whitelist ); 228 229 List<ProxyConnectorRuleConfiguration> proxyConnectorRuleConfigurations = 230 findProxyConnectorRules( connector.getSourceRepository().getId(), 231 connector.getTargetRepository().getId(), 232 allProxyConnectorRuleConfigurations ); 233 234 if ( !proxyConnectorRuleConfigurations.isEmpty() ) 235 { 236 for ( ProxyConnectorRuleConfiguration proxyConnectorRuleConfiguration : proxyConnectorRuleConfigurations ) 237 { 238 if ( StringUtils.equals( proxyConnectorRuleConfiguration.getRuleType(), 239 ProxyConnectorRuleType.BLACK_LIST.getRuleType() ) ) 240 { 241 connector.getBlacklist().add( proxyConnectorRuleConfiguration.getPattern() ); 242 } 243 244 if ( StringUtils.equals( proxyConnectorRuleConfiguration.getRuleType(), 245 ProxyConnectorRuleType.WHITE_LIST.getRuleType() ) ) 246 { 247 connector.getWhitelist().add( proxyConnectorRuleConfiguration.getPattern() ); 248 } 249 } 250 } 251 252 // Get other connectors 253 List<ProxyConnector> connectors = this.proxyConnectorMap.get( key ); 254 if ( connectors == null ) 255 { 256 // Create if we are the first. 257 connectors = new ArrayList<ProxyConnector>( 1 ); 258 } 259 260 // Add the connector. 261 connectors.add( connector ); 262 263 // Ensure the list is sorted. 264 Collections.sort( connectors, proxyOrderSorter ); 265 266 // Set the key to the list of connectors. 267 this.proxyConnectorMap.put( key, connectors ); 268 } 269 catch ( RepositoryNotFoundException e ) 270 { 271 log.warn( "Unable to use proxy connector: " + e.getMessage(), e ); 272 } 273 catch ( RepositoryException e ) 274 { 275 log.warn( "Unable to use proxy connector: " + e.getMessage(), e ); 276 } 277 278 279 } 280 281 this.networkProxyMap.clear(); 282 283 List<NetworkProxyConfiguration> networkProxies = archivaConfiguration.getConfiguration().getNetworkProxies(); 284 for ( NetworkProxyConfiguration networkProxyConfig : networkProxies ) 285 { 286 String key = networkProxyConfig.getId(); 287 288 ProxyInfo proxy = new ProxyInfo(); 289 290 proxy.setType( networkProxyConfig.getProtocol() ); 291 proxy.setHost( networkProxyConfig.getHost() ); 292 proxy.setPort( networkProxyConfig.getPort() ); 293 proxy.setUserName( networkProxyConfig.getUsername() ); 294 proxy.setPassword( networkProxyConfig.getPassword() ); 295 296 this.networkProxyMap.put( key, proxy ); 297 } 298 299 } 300 301 private List<ProxyConnectorRuleConfiguration> findProxyConnectorRules( String sourceRepository, 302 String targetRepository, 303 List<ProxyConnectorRuleConfiguration> all ) 304 { 305 List<ProxyConnectorRuleConfiguration> proxyConnectorRuleConfigurations = 306 new ArrayList<ProxyConnectorRuleConfiguration>(); 307 308 for ( ProxyConnectorRuleConfiguration proxyConnectorRuleConfiguration : all ) 309 { 310 for ( ProxyConnectorConfiguration proxyConnector : proxyConnectorRuleConfiguration.getProxyConnectors() ) 311 { 312 if ( StringUtils.equals( sourceRepository, proxyConnector.getSourceRepoId() ) && StringUtils.equals( 313 targetRepository, proxyConnector.getTargetRepoId() ) ) 314 { 315 proxyConnectorRuleConfigurations.add( proxyConnectorRuleConfiguration ); 316 } 317 } 318 } 319 320 return proxyConnectorRuleConfigurations; 321 } 322 323 public File fetchFromProxies( ManagedRepositoryContent repository, ArtifactReference artifact ) 324 throws ProxyDownloadException 325 { 326 File localFile = toLocalFile( repository, artifact ); 327 328 Properties requestProperties = new Properties(); 329 requestProperties.setProperty( "filetype", "artifact" ); 330 requestProperties.setProperty( "version", artifact.getVersion() ); 331 requestProperties.setProperty( "managedRepositoryId", repository.getId() ); 332 333 List<ProxyConnector> connectors = getProxyConnectors( repository ); 334 Map<String, Exception> previousExceptions = new LinkedHashMap<String, Exception>(); 335 for ( ProxyConnector connector : connectors ) 336 { 337 if ( connector.isDisabled() ) 338 { 339 continue; 340 } 341 342 RemoteRepositoryContent targetRepository = connector.getTargetRepository(); 343 requestProperties.setProperty( "remoteRepositoryId", targetRepository.getId() ); 344 345 String targetPath = targetRepository.toPath( artifact ); 346 347 if ( SystemUtils.IS_OS_WINDOWS ) 348 { 349 // toPath use system PATH_SEPARATOR so on windows url are \ which doesn't work very well :-) 350 targetPath = FilenameUtils.separatorsToUnix( targetPath ); 351 } 352 353 try 354 { 355 File downloadedFile = 356 transferFile( connector, targetRepository, targetPath, repository, localFile, requestProperties, 357 true ); 358 359 if ( fileExists( downloadedFile ) ) 360 { 361 log.debug( "Successfully transferred: {}", downloadedFile.getAbsolutePath() ); 362 return downloadedFile; 363 } 364 } 365 catch ( NotFoundException e ) 366 { 367 log.debug( "Artifact {} not found on repository \"{}\".", Keys.toKey( artifact ), 368 targetRepository.getRepository().getId() ); 369 } 370 catch ( NotModifiedException e ) 371 { 372 log.debug( "Artifact {} not updated on repository \"{}\".", Keys.toKey( artifact ), 373 targetRepository.getRepository().getId() ); 374 } 375 catch ( ProxyException e ) 376 { 377 validatePolicies( this.downloadErrorPolicies, connector.getPolicies(), requestProperties, artifact, 378 targetRepository, localFile, e, previousExceptions ); 379 } 380 catch ( RepositoryAdminException e ) 381 { 382 validatePolicies( this.downloadErrorPolicies, connector.getPolicies(), requestProperties, artifact, 383 targetRepository, localFile, e, previousExceptions ); 384 } 385 } 386 387 if ( !previousExceptions.isEmpty() ) 388 { 389 throw new ProxyDownloadException( "Failures occurred downloading from some remote repositories", 390 previousExceptions ); 391 } 392 393 log.debug( "Exhausted all target repositories, artifact {} not found.", Keys.toKey( artifact ) ); 394 395 return null; 396 } 397 398 public File fetchFromProxies( ManagedRepositoryContent repository, String path ) 399 { 400 File localFile = new File( repository.getRepoRoot(), path ); 401 402 // no update policies for these paths 403 if ( localFile.exists() ) 404 { 405 return null; 406 } 407 408 Properties requestProperties = new Properties(); 409 requestProperties.setProperty( "filetype", "resource" ); 410 requestProperties.setProperty( "managedRepositoryId", repository.getId() ); 411 412 List<ProxyConnector> connectors = getProxyConnectors( repository ); 413 for ( ProxyConnector connector : connectors ) 414 { 415 if ( connector.isDisabled() ) 416 { 417 continue; 418 } 419 420 RemoteRepositoryContent targetRepository = connector.getTargetRepository(); 421 requestProperties.setProperty( "remoteRepositoryId", targetRepository.getId() ); 422 423 String targetPath = path; 424 425 try 426 { 427 File downloadedFile = 428 transferFile( connector, targetRepository, targetPath, repository, localFile, requestProperties, 429 false ); 430 431 if ( fileExists( downloadedFile ) ) 432 { 433 log.debug( "Successfully transferred: {}", downloadedFile.getAbsolutePath() ); 434 return downloadedFile; 435 } 436 } 437 catch ( NotFoundException e ) 438 { 439 log.debug( "Resource {} not found on repository \"{}\".", path, 440 targetRepository.getRepository().getId() ); 441 } 442 catch ( NotModifiedException e ) 443 { 444 log.debug( "Resource {} not updated on repository \"{}\".", path, 445 targetRepository.getRepository().getId() ); 446 } 447 catch ( ProxyException e ) 448 { 449 log.warn( 450 "Transfer error from repository \"" + targetRepository.getRepository().getId() + "\" for resource " 451 + path + ", continuing to next repository. Error message: {}", e.getMessage() ); 452 log.debug( MarkerFactory.getDetachedMarker( "transfer.error" ), 453 "Transfer error from repository \"" + targetRepository.getRepository().getId() 454 + "\" for resource " + path + ", continuing to next repository. Error message: {}", 455 e.getMessage(), e ); 456 } 457 catch ( RepositoryAdminException e ) 458 { 459 log.debug( MarkerFactory.getDetachedMarker( "transfer.error" ), 460 "Transfer error from repository \"" + targetRepository.getRepository().getId() 461 + "\" for resource " + path + ", continuing to next repository. Error message: {}", 462 e.getMessage(), e ); 463 log.debug( MarkerFactory.getDetachedMarker( "transfer.error" ), "Full stack trace", e ); 464 } 465 } 466 467 log.debug( "Exhausted all target repositories, resource {} not found.", path ); 468 469 return null; 470 } 471 472 public File fetchMetatadaFromProxies( ManagedRepositoryContent repository, String logicalPath ) 473 { 474 File localFile = new File( repository.getRepoRoot(), logicalPath ); 475 476 Properties requestProperties = new Properties(); 477 requestProperties.setProperty( "filetype", "metadata" ); 478 boolean metadataNeedsUpdating = false; 479 long originalTimestamp = getLastModified( localFile ); 480 481 List<ProxyConnector> connectors = getProxyConnectors( repository ); 482 for ( ProxyConnector connector : connectors ) 483 { 484 if ( connector.isDisabled() ) 485 { 486 continue; 487 } 488 489 RemoteRepositoryContent targetRepository = connector.getTargetRepository(); 490 491 File localRepoFile = toLocalRepoFile( repository, targetRepository, logicalPath ); 492 long originalMetadataTimestamp = getLastModified( localRepoFile ); 493 494 try 495 { 496 transferFile( connector, targetRepository, logicalPath, repository, localRepoFile, requestProperties, 497 true ); 498 499 if ( hasBeenUpdated( localRepoFile, originalMetadataTimestamp ) ) 500 { 501 metadataNeedsUpdating = true; 502 } 503 } 504 catch ( NotFoundException e ) 505 { 506 507 log.debug( "Metadata {} not found on remote repository '{}'.", logicalPath, 508 targetRepository.getRepository().getId(), e ); 509 510 } 511 catch ( NotModifiedException e ) 512 { 513 514 log.debug( "Metadata {} not updated on remote repository '{}'.", logicalPath, 515 targetRepository.getRepository().getId(), e ); 516 517 } 518 catch ( ProxyException e ) 519 { 520 log.warn( "Transfer error from repository \"" + targetRepository.getRepository().getId() 521 + "\" for versioned Metadata " + logicalPath 522 + ", continuing to next repository. Error message: " + e.getMessage() ); 523 log.debug( "Full stack trace", e ); 524 } 525 catch ( RepositoryAdminException e ) 526 { 527 log.warn( "Transfer error from repository \"" + targetRepository.getRepository().getId() 528 + "\" for versioned Metadata " + logicalPath 529 + ", continuing to next repository. Error message: " + e.getMessage() ); 530 log.debug( "Full stack trace", e ); 531 } 532 } 533 534 if ( hasBeenUpdated( localFile, originalTimestamp ) ) 535 { 536 metadataNeedsUpdating = true; 537 } 538 539 if ( metadataNeedsUpdating || !localFile.exists() ) 540 { 541 try 542 { 543 metadataTools.updateMetadata( repository, logicalPath ); 544 } 545 catch ( RepositoryMetadataException e ) 546 { 547 log.warn( "Unable to update metadata " + localFile.getAbsolutePath() + ": " + e.getMessage(), e ); 548 } 549 } 550 551 if ( fileExists( localFile ) ) 552 { 553 return localFile; 554 } 555 556 return null; 557 } 558 559 /** 560 * @param connector 561 * @param remoteRepository 562 * @param tmpMd5 563 * @param tmpSha1 564 * @param tmpResource 565 * @param url 566 * @param remotePath 567 * @param resource 568 * @param workingDirectory 569 * @param repository 570 * @throws ProxyException 571 * @throws NotModifiedException 572 */ 573 protected void transferResources( ProxyConnector connector, RemoteRepositoryContent remoteRepository, File tmpMd5, 574 File tmpSha1, File tmpResource, String url, String remotePath, File resource, 575 File workingDirectory, ManagedRepositoryContent repository ) 576 throws ProxyException, NotModifiedException, RepositoryAdminException 577 { 578 Wagon wagon = null; 579 try 580 { 581 RepositoryURL repoUrl = remoteRepository.getURL(); 582 String protocol = repoUrl.getProtocol(); 583 NetworkProxy networkProxy = null; 584 if ( StringUtils.isNotBlank( connector.getProxyId() ) ) 585 { 586 networkProxy = networkProxyAdmin.getNetworkProxy( connector.getProxyId() ); 587 } 588 WagonFactoryRequest wagonFactoryRequest = new WagonFactoryRequest( "wagon#" + protocol, 589 remoteRepository.getRepository().getExtraHeaders() ).networkProxy( 590 networkProxy ); 591 wagon = wagonFactory.getWagon( wagonFactoryRequest ); 592 if ( wagon == null ) 593 { 594 throw new ProxyException( "Unsupported target repository protocol: " + protocol ); 595 } 596 597 if ( wagon == null ) 598 { 599 throw new ProxyException( "Unsupported target repository protocol: " + protocol ); 600 } 601 602 boolean connected = connectToRepository( connector, wagon, remoteRepository ); 603 if ( connected ) 604 { 605 transferArtifact( wagon, remoteRepository, remotePath, repository, resource, workingDirectory, 606 tmpResource ); 607 608 // TODO: these should be used to validate the download based on the policies, not always downloaded 609 // to 610 // save on connections since md5 is rarely used 611 transferChecksum( wagon, remoteRepository, remotePath, repository, resource, workingDirectory, ".sha1", 612 tmpSha1 ); 613 transferChecksum( wagon, remoteRepository, remotePath, repository, resource, workingDirectory, ".md5", 614 tmpMd5 ); 615 } 616 } 617 catch ( NotFoundException e ) 618 { 619 urlFailureCache.cacheFailure( url ); 620 throw e; 621 } 622 catch ( NotModifiedException e ) 623 { 624 // Do not cache url here. 625 throw e; 626 } 627 catch ( ProxyException e ) 628 { 629 urlFailureCache.cacheFailure( url ); 630 throw e; 631 } 632 catch ( WagonFactoryException e ) 633 { 634 throw new ProxyException( e.getMessage(), e ); 635 } 636 finally 637 { 638 if ( wagon != null ) 639 { 640 try 641 { 642 wagon.disconnect(); 643 } 644 catch ( ConnectionException e ) 645 { 646 log.warn( "Unable to disconnect wagon.", e ); 647 } 648 } 649 } 650 } 651 652 private void transferArtifact( Wagon wagon, RemoteRepositoryContent remoteRepository, String remotePath, 653 ManagedRepositoryContent repository, File resource, File tmpDirectory, 654 File destFile ) 655 throws ProxyException 656 { 657 transferSimpleFile( wagon, remoteRepository, remotePath, repository, resource, destFile ); 658 } 659 660 private long getLastModified( File file ) 661 { 662 if ( !file.exists() || !file.isFile() ) 663 { 664 return 0; 665 } 666 667 return file.lastModified(); 668 } 669 670 private boolean hasBeenUpdated( File file, long originalLastModified ) 671 { 672 if ( !file.exists() || !file.isFile() ) 673 { 674 return false; 675 } 676 677 long currentLastModified = getLastModified( file ); 678 return ( currentLastModified > originalLastModified ); 679 } 680 681 private File toLocalRepoFile( ManagedRepositoryContent repository, RemoteRepositoryContent targetRepository, 682 String targetPath ) 683 { 684 String repoPath = metadataTools.getRepositorySpecificName( targetRepository, targetPath ); 685 return new File( repository.getRepoRoot(), repoPath ); 686 } 687 688 /** 689 * Test if the provided ManagedRepositoryContent has any proxies configured for it. 690 */ 691 public boolean hasProxies( ManagedRepositoryContent repository ) 692 { 693 synchronized ( this.proxyConnectorMap ) 694 { 695 return this.proxyConnectorMap.containsKey( repository.getId() ); 696 } 697 } 698 699 private File toLocalFile( ManagedRepositoryContent repository, ArtifactReference artifact ) 700 { 701 return repository.toFile( artifact ); 702 } 703 704 /** 705 * Simple method to test if the file exists on the local disk. 706 * 707 * @param file the file to test. (may be null) 708 * @return true if file exists. false if the file param is null, doesn't exist, or is not of type File. 709 */ 710 private boolean fileExists( File file ) 711 { 712 if ( file == null ) 713 { 714 return false; 715 } 716 717 if ( !file.exists() ) 718 { 719 return false; 720 } 721 722 if ( !file.isFile() ) 723 { 724 return false; 725 } 726 727 return true; 728 } 729 730 /** 731 * Perform the transfer of the file. 732 * 733 * @param connector the connector configuration to use. 734 * @param remoteRepository the remote repository get the resource from. 735 * @param remotePath the path in the remote repository to the resource to get. 736 * @param repository the managed repository that will hold the file 737 * @param resource the local file to place the downloaded resource into 738 * @param requestProperties the request properties to utilize for policy handling. 739 * @param executeConsumers whether to execute the consumers after proxying 740 * @return the local file that was downloaded, or null if not downloaded. 741 * @throws NotFoundException if the file was not found on the remote repository. 742 * @throws NotModifiedException if the localFile was present, and the resource was present on remote repository, but 743 * the remote resource is not newer than the local File. 744 * @throws ProxyException if transfer was unsuccessful. 745 */ 746 private File transferFile( ProxyConnector connector, RemoteRepositoryContent remoteRepository, String remotePath, 747 ManagedRepositoryContent repository, File resource, Properties requestProperties, 748 boolean executeConsumers ) 749 throws ProxyException, NotModifiedException, RepositoryAdminException 750 { 751 String url = remoteRepository.getURL().getUrl(); 752 if ( !url.endsWith( "/" ) ) 753 { 754 url = url + "/"; 755 } 756 url = url + remotePath; 757 requestProperties.setProperty( "url", url ); 758 759 // Is a whitelist defined? 760 if ( CollectionUtils.isNotEmpty( connector.getWhitelist() ) ) 761 { 762 // Path must belong to whitelist. 763 if ( !matchesPattern( remotePath, connector.getWhitelist() ) ) 764 { 765 log.debug( "Path [{}] is not part of defined whitelist (skipping transfer from repository [{}]).", 766 remotePath, remoteRepository.getRepository().getName() ); 767 return null; 768 } 769 } 770 771 // Is target path part of blacklist? 772 if ( matchesPattern( remotePath, connector.getBlacklist() ) ) 773 { 774 log.debug( "Path [{}] is part of blacklist (skipping transfer from repository [{}]).", remotePath, 775 remoteRepository.getRepository().getName() ); 776 return null; 777 } 778 779 // Handle pre-download policy 780 try 781 { 782 validatePolicies( this.preDownloadPolicies, connector.getPolicies(), requestProperties, resource ); 783 } 784 catch ( PolicyViolationException e ) 785 { 786 String emsg = "Transfer not attempted on " + url + " : " + e.getMessage(); 787 if ( fileExists( resource ) ) 788 { 789 log.debug( "{} : using already present local file.", emsg ); 790 return resource; 791 } 792 793 log.debug( emsg ); 794 return null; 795 } 796 797 File workingDirectory = createWorkingDirectory( repository ); 798 File tmpResource = new File( workingDirectory, resource.getName() ); 799 File tmpMd5 = new File( workingDirectory, resource.getName() + ".md5" ); 800 File tmpSha1 = new File( workingDirectory, resource.getName() + ".sha1" ); 801 802 try 803 { 804 Wagon wagon = null; 805 806 transferResources( connector, remoteRepository, tmpMd5, tmpSha1, tmpResource, url, remotePath, resource, 807 workingDirectory, repository ); 808 809 // Handle post-download policies. 810 try 811 { 812 validatePolicies( this.postDownloadPolicies, connector.getPolicies(), requestProperties, tmpResource ); 813 } 814 catch ( PolicyViolationException e ) 815 { 816 log.warn( "Transfer invalidated from {} : {}", url, e.getMessage() ); 817 executeConsumers = false; 818 if ( !fileExists( tmpResource ) ) 819 { 820 resource = null; 821 } 822 } 823 824 if ( resource != null ) 825 { 826 synchronized ( resource.getAbsolutePath().intern() ) 827 { 828 File directory = resource.getParentFile(); 829 moveFileIfExists( tmpMd5, directory ); 830 moveFileIfExists( tmpSha1, directory ); 831 moveFileIfExists( tmpResource, directory ); 832 } 833 } 834 } 835 finally 836 { 837 FileUtils.deleteQuietly( workingDirectory ); 838 } 839 840 if ( executeConsumers ) 841 { 842 // Just-in-time update of the index and database by executing the consumers for this artifact 843 //consumers.executeConsumers( connector.getSourceRepository().getRepository(), resource ); 844 queueRepositoryTask( connector.getSourceRepository().getRepository().getId(), resource ); 845 } 846 847 return resource; 848 } 849 850 private void queueRepositoryTask( String repositoryId, File localFile ) 851 { 852 RepositoryTask task = new RepositoryTask(); 853 task.setRepositoryId( repositoryId ); 854 task.setResourceFile( localFile ); 855 task.setUpdateRelatedArtifacts( true ); 856 task.setScanAll( true ); 857 858 try 859 { 860 scheduler.queueTask( task ); 861 } 862 catch ( TaskQueueException e ) 863 { 864 log.error( "Unable to queue repository task to execute consumers on resource file ['" + localFile.getName() 865 + "']." ); 866 } 867 } 868 869 /** 870 * Moves the file into repository location if it exists 871 * 872 * @param fileToMove this could be either the main artifact, sha1 or md5 checksum file. 873 * @param directory directory to write files to 874 */ 875 private void moveFileIfExists( File fileToMove, File directory ) 876 throws ProxyException 877 { 878 if ( fileToMove != null && fileToMove.exists() ) 879 { 880 File newLocation = new File( directory, fileToMove.getName() ); 881 moveTempToTarget( fileToMove, newLocation ); 882 } 883 } 884 885 /** 886 * <p> 887 * Quietly transfer the checksum file from the remote repository to the local file. 888 * </p> 889 * 890 * @param wagon the wagon instance (should already be connected) to use. 891 * @param remoteRepository the remote repository to transfer from. 892 * @param remotePath the remote path to the resource to get. 893 * @param repository the managed repository that will hold the file 894 * @param resource the local file that should contain the downloaded contents 895 * @param tmpDirectory the temporary directory to download to 896 * @param ext the type of checksum to transfer (example: ".md5" or ".sha1") 897 * @throws ProxyException if copying the downloaded file into place did not succeed. 898 */ 899 private void transferChecksum( Wagon wagon, RemoteRepositoryContent remoteRepository, String remotePath, 900 ManagedRepositoryContent repository, File resource, File tmpDirectory, String ext, 901 File destFile ) 902 throws ProxyException 903 { 904 String url = remoteRepository.getURL().getUrl() + remotePath + ext; 905 906 // Transfer checksum does not use the policy. 907 if ( urlFailureCache.hasFailedBefore( url ) ) 908 { 909 return; 910 } 911 912 try 913 { 914 transferSimpleFile( wagon, remoteRepository, remotePath + ext, repository, resource, destFile ); 915 log.debug( "Checksum {} Downloaded: {} to move to {}", url, destFile, resource ); 916 } 917 catch ( NotFoundException e ) 918 { 919 urlFailureCache.cacheFailure( url ); 920 log.debug( "Transfer failed, checksum not found: {}", url ); 921 // Consume it, do not pass this on. 922 } 923 catch ( NotModifiedException e ) 924 { 925 log.debug( "Transfer skipped, checksum not modified: {}", url ); 926 // Consume it, do not pass this on. 927 } 928 catch ( ProxyException e ) 929 { 930 urlFailureCache.cacheFailure( url ); 931 log.warn( "Transfer failed on checksum: " + url + " : " + e.getMessage(), e ); 932 // Critical issue, pass it on. 933 throw e; 934 } 935 } 936 937 /** 938 * Perform the transfer of the remote file to the local file specified. 939 * 940 * @param wagon the wagon instance to use. 941 * @param remoteRepository the remote repository to use 942 * @param remotePath the remote path to attempt to get 943 * @param repository the managed repository that will hold the file 944 * @param origFile the local file to save to 945 * @return The local file that was transfered. 946 * @throws ProxyException if there was a problem moving the downloaded file into place. 947 * @throws WagonException if there was a problem tranfering the file. 948 */ 949 private void transferSimpleFile( Wagon wagon, RemoteRepositoryContent remoteRepository, String remotePath, 950 ManagedRepositoryContent repository, File origFile, File destFile ) 951 throws ProxyException 952 { 953 assert ( remotePath != null ); 954 955 // Transfer the file. 956 try 957 { 958 boolean success = false; 959 960 if ( !origFile.exists() ) 961 { 962 log.debug( "Retrieving {} from {}", remotePath, remoteRepository.getRepository().getName() ); 963 wagon.get( addParameters( remotePath, remoteRepository.getRepository() ), destFile ); 964 success = true; 965 966 // You wouldn't get here on failure, a WagonException would have been thrown. 967 log.debug( "Downloaded successfully." ); 968 } 969 else 970 { 971 log.debug( "Retrieving {} from {} if updated", remotePath, remoteRepository.getRepository().getName() ); 972 success = wagon.getIfNewer( addParameters( remotePath, remoteRepository.getRepository() ), destFile, 973 origFile.lastModified() ); 974 if ( !success ) 975 { 976 throw new NotModifiedException( 977 "Not downloaded, as local file is newer than remote side: " + origFile.getAbsolutePath() ); 978 } 979 980 if ( destFile.exists() ) 981 { 982 log.debug( "Downloaded successfully." ); 983 } 984 } 985 } 986 catch ( ResourceDoesNotExistException e ) 987 { 988 throw new NotFoundException( 989 "Resource [" + remoteRepository.getURL() + "/" + remotePath + "] does not exist: " + e.getMessage(), 990 e ); 991 } 992 catch ( WagonException e ) 993 { 994 // TODO: shouldn't have to drill into the cause, but TransferFailedException is often not descriptive enough 995 996 String msg = 997 "Download failure on resource [" + remoteRepository.getURL() + "/" + remotePath + "]:" + e.getMessage(); 998 if ( e.getCause() != null ) 999 { 1000 msg += " (cause: " + e.getCause() + ")"; 1001 } 1002 throw new ProxyException( msg, e ); 1003 } 1004 } 1005 1006 /** 1007 * Apply the policies. 1008 * 1009 * @param policies the map of policies to execute. (Map of String policy keys, to {@link DownloadPolicy} objects) 1010 * @param settings the map of settings for the policies to execute. (Map of String policy keys, to String policy 1011 * setting) 1012 * @param request the request properties (utilized by the {@link DownloadPolicy#applyPolicy(String, Properties, File)} 1013 * ) 1014 * @param localFile the local file (utilized by the {@link DownloadPolicy#applyPolicy(String, Properties, File)}) 1015 */ 1016 private void validatePolicies( Map<String, ? extends DownloadPolicy> policies, Map<String, String> settings, 1017 Properties request, File localFile ) 1018 throws PolicyViolationException 1019 { 1020 for ( Entry<String, ? extends DownloadPolicy> entry : policies.entrySet() ) 1021 { 1022 // olamy with spring rolehint is now downloadPolicy#hint 1023 // so substring after last # to get the hint as with plexus 1024 String key = StringUtils.substringAfterLast( entry.getKey(), "#" ); 1025 DownloadPolicy policy = entry.getValue(); 1026 String defaultSetting = policy.getDefaultOption(); 1027 1028 String setting = StringUtils.defaultString( settings.get( key ), defaultSetting ); 1029 1030 log.debug( "Applying [{}] policy with [{}]", key, setting ); 1031 try 1032 { 1033 policy.applyPolicy( setting, request, localFile ); 1034 } 1035 catch ( PolicyConfigurationException e ) 1036 { 1037 log.error( e.getMessage(), e ); 1038 } 1039 } 1040 } 1041 1042 private void validatePolicies( Map<String, DownloadErrorPolicy> policies, Map<String, String> settings, 1043 Properties request, ArtifactReference artifact, RemoteRepositoryContent content, 1044 File localFile, Exception exception, Map<String, Exception> previousExceptions ) 1045 throws ProxyDownloadException 1046 { 1047 boolean process = true; 1048 for ( Entry<String, ? extends DownloadErrorPolicy> entry : policies.entrySet() ) 1049 { 1050 1051 // olamy with spring rolehint is now downloadPolicy#hint 1052 // so substring after last # to get the hint as with plexus 1053 String key = StringUtils.substringAfterLast( entry.getKey(), "#" ); 1054 DownloadErrorPolicy policy = entry.getValue(); 1055 String defaultSetting = policy.getDefaultOption(); 1056 String setting = StringUtils.defaultString( settings.get( key ), defaultSetting ); 1057 1058 log.debug( "Applying [{}] policy with [{}]", key, setting ); 1059 try 1060 { 1061 // all policies must approve the exception, any can cancel 1062 process = policy.applyPolicy( setting, request, localFile, exception, previousExceptions ); 1063 if ( !process ) 1064 { 1065 break; 1066 } 1067 } 1068 catch ( PolicyConfigurationException e ) 1069 { 1070 log.error( e.getMessage(), e ); 1071 } 1072 } 1073 1074 if ( process ) 1075 { 1076 // if the exception was queued, don't throw it 1077 if ( !previousExceptions.containsKey( content.getId() ) ) 1078 { 1079 throw new ProxyDownloadException( 1080 "An error occurred in downloading from the remote repository, and the policy is to fail immediately", 1081 content.getId(), exception ); 1082 } 1083 } 1084 else 1085 { 1086 // if the exception was queued, but cancelled, remove it 1087 previousExceptions.remove( content.getId() ); 1088 } 1089 1090 log.warn( 1091 "Transfer error from repository \"" + content.getRepository().getId() + "\" for artifact " + Keys.toKey( 1092 artifact ) + ", continuing to next repository. Error message: " + exception.getMessage() ); 1093 log.debug( "Full stack trace", exception ); 1094 } 1095 1096 /** 1097 * Creates a working directory 1098 * 1099 * @param repository 1100 * @return file location of working directory 1101 * @throws IOException 1102 */ 1103 private File createWorkingDirectory( ManagedRepositoryContent repository ) 1104 { 1105 return Files.createTempDir(); 1106 } 1107 1108 /** 1109 * Used to move the temporary file to its real destination. This is patterned from the way WagonManager handles its 1110 * downloaded files. 1111 * 1112 * @param temp The completed download file 1113 * @param target The final location of the downloaded file 1114 * @throws ProxyException when the temp file cannot replace the target file 1115 */ 1116 private void moveTempToTarget( File temp, File target ) 1117 throws ProxyException 1118 { 1119 if ( target.exists() && !target.delete() ) 1120 { 1121 throw new ProxyException( "Unable to overwrite existing target file: " + target.getAbsolutePath() ); 1122 } 1123 1124 target.getParentFile().mkdirs(); 1125 if ( !temp.renameTo( target ) ) 1126 { 1127 log.warn( "Unable to rename tmp file to its final name... resorting to copy command." ); 1128 1129 try 1130 { 1131 FileUtils.copyFile( temp, target ); 1132 } 1133 catch ( IOException e ) 1134 { 1135 if ( target.exists() ) 1136 { 1137 log.debug( "Tried to copy file {} to {} but file with this name already exists.", temp.getName(), 1138 target.getAbsolutePath() ); 1139 } 1140 else 1141 { 1142 throw new ProxyException( 1143 "Cannot copy tmp file " + temp.getAbsolutePath() + " to its final location", e ); 1144 } 1145 } 1146 finally 1147 { 1148 FileUtils.deleteQuietly( temp ); 1149 } 1150 } 1151 } 1152 1153 /** 1154 * Using wagon, connect to the remote repository. 1155 * 1156 * @param connector the connector configuration to utilize (for obtaining network proxy configuration from) 1157 * @param wagon the wagon instance to establish the connection on. 1158 * @param remoteRepository the remote repository to connect to. 1159 * @return true if the connection was successful. false if not connected. 1160 */ 1161 private boolean connectToRepository( ProxyConnector connector, Wagon wagon, 1162 RemoteRepositoryContent remoteRepository ) 1163 { 1164 boolean connected = false; 1165 1166 final ProxyInfo networkProxy = 1167 connector.getProxyId() == null ? null : this.networkProxyMap.get( connector.getProxyId() ); 1168 1169 if ( log.isDebugEnabled() ) 1170 { 1171 if ( networkProxy != null ) 1172 { 1173 // TODO: move to proxyInfo.toString() 1174 String msg = "Using network proxy " + networkProxy.getHost() + ":" + networkProxy.getPort() 1175 + " to connect to remote repository " + remoteRepository.getURL(); 1176 if ( networkProxy.getNonProxyHosts() != null ) 1177 { 1178 msg += "; excluding hosts: " + networkProxy.getNonProxyHosts(); 1179 } 1180 if ( StringUtils.isNotBlank( networkProxy.getUserName() ) ) 1181 { 1182 msg += "; as user: " + networkProxy.getUserName(); 1183 } 1184 log.debug( msg ); 1185 } 1186 } 1187 1188 AuthenticationInfo authInfo = null; 1189 String username = remoteRepository.getRepository().getUserName(); 1190 String password = remoteRepository.getRepository().getPassword(); 1191 1192 if ( StringUtils.isNotBlank( username ) && StringUtils.isNotBlank( password ) ) 1193 { 1194 log.debug( "Using username {} to connect to remote repository {}", username, remoteRepository.getURL() ); 1195 authInfo = new AuthenticationInfo(); 1196 authInfo.setUserName( username ); 1197 authInfo.setPassword( password ); 1198 } 1199 1200 // Convert seconds to milliseconds 1201 int timeoutInMilliseconds = remoteRepository.getRepository().getTimeout() * 1000; 1202 1203 // Set timeout read and connect 1204 // FIXME olamy having 2 config values 1205 wagon.setReadTimeout( timeoutInMilliseconds ); 1206 wagon.setTimeout( timeoutInMilliseconds ); 1207 1208 try 1209 { 1210 Repository wagonRepository = 1211 new Repository( remoteRepository.getId(), remoteRepository.getURL().toString() ); 1212 wagon.connect( wagonRepository, authInfo, networkProxy ); 1213 connected = true; 1214 } 1215 catch ( ConnectionException e ) 1216 { 1217 log.warn( "Could not connect to " + remoteRepository.getRepository().getName() + ": " + e.getMessage() ); 1218 connected = false; 1219 } 1220 catch ( AuthenticationException e ) 1221 { 1222 log.warn( "Could not connect to " + remoteRepository.getRepository().getName() + ": " + e.getMessage() ); 1223 connected = false; 1224 } 1225 1226 return connected; 1227 } 1228 1229 /** 1230 * Tests whitelist and blacklist patterns against path. 1231 * 1232 * @param path the path to test. 1233 * @param patterns the list of patterns to check. 1234 * @return true if the path matches at least 1 pattern in the provided patterns list. 1235 */ 1236 private boolean matchesPattern( String path, List<String> patterns ) 1237 { 1238 if ( CollectionUtils.isEmpty( patterns ) ) 1239 { 1240 return false; 1241 } 1242 1243 if ( !path.startsWith( "/" ) ) 1244 { 1245 path = "/" + path; 1246 } 1247 1248 for ( String pattern : patterns ) 1249 { 1250 if ( !pattern.startsWith( "/" ) ) 1251 { 1252 pattern = "/" + pattern; 1253 } 1254 1255 if ( SelectorUtils.matchPath( pattern, path, false ) ) 1256 { 1257 return true; 1258 } 1259 } 1260 1261 return false; 1262 } 1263 1264 /** 1265 * TODO: Ensure that list is correctly ordered based on configuration. See MRM-477 1266 */ 1267 public List<ProxyConnector> getProxyConnectors( ManagedRepositoryContent repository ) 1268 { 1269 synchronized ( this.proxyConnectorMap ) 1270 { 1271 List<ProxyConnector> ret = this.proxyConnectorMap.get( repository.getId() ); 1272 if ( ret == null ) 1273 { 1274 return Collections.emptyList(); 1275 } 1276 1277 Collections.sort( ret, ProxyConnectorOrderComparator.getInstance() ); 1278 return ret; 1279 } 1280 } 1281 1282 public void afterConfigurationChange( Registry registry, String propertyName, Object propertyValue ) 1283 { 1284 if ( ConfigurationNames.isNetworkProxy( propertyName ) || ConfigurationNames.isManagedRepositories( 1285 propertyName ) || ConfigurationNames.isRemoteRepositories( propertyName ) 1286 || ConfigurationNames.isProxyConnector( propertyName ) ) 1287 { 1288 initConnectorsAndNetworkProxies(); 1289 } 1290 } 1291 1292 protected String addParameters( String path, RemoteRepository remoteRepository ) 1293 { 1294 if ( remoteRepository.getExtraParameters().isEmpty() ) 1295 { 1296 return path; 1297 } 1298 1299 boolean question = false; 1300 1301 StringBuilder res = new StringBuilder( path == null ? "" : path ); 1302 1303 for ( Entry<String, String> entry : remoteRepository.getExtraParameters().entrySet() ) 1304 { 1305 if ( !question ) 1306 { 1307 res.append( '?' ).append( entry.getKey() ).append( '=' ).append( entry.getValue() ); 1308 } 1309 } 1310 1311 return res.toString(); 1312 } 1313 1314 1315 public void beforeConfigurationChange( Registry registry, String propertyName, Object propertyValue ) 1316 { 1317 /* do nothing */ 1318 } 1319 1320 public ArchivaConfiguration getArchivaConfiguration() 1321 { 1322 return archivaConfiguration; 1323 } 1324 1325 public void setArchivaConfiguration( ArchivaConfiguration archivaConfiguration ) 1326 { 1327 this.archivaConfiguration = archivaConfiguration; 1328 } 1329 1330 public RepositoryContentFactory getRepositoryFactory() 1331 { 1332 return repositoryFactory; 1333 } 1334 1335 public void setRepositoryFactory( RepositoryContentFactory repositoryFactory ) 1336 { 1337 this.repositoryFactory = repositoryFactory; 1338 } 1339 1340 public MetadataTools getMetadataTools() 1341 { 1342 return metadataTools; 1343 } 1344 1345 public void setMetadataTools( MetadataTools metadataTools ) 1346 { 1347 this.metadataTools = metadataTools; 1348 } 1349 1350 public UrlFailureCache getUrlFailureCache() 1351 { 1352 return urlFailureCache; 1353 } 1354 1355 public void setUrlFailureCache( UrlFailureCache urlFailureCache ) 1356 { 1357 this.urlFailureCache = urlFailureCache; 1358 } 1359 1360 public WagonFactory getWagonFactory() 1361 { 1362 return wagonFactory; 1363 } 1364 1365 public void setWagonFactory( WagonFactory wagonFactory ) 1366 { 1367 this.wagonFactory = wagonFactory; 1368 } 1369 1370 public Map<String, PreDownloadPolicy> getPreDownloadPolicies() 1371 { 1372 return preDownloadPolicies; 1373 } 1374 1375 public void setPreDownloadPolicies( Map<String, PreDownloadPolicy> preDownloadPolicies ) 1376 { 1377 this.preDownloadPolicies = preDownloadPolicies; 1378 } 1379 1380 public Map<String, PostDownloadPolicy> getPostDownloadPolicies() 1381 { 1382 return postDownloadPolicies; 1383 } 1384 1385 public void setPostDownloadPolicies( Map<String, PostDownloadPolicy> postDownloadPolicies ) 1386 { 1387 this.postDownloadPolicies = postDownloadPolicies; 1388 } 1389 1390 public Map<String, DownloadErrorPolicy> getDownloadErrorPolicies() 1391 { 1392 return downloadErrorPolicies; 1393 } 1394 1395 public void setDownloadErrorPolicies( Map<String, DownloadErrorPolicy> downloadErrorPolicies ) 1396 { 1397 this.downloadErrorPolicies = downloadErrorPolicies; 1398 } 1399 }