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