| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| package org.apache.maven.buildcache; |
| |
| import java.io.ByteArrayInputStream; |
| import java.io.ByteArrayOutputStream; |
| import java.io.File; |
| import java.io.IOException; |
| import java.lang.reflect.InvocationTargetException; |
| import java.lang.reflect.Method; |
| import java.nio.file.Files; |
| import java.nio.file.Path; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.Optional; |
| import java.util.Properties; |
| import java.util.Queue; |
| import java.util.concurrent.ConcurrentLinkedQueue; |
| import java.util.concurrent.atomic.AtomicBoolean; |
| import java.util.concurrent.atomic.AtomicReference; |
| import javax.annotation.Nonnull; |
| import javax.annotation.PreDestroy; |
| import javax.inject.Inject; |
| import javax.inject.Named; |
| import org.apache.maven.SessionScoped; |
| import org.apache.maven.buildcache.checksum.MavenProjectInput; |
| import org.apache.maven.buildcache.xml.Build; |
| import org.apache.maven.buildcache.xml.CacheConfig; |
| import org.apache.maven.buildcache.xml.CacheSource; |
| import org.apache.maven.buildcache.xml.XmlService; |
| import org.apache.maven.buildcache.xml.build.Artifact; |
| import org.apache.maven.buildcache.xml.report.CacheReport; |
| import org.apache.maven.buildcache.xml.report.ProjectReport; |
| import org.apache.maven.execution.MavenExecutionRequest; |
| import org.apache.maven.execution.MavenSession; |
| import org.apache.maven.project.MavenProject; |
| import org.apache.maven.repository.RepositorySystem; |
| import org.apache.maven.wagon.ResourceDoesNotExistException; |
| import org.apache.maven.wagon.StreamingWagon; |
| import org.apache.maven.wagon.Wagon; |
| import org.apache.maven.wagon.WagonException; |
| import org.apache.maven.wagon.authentication.AuthenticationInfo; |
| import org.apache.maven.wagon.proxy.ProxyInfo; |
| import org.apache.maven.wagon.proxy.ProxyInfoProvider; |
| import org.apache.maven.wagon.repository.Repository; |
| import org.apache.maven.wagon.repository.RepositoryPermissions; |
| import org.codehaus.plexus.component.repository.exception.ComponentLifecycleException; |
| import org.codehaus.plexus.component.repository.exception.ComponentLookupException; |
| import org.eclipse.aether.ConfigurationProperties; |
| import org.eclipse.aether.RepositoryException; |
| import org.eclipse.aether.RepositorySystemSession; |
| import org.eclipse.aether.repository.Authentication; |
| import org.eclipse.aether.repository.AuthenticationContext; |
| import org.eclipse.aether.repository.Proxy; |
| import org.eclipse.aether.repository.RemoteRepository; |
| import org.eclipse.aether.transfer.NoTransporterException; |
| import org.eclipse.aether.util.ConfigUtils; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| @SessionScoped |
| @Named( "wagon" ) |
| @SuppressWarnings( "unused" ) |
| public class WagonRemoteCacheRepository implements RemoteCacheRepository |
| { |
| |
| public static final String BUILDINFO_XML = "buildinfo.xml"; |
| |
| public static final String CACHE_REPORT_XML = "cache-report.xml"; |
| |
| private static final String CONFIG_PROP_CONFIG = "remote.caching.wagon.config"; |
| |
| private static final String CONFIG_PROP_FILE_MODE = "remote.caching.wagon.perms.fileMode"; |
| |
| private static final String CONFIG_PROP_DIR_MODE = "remote.caching.wagon.perms.dirMode"; |
| |
| private static final String CONFIG_PROP_GROUP = "remote.caching.wagon.perms.group"; |
| |
| private static final Logger LOGGER = LoggerFactory.getLogger( HttpCacheRepositoryImpl.class ); |
| |
| private final MavenSession mavenSession; |
| |
| private final XmlService xmlService; |
| |
| private final CacheConfig cacheConfig; |
| |
| private final WagonConfigurator wagonConfigurator; |
| |
| private final WagonProvider wagonProvider; |
| |
| private final RemoteRepository repository; |
| |
| private final AuthenticationContext repoAuthContext; |
| |
| private final AuthenticationContext proxyAuthContext; |
| |
| private final String wagonHint; |
| |
| private final Repository wagonRepo; |
| |
| private final AuthenticationInfo wagonAuth; |
| |
| private final ProxyInfoProvider wagonProxy; |
| |
| private final Properties headers; |
| |
| private final AtomicBoolean closed = new AtomicBoolean(); |
| |
| private final Queue<Wagon> wagons = new ConcurrentLinkedQueue<>(); |
| |
| private final AtomicReference<Optional<CacheReport>> cacheReportSupplier = new AtomicReference<>(); |
| |
| @Inject |
| public WagonRemoteCacheRepository( MavenSession mavenSession, |
| XmlService xmlService, |
| CacheConfig cacheConfig, |
| WagonProvider wagonProvider, |
| WagonConfigurator wagonConfigurator, |
| RepositorySystem repositorySystem ) |
| throws RepositoryException |
| { |
| this.xmlService = xmlService; |
| this.cacheConfig = cacheConfig; |
| this.mavenSession = mavenSession; |
| this.wagonProvider = wagonProvider; |
| this.wagonConfigurator = wagonConfigurator; |
| |
| MavenExecutionRequest request = mavenSession.getRequest(); |
| RepositorySystemSession session = mavenSession.getRepositorySession(); |
| |
| cacheConfig.initialize(); |
| |
| RemoteRepository repo = new RemoteRepository.Builder( |
| cacheConfig.getId(), "cache", cacheConfig.getUrl() ).build(); |
| RemoteRepository mirror = session.getMirrorSelector().getMirror( repo ); |
| RemoteRepository repoOrMirror = mirror != null ? mirror : repo; |
| Proxy proxy = session.getProxySelector().getProxy( repoOrMirror ); |
| Authentication auth = session.getAuthenticationSelector().getAuthentication( repoOrMirror ); |
| repository = new RemoteRepository.Builder( repoOrMirror ) |
| .setProxy( proxy ) |
| .setAuthentication( auth ) |
| .build(); |
| |
| wagonRepo = new Repository( repository.getId(), repository.getUrl() ); |
| wagonRepo.setPermissions( getPermissions( repository.getId(), session ) ); |
| |
| wagonHint = wagonRepo.getProtocol().toLowerCase( Locale.ENGLISH ); |
| if ( wagonHint.isEmpty() ) |
| { |
| throw new IllegalArgumentException( "Could not find a wagon provider for " + wagonRepo ); |
| } |
| |
| try |
| { |
| wagons.add( lookupWagon() ); |
| } |
| catch ( Exception e ) |
| { |
| LOGGER.debug( "No transport {}", e, e ); |
| throw new NoTransporterException( repository, e ); |
| } |
| |
| repoAuthContext = AuthenticationContext.forRepository( session, repository ); |
| proxyAuthContext = AuthenticationContext.forProxy( session, repository ); |
| |
| wagonAuth = getAuthenticationInfo( repoAuthContext ); |
| wagonProxy = getProxy( repository, proxyAuthContext ); |
| |
| this.headers = new Properties(); |
| this.headers.put( "User-Agent", ConfigUtils.getString( session, ConfigurationProperties.DEFAULT_USER_AGENT, |
| ConfigurationProperties.USER_AGENT ) ); |
| Map<?, ?> headers = ConfigUtils.getMap( session, null, |
| ConfigurationProperties.HTTP_HEADERS + "." + repository.getId(), |
| ConfigurationProperties.HTTP_HEADERS ); |
| if ( headers != null ) |
| { |
| this.headers.putAll( headers ); |
| } |
| } |
| |
| @PreDestroy |
| void destroy() |
| { |
| if ( closed.compareAndSet( false, true ) ) |
| { |
| AuthenticationContext.close( repoAuthContext ); |
| AuthenticationContext.close( proxyAuthContext ); |
| |
| for ( Wagon wagon = wagons.poll(); wagon != null; wagon = wagons.poll() ) |
| { |
| disconnectWagon( wagon ); |
| releaseWagon( wagon ); |
| } |
| } |
| } |
| |
| private static RepositoryPermissions getPermissions( String repoId, RepositorySystemSession session ) |
| { |
| RepositoryPermissions result = null; |
| RepositoryPermissions perms = new RepositoryPermissions(); |
| String suffix = '.' + repoId; |
| String fileMode = ConfigUtils.getString( session, null, CONFIG_PROP_FILE_MODE + suffix ); |
| if ( fileMode != null ) |
| { |
| perms.setFileMode( fileMode ); |
| result = perms; |
| } |
| String dirMode = ConfigUtils.getString( session, null, CONFIG_PROP_DIR_MODE + suffix ); |
| if ( dirMode != null ) |
| { |
| perms.setDirectoryMode( dirMode ); |
| result = perms; |
| } |
| String group = ConfigUtils.getString( session, null, CONFIG_PROP_GROUP + suffix ); |
| if ( group != null ) |
| { |
| perms.setGroup( group ); |
| result = perms; |
| } |
| return result; |
| } |
| |
| private AuthenticationInfo getAuthenticationInfo( final AuthenticationContext authContext ) |
| { |
| AuthenticationInfo auth = null; |
| |
| if ( authContext != null ) |
| { |
| auth = new AuthenticationInfo() |
| { |
| |
| @Override |
| public String getUserName() |
| { |
| return authContext.get( AuthenticationContext.USERNAME ); |
| } |
| |
| @Override |
| public String getPassword() |
| { |
| return authContext.get( AuthenticationContext.PASSWORD ); |
| } |
| |
| @Override |
| public String getPrivateKey() |
| { |
| return authContext.get( AuthenticationContext.PRIVATE_KEY_PATH ); |
| } |
| |
| @Override |
| public String getPassphrase() |
| { |
| return authContext.get( AuthenticationContext.PRIVATE_KEY_PASSPHRASE ); |
| } |
| }; |
| } |
| |
| return auth; |
| } |
| |
| private ProxyInfoProvider getProxy( RemoteRepository repository, final AuthenticationContext authContext ) |
| { |
| ProxyInfoProvider proxy = null; |
| |
| Proxy p = repository.getProxy(); |
| if ( p != null ) |
| { |
| final ProxyInfo prox; |
| if ( authContext != null ) |
| { |
| prox = new ProxyInfo() |
| { |
| |
| @Override |
| public String getUserName() |
| { |
| return authContext.get( AuthenticationContext.USERNAME ); |
| } |
| |
| @Override |
| public String getPassword() |
| { |
| return authContext.get( AuthenticationContext.PASSWORD ); |
| } |
| |
| @Override |
| public String getNtlmDomain() |
| { |
| return authContext.get( AuthenticationContext.NTLM_DOMAIN ); |
| } |
| |
| @Override |
| public String getNtlmHost() |
| { |
| return authContext.get( AuthenticationContext.NTLM_WORKSTATION ); |
| } |
| }; |
| } |
| else |
| { |
| prox = new ProxyInfo(); |
| } |
| prox.setType( p.getType() ); |
| prox.setHost( p.getHost() ); |
| prox.setPort( p.getPort() ); |
| |
| proxy = protocol -> prox; |
| } |
| |
| return proxy; |
| } |
| |
| @Override |
| @Nonnull |
| public Optional<Build> findBuild( CacheContext context ) throws IOException |
| { |
| final String resourceUrl = doGetResourceUrl( context, BUILDINFO_XML ); |
| return doGet( resourceUrl ) |
| .map( content -> new Build( xmlService.loadBuild( content ), CacheSource.REMOTE ) ); |
| } |
| |
| @Override |
| public boolean getArtifactContent( CacheContext context, Artifact artifact, Path target ) |
| throws IOException |
| { |
| return doGet( doGetResourceUrl( context, artifact.getFileName() ), target ); |
| } |
| |
| @Override |
| public void saveBuildInfo( CacheResult cacheResult, Build build ) |
| throws IOException |
| { |
| final String resourceUrl = doGetResourceUrl( cacheResult.getContext(), BUILDINFO_XML ); |
| doPut( resourceUrl, xmlService.toBytes( build.getDto() ) ); |
| } |
| |
| @Override |
| public void saveCacheReport( String buildId, MavenSession session, CacheReport cacheReport ) throws IOException |
| { |
| MavenProject rootProject = session.getTopLevelProject(); |
| final String resourceUrl = doGetResourceUrl( CACHE_REPORT_XML, rootProject, buildId ); |
| doPut( resourceUrl, xmlService.toBytes( cacheReport ) ); |
| } |
| |
| @Override |
| public void saveArtifactFile( CacheResult cacheResult, |
| org.apache.maven.artifact.Artifact artifact ) throws IOException |
| { |
| final String resourceUrl = doGetResourceUrl( cacheResult.getContext(), CacheUtils.normalizedName( artifact ) ); |
| doPut( resourceUrl, artifact.getFile().toPath() ); |
| } |
| |
| @Override |
| public String getResourceUrl( CacheContext context, String filename ) |
| { |
| String base = cacheConfig.getUrl(); |
| return base.endsWith( "/" ) |
| ? base + doGetResourceUrl( context, filename ) |
| : base + "/" + doGetResourceUrl( context, filename ); |
| } |
| |
| public String doGetResourceUrl( CacheContext context, String filename ) |
| { |
| return doGetResourceUrl( filename, context.getProject(), context.getInputInfo().getChecksum() ); |
| } |
| |
| private String doGetResourceUrl( String filename, MavenProject project, String checksum ) |
| { |
| return doGetResourceUrl( filename, project.getGroupId(), project.getArtifactId(), checksum ); |
| } |
| |
| private String doGetResourceUrl( String filename, String groupId, String artifactId, String checksum ) |
| { |
| return MavenProjectInput.CACHE_IMPLEMENTATION_VERSION + "/" + groupId + "/" |
| + artifactId + "/" + checksum + "/" + filename; |
| } |
| |
| @Override |
| public Optional<Build> findBaselineBuild( MavenProject project ) |
| { |
| Optional<List<ProjectReport>> cachedProjectsHolder = findCacheInfo() |
| .map( CacheReport::getProjects ); |
| |
| if ( !cachedProjectsHolder.isPresent() ) |
| { |
| return Optional.empty(); |
| } |
| |
| final List<ProjectReport> projects = cachedProjectsHolder.get(); |
| final Optional<ProjectReport> projectReportHolder = projects.stream() |
| .filter( p -> project.getArtifactId().equals( p.getArtifactId() ) |
| && project.getGroupId().equals( p.getGroupId() ) ) |
| .findFirst(); |
| |
| if ( !projectReportHolder.isPresent() ) |
| { |
| return Optional.empty(); |
| } |
| |
| final ProjectReport projectReport = projectReportHolder.get(); |
| |
| String url; |
| if ( projectReport.getUrl() != null ) |
| { |
| url = projectReport.getUrl(); |
| LOGGER.info( "Retrieving baseline buildinfo: {}", url ); |
| } |
| else |
| { |
| url = doGetResourceUrl( BUILDINFO_XML, project, projectReport.getChecksum() ); |
| LOGGER.info( "Baseline project record doesn't have url, trying default location {}", url ); |
| } |
| |
| try |
| { |
| return doGet( url ) |
| .map( content -> new Build( xmlService.loadBuild( content ), CacheSource.REMOTE ) ); |
| } |
| catch ( Exception e ) |
| { |
| LOGGER.warn( "Error restoring baseline build at url: {}, skipping diff", url, e ); |
| return Optional.empty(); |
| } |
| } |
| |
| private Optional<CacheReport> findCacheInfo() |
| { |
| Optional<CacheReport> report = cacheReportSupplier.get(); |
| if ( !report.isPresent() ) |
| { |
| try |
| { |
| LOGGER.info( "Downloading baseline cache report from: {}", cacheConfig.getBaselineCacheUrl() ); |
| return doGet( cacheConfig.getBaselineCacheUrl() ).map( xmlService::loadCacheReport ); |
| } |
| catch ( Exception e ) |
| { |
| LOGGER.error( "Error downloading baseline report from: {}, skipping diff.", |
| cacheConfig.getBaselineCacheUrl(), e ); |
| report = Optional.empty(); |
| } |
| cacheReportSupplier.compareAndSet( null, report ); |
| } |
| return report; |
| } |
| |
| private void doPut( String url, Path path ) throws IOException |
| { |
| try |
| { |
| Wagon wagon = pollWagon(); |
| try |
| { |
| wagon.put( path.toFile(), url ); |
| } |
| finally |
| { |
| wagons.add( wagon ); |
| } |
| } |
| catch ( Exception e ) |
| { |
| throw new IOException( "Unable to upload resource " + url, e ); |
| } |
| } |
| |
| private void doPut( String url, byte[] data ) throws IOException |
| { |
| try |
| { |
| Wagon wagon = pollWagon(); |
| try |
| { |
| if ( wagon instanceof StreamingWagon ) |
| { |
| ( (StreamingWagon) wagon ).putFromStream( |
| new ByteArrayInputStream( data ), url, data.length, 0 ); |
| } |
| else |
| { |
| File temp = createTempFile(); |
| try |
| { |
| Files.write( temp.toPath(), data ); |
| wagon.put( temp, url ); |
| } |
| finally |
| { |
| delete( temp ); |
| } |
| } |
| } |
| finally |
| { |
| wagons.add( wagon ); |
| } |
| } |
| catch ( Exception e ) |
| { |
| throw new IOException( "Unable to upload resource " + url, e ); |
| } |
| } |
| |
| private Optional<byte[]> doGet( String url ) throws IOException |
| { |
| try |
| { |
| Wagon wagon = pollWagon(); |
| try |
| { |
| if ( wagon instanceof StreamingWagon ) |
| { |
| ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
| ( (StreamingWagon) wagon ).getToStream( url, baos ); |
| return Optional.of( baos.toByteArray() ); |
| } |
| else |
| { |
| File temp = createTempFile(); |
| try |
| { |
| wagon.get( url, temp ); |
| return Optional.of( Files.readAllBytes( temp.toPath() ) ); |
| } |
| finally |
| { |
| delete( temp ); |
| } |
| } |
| } |
| finally |
| { |
| wagons.add( wagon ); |
| } |
| } |
| catch ( ResourceDoesNotExistException e ) |
| { |
| return Optional.empty(); |
| } |
| catch ( Exception e ) |
| { |
| throw new IOException( "Unable to download resource " + url, e ); |
| } |
| } |
| |
| private boolean doGet( String url, Path target ) throws IOException |
| { |
| try |
| { |
| Wagon wagon = pollWagon(); |
| try |
| { |
| wagon.get( url, target.toFile() ); |
| return true; |
| } |
| finally |
| { |
| wagons.add( wagon ); |
| } |
| } |
| catch ( ResourceDoesNotExistException e ) |
| { |
| return false; |
| } |
| catch ( Exception e ) |
| { |
| throw new IOException( "Unable to download resource " + url, e ); |
| } |
| } |
| |
| private File createTempFile() throws IOException |
| { |
| return File.createTempFile( "maven-build-cache-", ".temp" ); |
| } |
| |
| @SuppressWarnings( "ResultOfMethodCallIgnored" ) |
| private void delete( File temp ) |
| { |
| temp.delete(); |
| } |
| |
| private Wagon lookupWagon() throws ComponentLookupException |
| { |
| return wagonProvider.lookup( wagonHint ); |
| } |
| |
| private void releaseWagon( Wagon wagon ) |
| { |
| try |
| { |
| wagonProvider.release( wagon ); |
| } |
| catch ( ComponentLifecycleException e ) |
| { |
| // ignore |
| } |
| } |
| |
| private void connectWagon( Wagon wagon ) |
| throws WagonException |
| { |
| if ( !headers.isEmpty() ) |
| { |
| try |
| { |
| Method setHttpHeaders = wagon.getClass().getMethod( "setHttpHeaders", Properties.class ); |
| setHttpHeaders.invoke( wagon, headers ); |
| } |
| catch ( NoSuchMethodException e ) |
| { |
| // normal for non-http wagons |
| } |
| catch ( InvocationTargetException | IllegalAccessException | RuntimeException e ) |
| { |
| LOGGER.debug( "Could not set user agent for Wagon {}", wagon.getClass().getName(), e ); |
| } |
| } |
| |
| RepositorySystemSession session = mavenSession.getRepositorySession(); |
| int connectTimeout = ConfigUtils.getInteger( session, ConfigurationProperties.DEFAULT_CONNECT_TIMEOUT, |
| ConfigurationProperties.CONNECT_TIMEOUT ); |
| int requestTimeout = ConfigUtils.getInteger( session, ConfigurationProperties.DEFAULT_REQUEST_TIMEOUT, |
| ConfigurationProperties.REQUEST_TIMEOUT ); |
| |
| wagon.setTimeout( Math.max( Math.max( connectTimeout, requestTimeout ), 0 ) ); |
| |
| wagon.setInteractive( ConfigUtils.getBoolean( session, ConfigurationProperties.DEFAULT_INTERACTIVE, |
| ConfigurationProperties.INTERACTIVE ) ); |
| |
| Object configuration = ConfigUtils.getObject( session, null, |
| CONFIG_PROP_CONFIG + "." + repository.getId() ); |
| if ( configuration != null && wagonConfigurator != null ) |
| { |
| try |
| { |
| wagonConfigurator.configure( wagon, configuration ); |
| } |
| catch ( Exception e ) |
| { |
| LOGGER.warn( "Could not apply configuration for {} to Wagon {}", |
| repository.getId(), wagon.getClass().getName(), e ); |
| } |
| } |
| |
| wagon.connect( wagonRepo, wagonAuth, wagonProxy ); |
| } |
| |
| private void disconnectWagon( Wagon wagon ) |
| { |
| try |
| { |
| if ( wagon != null ) |
| { |
| wagon.disconnect(); |
| } |
| } |
| catch ( WagonException e ) |
| { |
| LOGGER.debug( "Could not disconnect Wagon {}", wagon, e ); |
| } |
| } |
| |
| private Wagon pollWagon() |
| throws Exception |
| { |
| Wagon wagon = wagons.poll(); |
| |
| if ( wagon == null ) |
| { |
| try |
| { |
| wagon = lookupWagon(); |
| connectWagon( wagon ); |
| } |
| catch ( Exception e ) |
| { |
| releaseWagon( wagon ); |
| throw e; |
| } |
| } |
| else if ( wagon.getRepository() == null ) |
| { |
| try |
| { |
| connectWagon( wagon ); |
| } |
| catch ( WagonException e ) |
| { |
| wagons.add( wagon ); |
| throw e; |
| } |
| } |
| |
| return wagon; |
| } |
| |
| } |