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