]> source.dussan.org Git - archiva.git/blob
44d29474223442391d72b7c2601e0371bdd187de
[archiva.git] /
1 package org.apache.archiva.metadata.repository.storage.maven2;
2
3 /*
4  * Licensed to the Apache Software Foundation (ASF) under one
5  * or more contributor license agreements.  See the NOTICE file
6  * distributed with this work for additional information
7  * regarding copyright ownership.  The ASF licenses this file
8  * to you under the Apache License, Version 2.0 (the
9  * "License"); you may not use this file except in compliance
10  * with the License.  You may obtain a copy of the License at
11  *
12  *   http://www.apache.org/licenses/LICENSE-2.0
13  *
14  * Unless required by applicable law or agreed to in writing,
15  * software distributed under the License is distributed on an
16  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17  * KIND, either express or implied.  See the License for the
18  * specific language governing permissions and limitations
19  * under the License.
20  */
21
22 import org.apache.archiva.admin.model.RepositoryAdminException;
23 import org.apache.archiva.admin.model.beans.ManagedRepository;
24 import org.apache.archiva.admin.model.beans.NetworkProxy;
25 import org.apache.archiva.admin.model.beans.ProxyConnector;
26 import org.apache.archiva.admin.model.beans.RemoteRepository;
27 import org.apache.archiva.admin.model.managed.ManagedRepositoryAdmin;
28 import org.apache.archiva.admin.model.networkproxy.NetworkProxyAdmin;
29 import org.apache.archiva.admin.model.proxyconnector.ProxyConnectorAdmin;
30 import org.apache.archiva.admin.model.remote.RemoteRepositoryAdmin;
31 import org.apache.archiva.checksum.ChecksumAlgorithm;
32 import org.apache.archiva.checksum.ChecksummedFile;
33 import org.apache.archiva.common.utils.VersionUtil;
34 import org.apache.archiva.maven2.metadata.MavenMetadataReader;
35 import org.apache.archiva.metadata.model.ArtifactMetadata;
36 import org.apache.archiva.metadata.model.ProjectMetadata;
37 import org.apache.archiva.metadata.model.ProjectVersionMetadata;
38 import org.apache.archiva.metadata.model.facets.RepositoryProblemFacet;
39 import org.apache.archiva.metadata.repository.filter.Filter;
40 import org.apache.archiva.metadata.repository.storage.ReadMetadataRequest;
41 import org.apache.archiva.metadata.repository.storage.RelocationException;
42 import org.apache.archiva.metadata.repository.storage.RepositoryPathTranslator;
43 import org.apache.archiva.metadata.repository.storage.RepositoryStorage;
44 import org.apache.archiva.metadata.repository.storage.RepositoryStorageMetadataInvalidException;
45 import org.apache.archiva.metadata.repository.storage.RepositoryStorageMetadataNotFoundException;
46 import org.apache.archiva.metadata.repository.storage.RepositoryStorageRuntimeException;
47 import org.apache.archiva.model.ArchivaRepositoryMetadata;
48 import org.apache.archiva.model.ArtifactReference;
49 import org.apache.archiva.model.SnapshotVersion;
50 import org.apache.archiva.policies.ProxyDownloadException;
51 import org.apache.archiva.proxy.common.WagonFactory;
52 import org.apache.archiva.proxy.model.RepositoryProxyConnectors;
53 import org.apache.archiva.repository.ManagedRepositoryContent;
54 import org.apache.archiva.repository.content.PathParser;
55 import org.apache.archiva.repository.layout.LayoutException;
56 import org.apache.archiva.xml.XMLException;
57 import org.apache.commons.lang.ArrayUtils;
58 import org.apache.commons.lang.StringUtils;
59 import org.apache.maven.model.CiManagement;
60 import org.apache.maven.model.Dependency;
61 import org.apache.maven.model.DistributionManagement;
62 import org.apache.maven.model.IssueManagement;
63 import org.apache.maven.model.License;
64 import org.apache.maven.model.MailingList;
65 import org.apache.maven.model.Model;
66 import org.apache.maven.model.Organization;
67 import org.apache.maven.model.Relocation;
68 import org.apache.maven.model.Scm;
69 import org.apache.maven.model.building.DefaultModelBuilderFactory;
70 import org.apache.maven.model.building.DefaultModelBuildingRequest;
71 import org.apache.maven.model.building.ModelBuilder;
72 import org.apache.maven.model.building.ModelBuildingException;
73 import org.apache.maven.model.building.ModelBuildingRequest;
74 import org.apache.maven.model.building.ModelProblem;
75 import org.apache.maven.model.io.xpp3.MavenXpp3Reader;
76 import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
77 import org.slf4j.Logger;
78 import org.slf4j.LoggerFactory;
79 import org.springframework.context.ApplicationContext;
80 import org.springframework.stereotype.Service;
81
82 import javax.annotation.PostConstruct;
83 import javax.inject.Inject;
84 import javax.inject.Named;
85 import java.io.File;
86 import java.io.FileNotFoundException;
87 import java.io.FilenameFilter;
88 import java.io.IOException;
89 import java.io.Reader;
90 import java.nio.charset.Charset;
91 import java.nio.file.Files;
92 import java.util.ArrayList;
93 import java.util.Arrays;
94 import java.util.Collection;
95 import java.util.Collections;
96 import java.util.Date;
97 import java.util.HashMap;
98 import java.util.List;
99 import java.util.Map;
100
101 /**
102  * Maven 2 repository format storage implementation. This class currently takes parameters to indicate the repository to
103  * deal with rather than being instantiated per-repository.
104  * FIXME: instantiate one per repository and allocate permanently from a factory (which can be obtained within the session).
105  * <p/>
106  * The session is passed in as an argument to obtain any necessary resources, rather than the class being instantiated
107  * within the session in the context of a single managed repository's resolution needs.
108  * <p/>
109  */
110 @Service( "repositoryStorage#maven2" )
111 public class Maven2RepositoryStorage
112     implements RepositoryStorage
113 {
114
115     private static final Logger LOGGER = LoggerFactory.getLogger( Maven2RepositoryStorage.class );
116
117     private ModelBuilder builder;
118
119     @Inject
120     private RemoteRepositoryAdmin remoteRepositoryAdmin;
121
122     @Inject
123     private ManagedRepositoryAdmin managedRepositoryAdmin;
124
125     @Inject
126     private ProxyConnectorAdmin proxyConnectorAdmin;
127
128     @Inject
129     private NetworkProxyAdmin networkProxyAdmin;
130
131     @Inject
132     @Named( "repositoryPathTranslator#maven2" )
133     private RepositoryPathTranslator pathTranslator;
134
135     @Inject
136     private WagonFactory wagonFactory;
137
138     @Inject
139     private ApplicationContext applicationContext;
140
141     @Inject
142     @Named( "pathParser#default" )
143     private PathParser pathParser;
144
145     private static final String METADATA_FILENAME_START = "maven-metadata";
146
147     private static final String METADATA_FILENAME = METADATA_FILENAME_START + ".xml";
148
149     private static final MavenXpp3Reader MAVEN_XPP_3_READER = new MavenXpp3Reader();
150
151
152     @PostConstruct
153     public void initialize()
154     {
155         builder = new DefaultModelBuilderFactory().newInstance();
156
157     }
158
159     @Override
160     public ProjectMetadata readProjectMetadata( String repoId, String namespace, String projectId )
161     {
162         // TODO: could natively implement the "shared model" concept from the browse action to avoid needing it there?
163         return null;
164     }
165
166     @Override
167     public ProjectVersionMetadata readProjectVersionMetadata( ReadMetadataRequest readMetadataRequest )
168         throws RepositoryStorageMetadataNotFoundException, RepositoryStorageMetadataInvalidException,
169         RepositoryStorageRuntimeException
170     {
171         try
172         {
173             ManagedRepository managedRepository =
174                 managedRepositoryAdmin.getManagedRepository( readMetadataRequest.getRepositoryId() );
175             String artifactVersion = readMetadataRequest.getProjectVersion();
176             // olamy: in case of browsing via the ui we can mix repos (parent of a SNAPSHOT can come from release repo)
177             if ( !readMetadataRequest.isBrowsingRequest() )
178             {
179                 if ( VersionUtil.isSnapshot( artifactVersion ) )
180                 {
181                     // skygo trying to improve speed by honoring managed configuration MRM-1658
182                     if ( managedRepository.isReleases() && !managedRepository.isSnapshots() )
183                     {
184                         throw new RepositoryStorageRuntimeException( "lookforsnaponreleaseonly",
185                                                                      "managed repo is configured for release only" );
186                     }
187                 }
188                 else
189                 {
190                     if ( !managedRepository.isReleases() && managedRepository.isSnapshots() )
191                     {
192                         throw new RepositoryStorageRuntimeException( "lookforsreleaseonsneponly",
193                                                                      "managed repo is configured for snapshot only" );
194                     }
195                 }
196             }
197             File basedir = new File( managedRepository.getLocation() );
198             if ( VersionUtil.isSnapshot( artifactVersion ) )
199             {
200                 File metadataFile = pathTranslator.toFile( basedir, readMetadataRequest.getNamespace(),
201                                                            readMetadataRequest.getProjectId(), artifactVersion,
202                                                            METADATA_FILENAME );
203                 try
204                 {
205                     ArchivaRepositoryMetadata metadata = MavenMetadataReader.read( metadataFile );
206
207                     // re-adjust to timestamp if present, otherwise retain the original -SNAPSHOT filename
208                     SnapshotVersion snapshotVersion = metadata.getSnapshotVersion();
209                     if ( snapshotVersion != null )
210                     {
211                         artifactVersion =
212                             artifactVersion.substring( 0, artifactVersion.length() - 8 ); // remove SNAPSHOT from end
213                         artifactVersion =
214                             artifactVersion + snapshotVersion.getTimestamp() + "-" + snapshotVersion.getBuildNumber();
215                     }
216                 }
217                 catch ( XMLException e )
218                 {
219                     // unable to parse metadata - LOGGER it, and continue with the version as the original SNAPSHOT version
220                     LOGGER.warn( "Invalid metadata: {} - {}", metadataFile, e.getMessage() );
221                 }
222             }
223
224             // TODO: won't work well with some other layouts, might need to convert artifact parts to ID by path translator
225             String id = readMetadataRequest.getProjectId() + "-" + artifactVersion + ".pom";
226             File file =
227                 pathTranslator.toFile( basedir, readMetadataRequest.getNamespace(), readMetadataRequest.getProjectId(),
228                                        readMetadataRequest.getProjectVersion(), id );
229
230             if ( !file.exists() )
231             {
232                 // metadata could not be resolved
233                 throw new RepositoryStorageMetadataNotFoundException(
234                     "The artifact's POM file '" + file.getAbsolutePath() + "' was missing" );
235             }
236
237             // TODO: this is a workaround until we can properly resolve using proxies as well - this doesn't cache
238             //       anything locally!
239             List<RemoteRepository> remoteRepositories = new ArrayList<>();
240             Map<String, NetworkProxy> networkProxies = new HashMap<>();
241
242             Map<String, List<ProxyConnector>> proxyConnectorsMap = proxyConnectorAdmin.getProxyConnectorAsMap();
243             List<ProxyConnector> proxyConnectors = proxyConnectorsMap.get( readMetadataRequest.getRepositoryId() );
244             if ( proxyConnectors != null )
245             {
246                 for ( ProxyConnector proxyConnector : proxyConnectors )
247                 {
248                     RemoteRepository remoteRepoConfig =
249                         remoteRepositoryAdmin.getRemoteRepository( proxyConnector.getTargetRepoId() );
250
251                     if ( remoteRepoConfig != null )
252                     {
253                         remoteRepositories.add( remoteRepoConfig );
254
255                         NetworkProxy networkProxyConfig =
256                             networkProxyAdmin.getNetworkProxy( proxyConnector.getProxyId() );
257
258                         if ( networkProxyConfig != null )
259                         {
260                             // key/value: remote repo ID/proxy info
261                             networkProxies.put( proxyConnector.getTargetRepoId(), networkProxyConfig );
262                         }
263                     }
264                 }
265             }
266
267             // That's a browsing request so we can a mix of SNAPSHOT and release artifacts (especially with snapshots which
268             // can have released parent pom
269             if ( readMetadataRequest.isBrowsingRequest() )
270             {
271                 remoteRepositories.addAll( remoteRepositoryAdmin.getRemoteRepositories() );
272             }
273
274             ModelBuildingRequest req =
275                 new DefaultModelBuildingRequest().setProcessPlugins( false ).setPomFile( file ).setTwoPhaseBuilding(
276                     false ).setValidationLevel( ModelBuildingRequest.VALIDATION_LEVEL_MINIMAL );
277
278             //MRM-1607. olamy this will resolve jdk profiles on the current running archiva jvm
279             req.setSystemProperties( System.getProperties() );
280
281             // MRM-1411
282             req.setModelResolver(
283                 new RepositoryModelResolver( managedRepository, pathTranslator, wagonFactory, remoteRepositories,
284                                              networkProxies, managedRepository ) );
285
286             Model model;
287             try
288             {
289                 model = builder.build( req ).getEffectiveModel();
290             }
291             catch ( ModelBuildingException e )
292             {
293                 String msg = "The artifact's POM file '" + file + "' was invalid: " + e.getMessage();
294
295                 List<ModelProblem> modelProblems = e.getProblems();
296                 for ( ModelProblem problem : modelProblems )
297                 {
298                     // MRM-1411, related to MRM-1335
299                     // this means that the problem was that the parent wasn't resolved!
300                     // olamy really hackhish but fail with java profile so use error message
301                     // || ( StringUtils.startsWith( problem.getMessage(), "Failed to determine Java version for profile" ) )
302                     // but setTwoPhaseBuilding(true) fix that
303                     if ( ( problem.getException() instanceof FileNotFoundException && e.getModelId() != null &&
304                         !e.getModelId().equals( problem.getModelId() ) ) )
305                     {
306                         LOGGER.warn( "The artifact's parent POM file '{}' cannot be resolved. "
307                                          + "Using defaults for project version metadata..", file );
308
309                         ProjectVersionMetadata metadata = new ProjectVersionMetadata();
310                         metadata.setId( readMetadataRequest.getProjectVersion() );
311
312                         MavenProjectFacet facet = new MavenProjectFacet();
313                         facet.setGroupId( readMetadataRequest.getNamespace() );
314                         facet.setArtifactId( readMetadataRequest.getProjectId() );
315                         facet.setPackaging( "jar" );
316                         metadata.addFacet( facet );
317
318                         String errMsg =
319                             "Error in resolving artifact's parent POM file. " + ( problem.getException() == null
320                                 ? problem.getMessage()
321                                 : problem.getException().getMessage() );
322                         RepositoryProblemFacet repoProblemFacet = new RepositoryProblemFacet();
323                         repoProblemFacet.setRepositoryId( readMetadataRequest.getRepositoryId() );
324                         repoProblemFacet.setId( readMetadataRequest.getRepositoryId() );
325                         repoProblemFacet.setMessage( errMsg );
326                         repoProblemFacet.setProblem( errMsg );
327                         repoProblemFacet.setProject( readMetadataRequest.getProjectId() );
328                         repoProblemFacet.setVersion( readMetadataRequest.getProjectVersion() );
329                         repoProblemFacet.setNamespace( readMetadataRequest.getNamespace() );
330
331                         metadata.addFacet( repoProblemFacet );
332
333                         return metadata;
334                     }
335                 }
336
337                 throw new RepositoryStorageMetadataInvalidException( "invalid-pom", msg, e );
338             }
339
340             // Check if the POM is in the correct location
341             boolean correctGroupId = readMetadataRequest.getNamespace().equals( model.getGroupId() );
342             boolean correctArtifactId = readMetadataRequest.getProjectId().equals( model.getArtifactId() );
343             boolean correctVersion = readMetadataRequest.getProjectVersion().equals( model.getVersion() );
344             if ( !correctGroupId || !correctArtifactId || !correctVersion )
345             {
346                 StringBuilder message = new StringBuilder( "Incorrect POM coordinates in '" + file + "':" );
347                 if ( !correctGroupId )
348                 {
349                     message.append( "\nIncorrect group ID: " ).append( model.getGroupId() );
350                 }
351                 if ( !correctArtifactId )
352                 {
353                     message.append( "\nIncorrect artifact ID: " ).append( model.getArtifactId() );
354                 }
355                 if ( !correctVersion )
356                 {
357                     message.append( "\nIncorrect version: " ).append( model.getVersion() );
358                 }
359
360                 throw new RepositoryStorageMetadataInvalidException( "mislocated-pom", message.toString() );
361             }
362
363             ProjectVersionMetadata metadata = new ProjectVersionMetadata();
364             metadata.setCiManagement( convertCiManagement( model.getCiManagement() ) );
365             metadata.setDescription( model.getDescription() );
366             metadata.setId( readMetadataRequest.getProjectVersion() );
367             metadata.setIssueManagement( convertIssueManagement( model.getIssueManagement() ) );
368             metadata.setLicenses( convertLicenses( model.getLicenses() ) );
369             metadata.setMailingLists( convertMailingLists( model.getMailingLists() ) );
370             metadata.setDependencies( convertDependencies( model.getDependencies() ) );
371             metadata.setName( model.getName() );
372             metadata.setOrganization( convertOrganization( model.getOrganization() ) );
373             metadata.setScm( convertScm( model.getScm() ) );
374             metadata.setUrl( model.getUrl() );
375
376             MavenProjectFacet facet = new MavenProjectFacet();
377             facet.setGroupId( model.getGroupId() != null ? model.getGroupId() : model.getParent().getGroupId() );
378             facet.setArtifactId( model.getArtifactId() );
379             facet.setPackaging( model.getPackaging() );
380             if ( model.getParent() != null )
381             {
382                 MavenProjectParent parent = new MavenProjectParent();
383                 parent.setGroupId( model.getParent().getGroupId() );
384                 parent.setArtifactId( model.getParent().getArtifactId() );
385                 parent.setVersion( model.getParent().getVersion() );
386                 facet.setParent( parent );
387             }
388             metadata.addFacet( facet );
389
390             return metadata;
391         }
392         catch ( RepositoryAdminException e )
393         {
394             throw new RepositoryStorageRuntimeException( "repo-admin", e.getMessage(), e );
395         }
396     }
397
398     public void setWagonFactory( WagonFactory wagonFactory )
399     {
400         this.wagonFactory = wagonFactory;
401     }
402
403     private List<org.apache.archiva.metadata.model.Dependency> convertDependencies( List<Dependency> dependencies )
404     {
405         List<org.apache.archiva.metadata.model.Dependency> l = new ArrayList<>();
406         for ( Dependency dependency : dependencies )
407         {
408             org.apache.archiva.metadata.model.Dependency newDependency =
409                 new org.apache.archiva.metadata.model.Dependency();
410             newDependency.setArtifactId( dependency.getArtifactId() );
411             newDependency.setClassifier( dependency.getClassifier() );
412             newDependency.setGroupId( dependency.getGroupId() );
413             newDependency.setOptional( dependency.isOptional() );
414             newDependency.setScope( dependency.getScope() );
415             newDependency.setSystemPath( dependency.getSystemPath() );
416             newDependency.setType( dependency.getType() );
417             newDependency.setVersion( dependency.getVersion() );
418             l.add( newDependency );
419         }
420         return l;
421     }
422
423     private org.apache.archiva.metadata.model.Scm convertScm( Scm scm )
424     {
425         org.apache.archiva.metadata.model.Scm newScm = null;
426         if ( scm != null )
427         {
428             newScm = new org.apache.archiva.metadata.model.Scm();
429             newScm.setConnection( scm.getConnection() );
430             newScm.setDeveloperConnection( scm.getDeveloperConnection() );
431             newScm.setUrl( scm.getUrl() );
432         }
433         return newScm;
434     }
435
436     private org.apache.archiva.metadata.model.Organization convertOrganization( Organization organization )
437     {
438         org.apache.archiva.metadata.model.Organization org = null;
439         if ( organization != null )
440         {
441             org = new org.apache.archiva.metadata.model.Organization();
442             org.setName( organization.getName() );
443             org.setUrl( organization.getUrl() );
444         }
445         return org;
446     }
447
448     private List<org.apache.archiva.metadata.model.License> convertLicenses( List<License> licenses )
449     {
450         List<org.apache.archiva.metadata.model.License> l = new ArrayList<>();
451         for ( License license : licenses )
452         {
453             org.apache.archiva.metadata.model.License newLicense = new org.apache.archiva.metadata.model.License();
454             newLicense.setName( license.getName() );
455             newLicense.setUrl( license.getUrl() );
456             l.add( newLicense );
457         }
458         return l;
459     }
460
461     private List<org.apache.archiva.metadata.model.MailingList> convertMailingLists( List<MailingList> mailingLists )
462     {
463         List<org.apache.archiva.metadata.model.MailingList> l = new ArrayList<>();
464         for ( MailingList mailingList : mailingLists )
465         {
466             org.apache.archiva.metadata.model.MailingList newMailingList =
467                 new org.apache.archiva.metadata.model.MailingList();
468             newMailingList.setName( mailingList.getName() );
469             newMailingList.setMainArchiveUrl( mailingList.getArchive() );
470             newMailingList.setPostAddress( mailingList.getPost() );
471             newMailingList.setSubscribeAddress( mailingList.getSubscribe() );
472             newMailingList.setUnsubscribeAddress( mailingList.getUnsubscribe() );
473             newMailingList.setOtherArchives( mailingList.getOtherArchives() );
474             l.add( newMailingList );
475         }
476         return l;
477     }
478
479     private org.apache.archiva.metadata.model.IssueManagement convertIssueManagement( IssueManagement issueManagement )
480     {
481         org.apache.archiva.metadata.model.IssueManagement im = null;
482         if ( issueManagement != null )
483         {
484             im = new org.apache.archiva.metadata.model.IssueManagement();
485             im.setSystem( issueManagement.getSystem() );
486             im.setUrl( issueManagement.getUrl() );
487         }
488         return im;
489     }
490
491     private org.apache.archiva.metadata.model.CiManagement convertCiManagement( CiManagement ciManagement )
492     {
493         org.apache.archiva.metadata.model.CiManagement ci = null;
494         if ( ciManagement != null )
495         {
496             ci = new org.apache.archiva.metadata.model.CiManagement();
497             ci.setSystem( ciManagement.getSystem() );
498             ci.setUrl( ciManagement.getUrl() );
499         }
500         return ci;
501     }
502
503     @Override
504     public Collection<String> listRootNamespaces( String repoId, Filter<String> filter )
505         throws RepositoryStorageRuntimeException
506     {
507         File dir = getRepositoryBasedir( repoId );
508
509         return getSortedFiles( dir, filter );
510     }
511
512     private static Collection<String> getSortedFiles( File dir, Filter<String> filter )
513     {
514         List<String> fileNames;
515         String[] files = dir.list( new DirectoryFilter( filter ) );
516         if ( files != null )
517         {
518             fileNames = new ArrayList<>( Arrays.asList( files ) );
519             Collections.sort( fileNames );
520         }
521         else
522         {
523             fileNames = Collections.emptyList();
524         }
525         return fileNames;
526     }
527
528     private File getRepositoryBasedir( String repoId )
529         throws RepositoryStorageRuntimeException
530     {
531         try
532         {
533             ManagedRepository repositoryConfiguration = managedRepositoryAdmin.getManagedRepository( repoId );
534
535             return new File( repositoryConfiguration.getLocation() );
536         }
537         catch ( RepositoryAdminException e )
538         {
539             throw new RepositoryStorageRuntimeException( "repo-admin", e.getMessage(), e );
540         }
541     }
542
543     @Override
544     public Collection<String> listNamespaces( String repoId, String namespace, Filter<String> filter )
545         throws RepositoryStorageRuntimeException
546     {
547         File dir = pathTranslator.toFile( getRepositoryBasedir( repoId ), namespace );
548
549         // scan all the directories which are potential namespaces. Any directories known to be projects are excluded
550         List<String> namespaces = new ArrayList<>();
551         File[] files = dir.listFiles( new DirectoryFilter( filter ) );
552         if ( files != null )
553         {
554             for ( File file : files )
555             {
556                 if ( !isProject( file, filter ) )
557                 {
558                     namespaces.add( file.getName() );
559                 }
560             }
561         }
562         Collections.sort( namespaces );
563         return namespaces;
564     }
565
566     @Override
567     public Collection<String> listProjects( String repoId, String namespace, Filter<String> filter )
568         throws RepositoryStorageRuntimeException
569     {
570         File dir = pathTranslator.toFile( getRepositoryBasedir( repoId ), namespace );
571
572         // scan all directories in the namespace, and only include those that are known to be projects
573         List<String> projects = new ArrayList<>();
574
575         File[] files = dir.listFiles( new DirectoryFilter( filter ) );
576         if ( files != null )
577         {
578             for ( File file : files )
579             {
580                 if ( isProject( file, filter ) )
581                 {
582                     projects.add( file.getName() );
583                 }
584             }
585         }
586         Collections.sort( projects );
587         return projects;
588     }
589
590     @Override
591     public Collection<String> listProjectVersions( String repoId, String namespace, String projectId,
592                                                    Filter<String> filter )
593         throws RepositoryStorageRuntimeException
594     {
595         File dir = pathTranslator.toFile( getRepositoryBasedir( repoId ), namespace, projectId );
596
597         // all directories in a project directory can be considered a version
598         return getSortedFiles( dir, filter );
599     }
600
601     @Override
602     public Collection<ArtifactMetadata> readArtifactsMetadata( ReadMetadataRequest readMetadataRequest )
603         throws RepositoryStorageRuntimeException
604     {
605         File dir = pathTranslator.toFile( getRepositoryBasedir( readMetadataRequest.getRepositoryId() ),
606                                           readMetadataRequest.getNamespace(), readMetadataRequest.getProjectId(),
607                                           readMetadataRequest.getProjectVersion() );
608
609         // all files that are not metadata and not a checksum / signature are considered artifacts
610         File[] files = dir.listFiles( new ArtifactDirectoryFilter( readMetadataRequest.getFilter() ) );
611
612         List<ArtifactMetadata> artifacts = new ArrayList<>();
613         if ( files != null )
614         {
615             for ( File file : files )
616             {
617                 ArtifactMetadata metadata =
618                     getArtifactFromFile( readMetadataRequest.getRepositoryId(), readMetadataRequest.getNamespace(),
619                                          readMetadataRequest.getProjectId(), readMetadataRequest.getProjectVersion(),
620                                          file );
621                 artifacts.add( metadata );
622             }
623         }
624         return artifacts;
625     }
626
627     @Override
628     public ArtifactMetadata readArtifactMetadataFromPath( String repoId, String path )
629         throws RepositoryStorageRuntimeException
630     {
631         ArtifactMetadata metadata = pathTranslator.getArtifactForPath( repoId, path );
632
633         populateArtifactMetadataFromFile( metadata, new File( getRepositoryBasedir( repoId ), path ) );
634
635         return metadata;
636     }
637
638     private ArtifactMetadata getArtifactFromFile( String repoId, String namespace, String projectId,
639                                                   String projectVersion, File file )
640     {
641         ArtifactMetadata metadata =
642             pathTranslator.getArtifactFromId( repoId, namespace, projectId, projectVersion, file.getName() );
643
644         populateArtifactMetadataFromFile( metadata, file );
645
646         return metadata;
647     }
648
649     @Override
650     public void applyServerSideRelocation( ManagedRepositoryContent managedRepository, ArtifactReference artifact )
651         throws ProxyDownloadException
652     {
653         if ( "pom".equals( artifact.getType() ) )
654         {
655             return;
656         }
657
658         // Build the artifact POM reference
659         ArtifactReference pomReference = new ArtifactReference();
660         pomReference.setGroupId( artifact.getGroupId() );
661         pomReference.setArtifactId( artifact.getArtifactId() );
662         pomReference.setVersion( artifact.getVersion() );
663         pomReference.setType( "pom" );
664
665         RepositoryProxyConnectors connectors =
666             applicationContext.getBean( "repositoryProxyConnectors#default", RepositoryProxyConnectors.class );
667
668         // Get the artifact POM from proxied repositories if needed
669         connectors.fetchFromProxies( managedRepository, pomReference );
670
671         // Open and read the POM from the managed repo
672         File pom = managedRepository.toFile( pomReference );
673
674         if ( !pom.exists() )
675         {
676             return;
677         }
678
679         try
680         {
681             // MavenXpp3Reader leaves the file open, so we need to close it ourselves.
682
683             Model model = null;
684             try (Reader reader = Files.newBufferedReader( pom.toPath(), Charset.defaultCharset() ))
685             {
686                 model = MAVEN_XPP_3_READER.read( reader );
687             }
688
689             DistributionManagement dist = model.getDistributionManagement();
690             if ( dist != null )
691             {
692                 Relocation relocation = dist.getRelocation();
693                 if ( relocation != null )
694                 {
695                     // artifact is relocated : update the repositoryPath
696                     if ( relocation.getGroupId() != null )
697                     {
698                         artifact.setGroupId( relocation.getGroupId() );
699                     }
700                     if ( relocation.getArtifactId() != null )
701                     {
702                         artifact.setArtifactId( relocation.getArtifactId() );
703                     }
704                     if ( relocation.getVersion() != null )
705                     {
706                         artifact.setVersion( relocation.getVersion() );
707                     }
708                 }
709             }
710         }
711         catch ( IOException e )
712         {
713             // Unable to read POM : ignore.
714         }
715         catch ( XmlPullParserException e )
716         {
717             // Invalid POM : ignore
718         }
719     }
720
721
722     @Override
723     public String getFilePath( String requestPath, ManagedRepository managedRepository )
724     {
725         // managedRepository can be null
726         // extract artifact reference from url
727         // groupId:artifactId:version:packaging:classifier
728         //org/apache/archiva/archiva-checksum/1.4-M4-SNAPSHOT/archiva-checksum-1.4-M4-SNAPSHOT.jar
729         String logicalResource = null;
730         String requestPathInfo = StringUtils.defaultString( requestPath );
731
732         //remove prefix ie /repository/blah becomes /blah
733         requestPathInfo = removePrefix( requestPathInfo );
734
735         // Remove prefixing slash as the repository id doesn't contain it;
736         if ( requestPathInfo.startsWith( "/" ) )
737         {
738             requestPathInfo = requestPathInfo.substring( 1 );
739         }
740
741         int slash = requestPathInfo.indexOf( '/' );
742         if ( slash > 0 )
743         {
744             logicalResource = requestPathInfo.substring( slash );
745
746             if ( logicalResource.endsWith( "/.." ) )
747             {
748                 logicalResource += "/";
749             }
750
751             if ( logicalResource != null && logicalResource.startsWith( "//" ) )
752             {
753                 logicalResource = logicalResource.substring( 1 );
754             }
755
756             if ( logicalResource == null )
757             {
758                 logicalResource = "/";
759             }
760         }
761         else
762         {
763             logicalResource = "/";
764         }
765         return logicalResource;
766
767     }
768
769     @Override
770     public String getFilePathWithVersion( final String requestPath, ManagedRepositoryContent managedRepositoryContent )
771         throws XMLException, RelocationException
772     {
773
774         if ( StringUtils.endsWith( requestPath, METADATA_FILENAME ) )
775         {
776             return getFilePath( requestPath, managedRepositoryContent.getRepository() );
777         }
778
779         String filePath = getFilePath( requestPath, managedRepositoryContent.getRepository() );
780
781         ArtifactReference artifactReference = null;
782         try
783         {
784             artifactReference = pathParser.toArtifactReference( filePath );
785         }
786         catch ( LayoutException e )
787         {
788             return filePath;
789         }
790
791         if ( StringUtils.endsWith( artifactReference.getVersion(), VersionUtil.SNAPSHOT ) )
792         {
793             // read maven metadata to get last timestamp
794             File metadataDir = new File( managedRepositoryContent.getRepoRoot(), filePath ).getParentFile();
795             if ( !metadataDir.exists() )
796             {
797                 return filePath;
798             }
799             File metadataFile = new File( metadataDir, METADATA_FILENAME );
800             if ( !metadataFile.exists() )
801             {
802                 return filePath;
803             }
804             ArchivaRepositoryMetadata archivaRepositoryMetadata = MavenMetadataReader.read( metadataFile );
805             int buildNumber = archivaRepositoryMetadata.getSnapshotVersion().getBuildNumber();
806             String timestamp = archivaRepositoryMetadata.getSnapshotVersion().getTimestamp();
807
808             // MRM-1846
809             if ( buildNumber < 1 && timestamp == null )
810             {
811                 return filePath;
812             }
813
814             // org/apache/archiva/archiva-checksum/1.4-M4-SNAPSHOT/archiva-checksum-1.4-M4-SNAPSHOT.jar
815             // ->  archiva-checksum-1.4-M4-20130425.081822-1.jar
816
817             filePath = StringUtils.replace( filePath, //
818                                             artifactReference.getArtifactId() //
819                                                 + "-" + artifactReference.getVersion(), //
820                                             artifactReference.getArtifactId() //
821                                                 + "-" + StringUtils.remove( artifactReference.getVersion(),
822                                                                             "-" + VersionUtil.SNAPSHOT ) //
823                                                 + "-" + timestamp //
824                                                 + "-" + buildNumber );
825
826             throw new RelocationException( "/repository/" + managedRepositoryContent.getRepository().getId() +
827                                                ( StringUtils.startsWith( filePath, "/" ) ? "" : "/" ) + filePath,
828                                            RelocationException.RelocationType.TEMPORARY );
829
830         }
831
832         return filePath;
833     }
834
835     //-----------------------------
836     // internal
837     //-----------------------------
838
839     /**
840      * FIXME remove
841      *
842      * @param href
843      * @return
844      */
845     private static String removePrefix( final String href )
846     {
847         String[] parts = StringUtils.split( href, '/' );
848         parts = (String[]) ArrayUtils.subarray( parts, 1, parts.length );
849         if ( parts == null || parts.length == 0 )
850         {
851             return "/";
852         }
853
854         String joinedString = StringUtils.join( parts, '/' );
855         if ( href.endsWith( "/" ) )
856         {
857             joinedString = joinedString + "/";
858         }
859
860         return joinedString;
861     }
862
863     private static void populateArtifactMetadataFromFile( ArtifactMetadata metadata, File file )
864     {
865         metadata.setWhenGathered( new Date() );
866         metadata.setFileLastModified( file.lastModified() );
867         ChecksummedFile checksummedFile = new ChecksummedFile( file );
868         try
869         {
870             metadata.setMd5( checksummedFile.calculateChecksum( ChecksumAlgorithm.MD5 ) );
871         }
872         catch ( IOException e )
873         {
874             LOGGER.error( "Unable to checksum file {}: {},MD5", file, e.getMessage() );
875         }
876         try
877         {
878             metadata.setSha1( checksummedFile.calculateChecksum( ChecksumAlgorithm.SHA1 ) );
879         }
880         catch ( IOException e )
881         {
882             LOGGER.error( "Unable to checksum file {}: {},SHA1", file, e.getMessage() );
883         }
884         metadata.setSize( file.length() );
885     }
886
887     private boolean isProject( File dir, Filter<String> filter )
888     {
889         // scan directories for a valid project version subdirectory, meaning this must be a project directory
890         File[] files = dir.listFiles( new DirectoryFilter( filter ) );
891         if ( files != null )
892         {
893             for ( File file : files )
894             {
895                 if ( isProjectVersion( file ) )
896                 {
897                     return true;
898                 }
899             }
900         }
901
902         // if a metadata file is present, check if this is the "artifactId" directory, marking it as a project
903         ArchivaRepositoryMetadata metadata = readMetadata( dir );
904         if ( metadata != null && dir.getName().equals( metadata.getArtifactId() ) )
905         {
906             return true;
907         }
908
909         return false;
910     }
911
912     private boolean isProjectVersion( File dir )
913     {
914         final String artifactId = dir.getParentFile().getName();
915         final String projectVersion = dir.getName();
916
917         // check if there is a POM artifact file to ensure it is a version directory
918         File[] files;
919         if ( VersionUtil.isSnapshot( projectVersion ) )
920         {
921             files = dir.listFiles( new PomFilenameFilter( artifactId, projectVersion ) );
922         }
923         else
924         {
925             final String pomFile = artifactId + "-" + projectVersion + ".pom";
926             files = dir.listFiles( new PomFileFilter( pomFile ) );
927         }
928         if ( files != null && files.length > 0 )
929         {
930             return true;
931         }
932
933         // if a metadata file is present, check if this is the "version" directory, marking it as a project version
934         ArchivaRepositoryMetadata metadata = readMetadata( dir );
935         if ( metadata != null && projectVersion.equals( metadata.getVersion() ) )
936         {
937             return true;
938         }
939
940         return false;
941     }
942
943     private ArchivaRepositoryMetadata readMetadata( File directory )
944     {
945         ArchivaRepositoryMetadata metadata = null;
946         File metadataFile = new File( directory, METADATA_FILENAME );
947         if ( metadataFile.exists() )
948         {
949             try
950             {
951                 metadata = MavenMetadataReader.read( metadataFile );
952             }
953             catch ( XMLException e )
954             {
955                 // ignore missing or invalid metadata
956             }
957         }
958         return metadata;
959     }
960
961     private static class DirectoryFilter
962         implements FilenameFilter
963     {
964         private final Filter<String> filter;
965
966         public DirectoryFilter( Filter<String> filter )
967         {
968             this.filter = filter;
969         }
970
971         @Override
972         public boolean accept( File dir, String name )
973         {
974             if ( !filter.accept( name ) )
975             {
976                 return false;
977             }
978             else if ( name.startsWith( "." ) )
979             {
980                 return false;
981             }
982             else if ( !new File( dir, name ).isDirectory() )
983             {
984                 return false;
985             }
986             return true;
987         }
988     }
989
990     private static class ArtifactDirectoryFilter
991         implements FilenameFilter
992     {
993         private final Filter<String> filter;
994
995         private ArtifactDirectoryFilter( Filter<String> filter )
996         {
997             this.filter = filter;
998         }
999
1000         @Override
1001         public boolean accept( File dir, String name )
1002         {
1003             // TODO compare to logic in maven-repository-layer
1004             if ( !filter.accept( name ) )
1005             {
1006                 return false;
1007             }
1008             else if ( name.startsWith( "." ) )
1009             {
1010                 return false;
1011             }
1012             else if ( name.endsWith( ".md5" ) || name.endsWith( ".sha1" ) || name.endsWith( ".asc" ) )
1013             {
1014                 return false;
1015             }
1016             else if ( name.equals( METADATA_FILENAME ) )
1017             {
1018                 return false;
1019             }
1020             else if ( new File( dir, name ).isDirectory() )
1021             {
1022                 return false;
1023             }
1024             // some files from remote repositories can have name like maven-metadata-archiva-vm-all-public.xml
1025             else if ( StringUtils.startsWith( name, METADATA_FILENAME_START ) && StringUtils.endsWith( name, ".xml" ) )
1026             {
1027                 return false;
1028             }
1029
1030             return true;
1031
1032         }
1033     }
1034
1035
1036     private static final class PomFilenameFilter
1037         implements FilenameFilter
1038     {
1039
1040         private final String artifactId, projectVersion;
1041
1042         private PomFilenameFilter( String artifactId, String projectVersion )
1043         {
1044             this.artifactId = artifactId;
1045             this.projectVersion = projectVersion;
1046         }
1047
1048         @Override
1049         public boolean accept( File dir, String name )
1050         {
1051             if ( name.startsWith( artifactId + "-" ) && name.endsWith( ".pom" ) )
1052             {
1053                 String v = name.substring( artifactId.length() + 1, name.length() - 4 );
1054                 v = VersionUtil.getBaseVersion( v );
1055                 if ( v.equals( projectVersion ) )
1056                 {
1057                     return true;
1058                 }
1059             }
1060             return false;
1061         }
1062     }
1063
1064     private static class PomFileFilter
1065         implements FilenameFilter
1066     {
1067         private final String pomFile;
1068
1069         private PomFileFilter( String pomFile )
1070         {
1071             this.pomFile = pomFile;
1072         }
1073
1074         @Override
1075         public boolean accept( File dir, String name )
1076         {
1077             return pomFile.equals( name );
1078         }
1079     }
1080
1081
1082     public PathParser getPathParser()
1083     {
1084         return pathParser;
1085     }
1086
1087     public void setPathParser( PathParser pathParser )
1088     {
1089         this.pathParser = pathParser;
1090     }
1091 }