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