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