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