001    package org.apache.archiva.metadata.repository.storage.maven2;
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.beans.ManagedRepository;
024    import org.apache.archiva.admin.model.beans.NetworkProxy;
025    import org.apache.archiva.admin.model.beans.RemoteRepository;
026    import org.apache.archiva.common.utils.VersionUtil;
027    import org.apache.archiva.maven2.metadata.MavenMetadataReader;
028    import org.apache.archiva.metadata.repository.storage.RepositoryPathTranslator;
029    import org.apache.archiva.model.ArchivaRepositoryMetadata;
030    import org.apache.archiva.model.SnapshotVersion;
031    import org.apache.archiva.proxy.common.WagonFactory;
032    import org.apache.archiva.proxy.common.WagonFactoryException;
033    import org.apache.archiva.proxy.common.WagonFactoryRequest;
034    import org.apache.archiva.xml.XMLException;
035    import org.apache.commons.io.FileUtils;
036    import org.apache.commons.lang.StringUtils;
037    import org.apache.maven.model.Repository;
038    import org.apache.maven.model.building.FileModelSource;
039    import org.apache.maven.model.building.ModelSource;
040    import org.apache.maven.model.resolution.InvalidRepositoryException;
041    import org.apache.maven.model.resolution.ModelResolver;
042    import org.apache.maven.model.resolution.UnresolvableModelException;
043    import org.apache.maven.wagon.ConnectionException;
044    import org.apache.maven.wagon.ResourceDoesNotExistException;
045    import org.apache.maven.wagon.TransferFailedException;
046    import org.apache.maven.wagon.Wagon;
047    import org.apache.maven.wagon.authentication.AuthenticationException;
048    import org.apache.maven.wagon.authentication.AuthenticationInfo;
049    import org.apache.maven.wagon.authorization.AuthorizationException;
050    import org.apache.maven.wagon.proxy.ProxyInfo;
051    import org.slf4j.Logger;
052    import org.slf4j.LoggerFactory;
053    
054    import java.io.File;
055    import java.io.IOException;
056    import java.util.List;
057    import java.util.Map;
058    
059    public class RepositoryModelResolver
060        implements ModelResolver
061    {
062        private File basedir;
063    
064        private RepositoryPathTranslator pathTranslator;
065    
066        private WagonFactory wagonFactory;
067    
068        private List<RemoteRepository> remoteRepositories;
069    
070        private ManagedRepository targetRepository;
071    
072        private static final Logger log = LoggerFactory.getLogger( RepositoryModelResolver.class );
073    
074        private static final String METADATA_FILENAME = "maven-metadata.xml";
075    
076        // key/value: remote repo ID/network proxy
077        Map<String, NetworkProxy> networkProxyMap;
078    
079        private ManagedRepository managedRepository;
080    
081        public RepositoryModelResolver( File basedir, RepositoryPathTranslator pathTranslator )
082        {
083            this.basedir = basedir;
084    
085            this.pathTranslator = pathTranslator;
086        }
087    
088        public RepositoryModelResolver( ManagedRepository managedRepository, RepositoryPathTranslator pathTranslator,
089                                        WagonFactory wagonFactory, List<RemoteRepository> remoteRepositories,
090                                        Map<String, NetworkProxy> networkProxiesMap, ManagedRepository targetRepository )
091        {
092            this( new File( managedRepository.getLocation() ), pathTranslator );
093    
094            this.managedRepository = managedRepository;
095    
096            this.wagonFactory = wagonFactory;
097    
098            this.remoteRepositories = remoteRepositories;
099    
100            this.networkProxyMap = networkProxiesMap;
101    
102            this.targetRepository = targetRepository;
103        }
104    
105        public ModelSource resolveModel( String groupId, String artifactId, String version )
106            throws UnresolvableModelException
107        {
108            String filename = artifactId + "-" + version + ".pom";
109            // TODO: we need to convert 1.0-20091120.112233-1 type paths to baseVersion for the below call - add a test
110    
111            File model = pathTranslator.toFile( basedir, groupId, artifactId, version, filename );
112    
113            if ( !model.exists() )
114            {
115                /**
116                 *
117                 */
118                // is a SNAPSHOT ? so we can try to find locally before asking remote repositories.
119                if ( StringUtils.contains( version, VersionUtil.SNAPSHOT ) )
120                {
121                    File localSnapshotModel = findTimeStampedSnapshotPom( groupId, artifactId, version, model.getParent() );
122                    if ( localSnapshotModel != null )
123                    {
124                        return new FileModelSource( localSnapshotModel );
125                    }
126    
127                }
128    
129                for ( RemoteRepository remoteRepository : remoteRepositories )
130                {
131                    try
132                    {
133                        boolean success = getModelFromProxy( remoteRepository, groupId, artifactId, version, filename );
134                        if ( success && model.exists() )
135                        {
136                            log.info( "Model '{}' successfully retrieved from remote repository '{}'",
137                                      model.getAbsolutePath(), remoteRepository.getId() );
138                            break;
139                        }
140                    }
141                    catch ( ResourceDoesNotExistException e )
142                    {
143                        log.info(
144                            "An exception was caught while attempting to retrieve model '{}' from remote repository '{}'.Reason:{}",
145                            new Object[]{ model.getAbsolutePath(), remoteRepository.getId(), e.getMessage() } );
146                    }
147                    catch ( Exception e )
148                    {
149                        log.warn(
150                            "An exception was caught while attempting to retrieve model '{}' from remote repository '{}'.Reason:{}",
151                            new Object[]{ model.getAbsolutePath(), remoteRepository.getId(), e.getMessage() } );
152    
153                        continue;
154                    }
155                }
156            }
157    
158            return new FileModelSource( model );
159        }
160    
161        protected File findTimeStampedSnapshotPom( String groupId, String artifactId, String version,
162                                                   String parentDirectory )
163        {
164    
165            // reading metadata if there
166            File mavenMetadata = new File( parentDirectory, METADATA_FILENAME );
167            if ( mavenMetadata.exists() )
168            {
169                try
170                {
171                    ArchivaRepositoryMetadata archivaRepositoryMetadata = MavenMetadataReader.read( mavenMetadata );
172                    SnapshotVersion snapshotVersion = archivaRepositoryMetadata.getSnapshotVersion();
173                    if ( snapshotVersion != null )
174                    {
175                        String lastVersion = snapshotVersion.getTimestamp();
176                        int buildNumber = snapshotVersion.getBuildNumber();
177                        String snapshotPath =
178                            StringUtils.replaceChars( groupId, '.', '/' ) + '/' + artifactId + '/' + version + '/'
179                                + artifactId + '-' + StringUtils.remove( version, "-SNAPSHOT" ) + '-' + lastVersion + '-'
180                                + buildNumber + ".pom";
181    
182                        log.debug( "use snapshot path {} for maven coordinate {}:{}:{}", snapshotPath, groupId, artifactId,
183                                   version );
184    
185                        File model = new File( basedir, snapshotPath );
186                        //model = pathTranslator.toFile( basedir, groupId, artifactId, lastVersion, filename );
187                        if ( model.exists() )
188                        {
189                            return model;
190                        }
191                    }
192                }
193                catch ( XMLException e )
194                {
195                    log.warn( "fail to read {}, {}", mavenMetadata.getAbsolutePath(), e.getCause() );
196                }
197            }
198    
199            return null;
200        }
201    
202        public void addRepository( Repository repository )
203            throws InvalidRepositoryException
204        {
205            // we just ignore repositories outside of the current one for now
206            // TODO: it'd be nice to look them up from Archiva's set, but we want to do that by URL / mapping, not just the
207            //       ID since they will rarely match
208        }
209    
210        public ModelResolver newCopy()
211        {
212            return new RepositoryModelResolver( basedir, pathTranslator );
213        }
214    
215        // FIXME: we need to do some refactoring, we cannot re-use the proxy components of archiva-proxy in maven2-repository
216        // because it's causing a cyclic dependency
217        private boolean getModelFromProxy( RemoteRepository remoteRepository, String groupId, String artifactId,
218                                           String version, String filename )
219            throws AuthorizationException, TransferFailedException, ResourceDoesNotExistException, WagonFactoryException,
220            XMLException
221        {
222            boolean success = false;
223            File tmpMd5 = null;
224            File tmpSha1 = null;
225            File tmpResource = null;
226            String artifactPath = pathTranslator.toPath( groupId, artifactId, version, filename );
227            File resource = new File( targetRepository.getLocation(), artifactPath );
228    
229            File workingDirectory = createWorkingDirectory( targetRepository.getLocation() );
230            try
231            {
232                Wagon wagon = null;
233                try
234                {
235                    String protocol = getProtocol( remoteRepository.getUrl() );
236                    final NetworkProxy networkProxy = this.networkProxyMap.get( remoteRepository.getId() );
237    
238                    wagon = wagonFactory.getWagon(
239                        new WagonFactoryRequest( "wagon#" + protocol, remoteRepository.getExtraHeaders() ).networkProxy(
240                            networkProxy ) );
241    
242                    if ( wagon == null )
243                    {
244                        throw new RuntimeException( "Unsupported remote repository protocol: " + protocol );
245                    }
246    
247                    boolean connected = connectToRepository( wagon, remoteRepository );
248                    if ( connected )
249                    {
250                        tmpResource = new File( workingDirectory, filename );
251    
252                        if ( VersionUtil.isSnapshot( version ) )
253                        {
254                            // get the metadata first!
255                            File tmpMetadataResource = new File( workingDirectory, METADATA_FILENAME );
256    
257                            String metadataPath =
258                                StringUtils.substringBeforeLast( artifactPath, "/" ) + "/" + METADATA_FILENAME;
259    
260                            wagon.get( addParameters( metadataPath, remoteRepository ), tmpMetadataResource );
261    
262                            log.debug( "Successfully downloaded metadata." );
263    
264                            ArchivaRepositoryMetadata metadata = MavenMetadataReader.read( tmpMetadataResource );
265    
266                            // re-adjust to timestamp if present, otherwise retain the original -SNAPSHOT filename
267                            SnapshotVersion snapshotVersion = metadata.getSnapshotVersion();
268                            String timestampVersion = version;
269                            if ( snapshotVersion != null )
270                            {
271                                timestampVersion = timestampVersion.substring( 0, timestampVersion.length()
272                                    - 8 ); // remove SNAPSHOT from end
273                                timestampVersion = timestampVersion + snapshotVersion.getTimestamp() + "-"
274                                    + snapshotVersion.getBuildNumber();
275    
276                                filename = artifactId + "-" + timestampVersion + ".pom";
277    
278                                artifactPath = pathTranslator.toPath( groupId, artifactId, version, filename );
279    
280                                log.debug( "New artifactPath :{}", artifactPath );
281                            }
282                        }
283    
284                        log.info( "Retrieving {} from {}", artifactPath, remoteRepository.getName() );
285    
286                        wagon.get( addParameters( artifactPath, remoteRepository ), tmpResource );
287    
288                        log.debug( "Downloaded successfully." );
289    
290                        tmpSha1 = transferChecksum( wagon, remoteRepository, artifactPath, tmpResource, workingDirectory,
291                                                    ".sha1" );
292                        tmpMd5 = transferChecksum( wagon, remoteRepository, artifactPath, tmpResource, workingDirectory,
293                                                   ".md5" );
294                    }
295                }
296                finally
297                {
298                    if ( wagon != null )
299                    {
300                        try
301                        {
302                            wagon.disconnect();
303                        }
304                        catch ( ConnectionException e )
305                        {
306                            log.warn( "Unable to disconnect wagon.", e );
307                        }
308                    }
309                }
310    
311                if ( resource != null )
312                {
313                    synchronized ( resource.getAbsolutePath().intern() )
314                    {
315                        File directory = resource.getParentFile();
316                        moveFileIfExists( tmpMd5, directory );
317                        moveFileIfExists( tmpSha1, directory );
318                        moveFileIfExists( tmpResource, directory );
319                        success = true;
320                    }
321                }
322            }
323            finally
324            {
325                FileUtils.deleteQuietly( workingDirectory );
326            }
327    
328            // do we still need to execute the consumers?
329    
330            return success;
331        }
332    
333        /**
334         * Using wagon, connect to the remote repository.
335         *
336         * @param wagon the wagon instance to establish the connection on.
337         * @return true if the connection was successful. false if not connected.
338         */
339        private boolean connectToRepository( Wagon wagon, RemoteRepository remoteRepository )
340        {
341            boolean connected;
342    
343            final NetworkProxy proxyConnector = this.networkProxyMap.get( remoteRepository.getId() );
344            ProxyInfo networkProxy = null;
345            if ( proxyConnector != null )
346            {
347                networkProxy = new ProxyInfo();
348                networkProxy.setType( proxyConnector.getProtocol() );
349                networkProxy.setHost( proxyConnector.getHost() );
350                networkProxy.setPort( proxyConnector.getPort() );
351                networkProxy.setUserName( proxyConnector.getUsername() );
352                networkProxy.setPassword( proxyConnector.getPassword() );
353    
354                String msg = "Using network proxy " + networkProxy.getHost() + ":" + networkProxy.getPort()
355                    + " to connect to remote repository " + remoteRepository.getUrl();
356                if ( networkProxy.getNonProxyHosts() != null )
357                {
358                    msg += "; excluding hosts: " + networkProxy.getNonProxyHosts();
359                }
360    
361                if ( StringUtils.isNotBlank( networkProxy.getUserName() ) )
362                {
363                    msg += "; as user: " + networkProxy.getUserName();
364                }
365    
366                log.debug( msg );
367            }
368    
369            AuthenticationInfo authInfo = null;
370            String username = remoteRepository.getUserName();
371            String password = remoteRepository.getPassword();
372    
373            if ( StringUtils.isNotBlank( username ) && StringUtils.isNotBlank( password ) )
374            {
375                log.debug( "Using username {} to connect to remote repository {}", username, remoteRepository.getUrl() );
376                authInfo = new AuthenticationInfo();
377                authInfo.setUserName( username );
378                authInfo.setPassword( password );
379            }
380    
381            // Convert seconds to milliseconds
382            int timeoutInMilliseconds = remoteRepository.getTimeout() * 1000;
383            // FIXME olamy having 2 config values
384            // Set timeout
385            wagon.setReadTimeout( timeoutInMilliseconds );
386            wagon.setTimeout( timeoutInMilliseconds );
387    
388            try
389            {
390                org.apache.maven.wagon.repository.Repository wagonRepository =
391                    new org.apache.maven.wagon.repository.Repository( remoteRepository.getId(), remoteRepository.getUrl() );
392                if ( networkProxy != null )
393                {
394                    wagon.connect( wagonRepository, authInfo, networkProxy );
395                }
396                else
397                {
398                    wagon.connect( wagonRepository, authInfo );
399                }
400                connected = true;
401            }
402            catch ( ConnectionException e )
403            {
404                log.error( "Could not connect to {}:{} ", remoteRepository.getName(), e.getMessage() );
405                connected = false;
406            }
407            catch ( AuthenticationException e )
408            {
409                log.error( "Could not connect to {}:{} ", remoteRepository.getName(), e.getMessage() );
410                connected = false;
411            }
412    
413            return connected;
414        }
415    
416        private File transferChecksum( Wagon wagon, RemoteRepository remoteRepository, String remotePath, File resource,
417                                       File tmpDirectory, String ext )
418            throws AuthorizationException, TransferFailedException, ResourceDoesNotExistException
419        {
420            File destFile = new File( tmpDirectory, resource.getName() + ext );
421    
422            log.info( "Retrieving {} from {}", remotePath, remoteRepository.getName() );
423    
424            wagon.get( addParameters( remotePath, remoteRepository ), destFile );
425    
426            log.debug( "Downloaded successfully." );
427    
428            return destFile;
429        }
430    
431        private String getProtocol( String url )
432        {
433            String protocol = StringUtils.substringBefore( url, ":" );
434    
435            return protocol;
436        }
437    
438        private File createWorkingDirectory( String targetRepository )
439        {
440            return Files.createTempDir();
441        }
442    
443        private void moveFileIfExists( File fileToMove, File directory )
444        {
445            if ( fileToMove != null && fileToMove.exists() )
446            {
447                File newLocation = new File( directory, fileToMove.getName() );
448                if ( newLocation.exists() && !newLocation.delete() )
449                {
450                    throw new RuntimeException(
451                        "Unable to overwrite existing target file: " + newLocation.getAbsolutePath() );
452                }
453    
454                newLocation.getParentFile().mkdirs();
455                if ( !fileToMove.renameTo( newLocation ) )
456                {
457                    log.warn( "Unable to rename tmp file to its final name... resorting to copy command." );
458    
459                    try
460                    {
461                        FileUtils.copyFile( fileToMove, newLocation );
462                    }
463                    catch ( IOException e )
464                    {
465                        if ( newLocation.exists() )
466                        {
467                            log.error( "Tried to copy file {} to {} but file with this name already exists.",
468                                       fileToMove.getName(), newLocation.getAbsolutePath() );
469                        }
470                        else
471                        {
472                            throw new RuntimeException(
473                                "Cannot copy tmp file " + fileToMove.getAbsolutePath() + " to its final location", e );
474                        }
475                    }
476                    finally
477                    {
478                        FileUtils.deleteQuietly( fileToMove );
479                    }
480                }
481            }
482        }
483    
484        protected String addParameters( String path, RemoteRepository remoteRepository )
485        {
486            if ( remoteRepository.getExtraParameters().isEmpty() )
487            {
488                return path;
489            }
490    
491            boolean question = false;
492    
493            StringBuilder res = new StringBuilder( path == null ? "" : path );
494    
495            for ( Map.Entry<String, String> entry : remoteRepository.getExtraParameters().entrySet() )
496            {
497                if ( !question )
498                {
499                    res.append( '?' ).append( entry.getKey() ).append( '=' ).append( entry.getValue() );
500                }
501            }
502    
503            return res.toString();
504        }
505    }