1 package org.apache.maven.archiva.repository.metadata;
4 * Copyright 2001-2007 The Apache Software Foundation.
6 * Licensed under the Apache License, Version 2.0 (the "License");
7 * you may not use this file except in compliance with the License.
8 * You may obtain a copy of the License at
10 * http://www.apache.org/licenses/LICENSE-2.0
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
19 import org.apache.commons.collections.CollectionUtils;
20 import org.apache.commons.lang.StringUtils;
21 import org.apache.commons.lang.math.NumberUtils;
22 import org.apache.commons.lang.time.DateUtils;
23 import org.apache.maven.archiva.common.utils.Checksums;
24 import org.apache.maven.archiva.common.utils.PathUtil;
25 import org.apache.maven.archiva.common.utils.VersionComparator;
26 import org.apache.maven.archiva.common.utils.VersionUtil;
27 import org.apache.maven.archiva.configuration.ArchivaConfiguration;
28 import org.apache.maven.archiva.configuration.ConfigurationNames;
29 import org.apache.maven.archiva.configuration.FileTypes;
30 import org.apache.maven.archiva.configuration.ProxyConnectorConfiguration;
31 import org.apache.maven.archiva.model.ArchivaRepositoryMetadata;
32 import org.apache.maven.archiva.model.ArtifactReference;
33 import org.apache.maven.archiva.model.ProjectReference;
34 import org.apache.maven.archiva.model.SnapshotVersion;
35 import org.apache.maven.archiva.model.VersionedReference;
36 import org.apache.maven.archiva.repository.ContentNotFoundException;
37 import org.apache.maven.archiva.repository.ManagedRepositoryContent;
38 import org.apache.maven.archiva.repository.RemoteRepositoryContent;
39 import org.apache.maven.archiva.repository.layout.LayoutException;
40 import org.codehaus.plexus.personality.plexus.lifecycle.phase.Initializable;
41 import org.codehaus.plexus.personality.plexus.lifecycle.phase.InitializationException;
42 import org.codehaus.plexus.registry.Registry;
43 import org.codehaus.plexus.registry.RegistryListener;
44 import org.codehaus.plexus.util.SelectorUtils;
45 import org.slf4j.Logger;
46 import org.slf4j.LoggerFactory;
49 import java.io.IOException;
50 import java.text.ParseException;
51 import java.text.SimpleDateFormat;
52 import java.util.ArrayList;
53 import java.util.Calendar;
54 import java.util.Collections;
55 import java.util.Date;
56 import java.util.HashMap;
57 import java.util.HashSet;
58 import java.util.Iterator;
59 import java.util.List;
62 import java.util.regex.Matcher;
67 * @author <a href="mailto:joakime@apache.org">Joakim Erdfelt</a>
70 * @plexus.component role="org.apache.maven.archiva.repository.metadata.MetadataTools"
72 public class MetadataTools
73 implements RegistryListener, Initializable
75 private static Logger log = LoggerFactory.getLogger( MetadataTools.class );
77 public static final String MAVEN_METADATA = "maven-metadata.xml";
79 private static final char PATH_SEPARATOR = '/';
81 private static final char GROUP_SEPARATOR = '.';
86 private ArchivaConfiguration configuration;
91 private FileTypes filetypes;
96 private Checksums checksums;
98 private List<String> artifactPatterns;
100 private Map<String, Set<String>> proxies;
102 private static final char NUMS[] = new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
104 private SimpleDateFormat lastUpdatedFormat;
106 public MetadataTools()
108 lastUpdatedFormat = new SimpleDateFormat( "yyyyMMddHHmmss" );
109 lastUpdatedFormat.setTimeZone( DateUtils.UTC_TIME_ZONE );
112 public void afterConfigurationChange( Registry registry, String propertyName, Object propertyValue )
114 if ( ConfigurationNames.isProxyConnector( propertyName ) )
116 initConfigVariables();
120 public void beforeConfigurationChange( Registry registry, String propertyName, Object propertyValue )
126 * Gather the set of snapshot versions found in a particular versioned reference.
128 * @return the Set of snapshot artifact versions found.
129 * @throws LayoutException
130 * @throws ContentNotFoundException
132 public Set<String> gatherSnapshotVersions( ManagedRepositoryContent managedRepository, VersionedReference reference )
133 throws LayoutException, IOException, ContentNotFoundException
135 Set<String> foundVersions = managedRepository.getVersions( reference );
137 // Next gather up the referenced 'latest' versions found in any proxied repositories
138 // maven-metadata-${proxyId}.xml files that may be present.
140 // Does this repository have a set of remote proxied repositories?
141 Set<String> proxiedRepoIds = this.proxies.get( managedRepository.getId() );
143 if ( CollectionUtils.isNotEmpty( proxiedRepoIds ) )
145 String baseVersion = VersionUtil.getBaseVersion( reference.getVersion() );
146 baseVersion = baseVersion.substring( 0, baseVersion.indexOf( VersionUtil.SNAPSHOT ) - 1 );
148 // Add in the proxied repo version ids too.
149 Iterator<String> it = proxiedRepoIds.iterator();
150 while ( it.hasNext() )
152 String proxyId = it.next();
154 ArchivaRepositoryMetadata proxyMetadata = readProxyMetadata( managedRepository, reference, proxyId );
155 if ( proxyMetadata == null )
157 // There is no proxy metadata, skip it.
161 // Is there some snapshot info?
162 SnapshotVersion snapshot = proxyMetadata.getSnapshotVersion();
163 if ( snapshot != null )
165 String timestamp = snapshot.getTimestamp();
166 int buildNumber = snapshot.getBuildNumber();
168 // Only interested in the timestamp + buildnumber.
169 if ( StringUtils.isNotBlank( timestamp ) && ( buildNumber > 0 ) )
171 foundVersions.add( baseVersion + "-" + timestamp + "-" + buildNumber );
177 return foundVersions;
181 * Take a path to a maven-metadata.xml, and attempt to translate it to a VersionedReference.
186 public VersionedReference toVersionedReference( String path )
187 throws RepositoryMetadataException
189 if ( !path.endsWith( "/" + MAVEN_METADATA ) )
191 throw new RepositoryMetadataException( "Cannot convert to versioned reference, not a metadata file. " );
194 VersionedReference reference = new VersionedReference();
196 String normalizedPath = StringUtils.replace( path, "\\", "/" );
197 String pathParts[] = StringUtils.split( normalizedPath, '/' );
199 int versionOffset = pathParts.length - 2;
200 int artifactIdOffset = versionOffset - 1;
201 int groupIdEnd = artifactIdOffset - 1;
203 reference.setVersion( pathParts[versionOffset] );
205 if ( !hasNumberAnywhere( reference.getVersion() ) )
207 // Scary check, but without it, all paths are version references;
208 throw new RepositoryMetadataException(
209 "Not a versioned reference, as version id on path has no number in it." );
212 reference.setArtifactId( pathParts[artifactIdOffset] );
214 StringBuffer gid = new StringBuffer();
215 for ( int i = 0; i <= groupIdEnd; i++ )
221 gid.append( pathParts[i] );
224 reference.setGroupId( gid.toString() );
229 private boolean hasNumberAnywhere( String version )
231 return StringUtils.indexOfAny( version, NUMS ) != ( -1 );
234 public ProjectReference toProjectReference( String path )
235 throws RepositoryMetadataException
237 if ( !path.endsWith( "/" + MAVEN_METADATA ) )
239 throw new RepositoryMetadataException( "Cannot convert to versioned reference, not a metadata file. " );
242 ProjectReference reference = new ProjectReference();
244 String normalizedPath = StringUtils.replace( path, "\\", "/" );
245 String pathParts[] = StringUtils.split( normalizedPath, '/' );
247 // Assume last part of the path is the version.
249 int artifactIdOffset = pathParts.length - 2;
250 int groupIdEnd = artifactIdOffset - 1;
252 reference.setArtifactId( pathParts[artifactIdOffset] );
254 StringBuffer gid = new StringBuffer();
255 for ( int i = 0; i <= groupIdEnd; i++ )
261 gid.append( pathParts[i] );
264 reference.setGroupId( gid.toString() );
269 public String toPath( ProjectReference reference )
271 StringBuffer path = new StringBuffer();
273 path.append( formatAsDirectory( reference.getGroupId() ) ).append( PATH_SEPARATOR );
274 path.append( reference.getArtifactId() ).append( PATH_SEPARATOR );
275 path.append( MAVEN_METADATA );
277 return path.toString();
280 public String toPath( VersionedReference reference )
282 StringBuffer path = new StringBuffer();
284 path.append( formatAsDirectory( reference.getGroupId() ) ).append( PATH_SEPARATOR );
285 path.append( reference.getArtifactId() ).append( PATH_SEPARATOR );
286 if ( reference.getVersion() != null )
288 // add the version only if it is present
289 path.append( VersionUtil.getBaseVersion( reference.getVersion() ) ).append( PATH_SEPARATOR );
291 path.append( MAVEN_METADATA );
293 return path.toString();
296 private String formatAsDirectory( String directory )
298 return directory.replace( GROUP_SEPARATOR, PATH_SEPARATOR );
302 * Adjusts a path for a metadata.xml file to its repository specific path.
304 * @param repository the repository to base new path off of.
305 * @param path the path to the metadata.xml file to adjust the name of.
306 * @return the newly adjusted path reference to the repository specific metadata path.
308 public String getRepositorySpecificName( RemoteRepositoryContent repository, String path )
310 return getRepositorySpecificName( repository.getId(), path );
314 * Adjusts a path for a metadata.xml file to its repository specific path.
316 * @param proxyId the repository id to base new path off of.
317 * @param path the path to the metadata.xml file to adjust the name of.
318 * @return the newly adjusted path reference to the repository specific metadata path.
320 public String getRepositorySpecificName( String proxyId, String path )
322 StringBuffer ret = new StringBuffer();
324 int idx = path.lastIndexOf( "/" );
327 ret.append( path.substring( 0, idx + 1 ) );
330 // TODO: need to filter out 'bad' characters from the proxy id.
331 ret.append( "maven-metadata-" ).append( proxyId ).append( ".xml" );
333 return ret.toString();
336 public void initialize()
337 throws InitializationException
339 this.artifactPatterns = new ArrayList<String>();
340 this.proxies = new HashMap<String, Set<String>>();
341 initConfigVariables();
343 configuration.addChangeListener( this );
346 public ArchivaRepositoryMetadata readProxyMetadata( ManagedRepositoryContent managedRepository,
347 ProjectReference reference, String proxyId )
349 String metadataPath = getRepositorySpecificName( proxyId, toPath( reference ) );
350 File metadataFile = new File( managedRepository.getRepoRoot(), metadataPath );
352 if ( !metadataFile.exists() || !metadataFile.isFile() )
354 // Nothing to do. return null.
360 return RepositoryMetadataReader.read( metadataFile );
362 catch ( RepositoryMetadataException e )
364 // TODO: [monitor] consider a monitor for this event.
365 // TODO: consider a read-redo on monitor return code?
366 log.warn( "Unable to read metadata: " + metadataFile.getAbsolutePath(), e );
371 public ArchivaRepositoryMetadata readProxyMetadata( ManagedRepositoryContent managedRepository,
372 VersionedReference reference, String proxyId )
374 String metadataPath = getRepositorySpecificName( proxyId, toPath( reference ) );
375 File metadataFile = new File( managedRepository.getRepoRoot(), metadataPath );
377 if ( !metadataFile.exists() || !metadataFile.isFile() )
379 // Nothing to do. return null.
385 return RepositoryMetadataReader.read( metadataFile );
387 catch ( RepositoryMetadataException e )
389 // TODO: [monitor] consider a monitor for this event.
390 // TODO: consider a read-redo on monitor return code?
391 log.warn( "Unable to read metadata: " + metadataFile.getAbsolutePath(), e );
397 * Update the metadata to represent the all versions of
398 * the provided groupId:artifactId project reference,
399 * based off of information present in the repository,
400 * the maven-metadata.xml files, and the proxy/repository specific
401 * metadata file contents.
403 * @param managedRepository the managed repository where the metadata is kept.
404 * @param reference the versioned referencfe to update.
405 * @throws LayoutException
406 * @throws RepositoryMetadataException
407 * @throws IOException
408 * @throws ContentNotFoundException
410 public void updateMetadata( ManagedRepositoryContent managedRepository, ProjectReference reference )
411 throws LayoutException, RepositoryMetadataException, IOException, ContentNotFoundException
413 File metadataFile = new File( managedRepository.getRepoRoot(), toPath( reference ) );
415 long lastUpdated = getExistingLastUpdated( metadataFile );
417 ArchivaRepositoryMetadata metadata = new ArchivaRepositoryMetadata();
418 metadata.setGroupId( reference.getGroupId() );
419 metadata.setArtifactId( reference.getArtifactId() );
421 // Gather up all versions found in the managed repository.
422 Set<String> allVersions = managedRepository.getVersions( reference );
424 // Does this repository have a set of remote proxied repositories?
425 Set<String> proxiedRepoIds = this.proxies.get( managedRepository.getId() );
427 if ( CollectionUtils.isNotEmpty( proxiedRepoIds ) )
429 // Add in the proxied repo version ids too.
430 Iterator<String> it = proxiedRepoIds.iterator();
431 while ( it.hasNext() )
433 String proxyId = it.next();
435 ArchivaRepositoryMetadata proxyMetadata = readProxyMetadata( managedRepository, reference, proxyId );
436 if ( proxyMetadata != null )
438 allVersions.addAll( proxyMetadata.getAvailableVersions() );
439 long proxyLastUpdated = getLastUpdated( proxyMetadata );
441 lastUpdated = Math.max( lastUpdated, proxyLastUpdated );
446 if ( allVersions.size() == 0 )
448 throw new IOException( "No versions found for reference." );
452 List<String> sortedVersions = new ArrayList<String>( allVersions );
453 Collections.sort( sortedVersions, VersionComparator.getInstance() );
455 // Split the versions into released and snapshots.
456 List<String> releasedVersions = new ArrayList<String>();
457 List<String> snapshotVersions = new ArrayList<String>();
459 for ( String version : sortedVersions )
461 if ( VersionUtil.isSnapshot( version ) )
463 snapshotVersions.add( version );
467 releasedVersions.add( version );
471 Collections.sort( releasedVersions, VersionComparator.getInstance() );
472 Collections.sort( snapshotVersions, VersionComparator.getInstance() );
474 String latestVersion = sortedVersions.get( sortedVersions.size() - 1 );
475 String releaseVersion = null;
477 if ( CollectionUtils.isNotEmpty( releasedVersions ) )
479 releaseVersion = releasedVersions.get( releasedVersions.size() - 1 );
482 // Add the versions to the metadata model.
483 metadata.setAvailableVersions( sortedVersions );
485 metadata.setLatestVersion( latestVersion );
486 metadata.setReleasedVersion( releaseVersion );
487 if ( lastUpdated > 0 )
489 metadata.setLastUpdatedTimestamp( toLastUpdatedDate( lastUpdated ) );
492 // Save the metadata model to disk.
493 RepositoryMetadataWriter.write( metadata, metadataFile );
494 checksums.update( metadataFile );
497 private Date toLastUpdatedDate( long lastUpdated )
499 Calendar cal = Calendar.getInstance( DateUtils.UTC_TIME_ZONE );
500 cal.setTimeInMillis( lastUpdated );
502 return cal.getTime();
505 private long toLastUpdatedLong( String timestampString )
509 Date date = lastUpdatedFormat.parse( timestampString );
510 Calendar cal = Calendar.getInstance( DateUtils.UTC_TIME_ZONE );
513 return cal.getTimeInMillis();
515 catch ( ParseException e )
521 private long getLastUpdated( ArchivaRepositoryMetadata metadata )
523 if ( metadata == null )
531 String lastUpdated = metadata.getLastUpdated();
532 if ( StringUtils.isBlank( lastUpdated ) )
538 Date lastUpdatedDate = lastUpdatedFormat.parse( lastUpdated );
539 return lastUpdatedDate.getTime();
541 catch ( ParseException e )
543 // Bad format on the last updated string.
548 private long getExistingLastUpdated( File metadataFile )
550 if ( !metadataFile.exists() )
558 ArchivaRepositoryMetadata metadata = RepositoryMetadataReader.read( metadataFile );
560 return getLastUpdated( metadata );
562 catch ( RepositoryMetadataException e )
570 * Update the metadata based on the following rules.
572 * 1) If this is a SNAPSHOT reference, then utilize the proxy/repository specific
573 * metadata files to represent the current / latest SNAPSHOT available.
574 * 2) If this is a RELEASE reference, and the metadata file does not exist, then
575 * create the metadata file with contents required of the VersionedReference
577 * @param managedRepository the managed repository where the metadata is kept.
578 * @param reference the versioned reference to update
579 * @throws LayoutException
580 * @throws RepositoryMetadataException
581 * @throws IOException
582 * @throws ContentNotFoundException
584 public void updateMetadata( ManagedRepositoryContent managedRepository, VersionedReference reference )
585 throws LayoutException, RepositoryMetadataException, IOException, ContentNotFoundException
587 File metadataFile = new File( managedRepository.getRepoRoot(), toPath( reference ) );
589 long lastUpdated = getExistingLastUpdated( metadataFile );
591 ArchivaRepositoryMetadata metadata = new ArchivaRepositoryMetadata();
592 metadata.setGroupId( reference.getGroupId() );
593 metadata.setArtifactId( reference.getArtifactId() );
595 if ( VersionUtil.isSnapshot( reference.getVersion() ) )
597 // Do SNAPSHOT handling.
598 metadata.setVersion( VersionUtil.getBaseVersion( reference.getVersion() ) );
600 // Gather up all of the versions found in the reference dir, and any
601 // proxied maven-metadata.xml files.
602 Set<String> snapshotVersions = gatherSnapshotVersions( managedRepository, reference );
604 if ( snapshotVersions.isEmpty() )
606 throw new ContentNotFoundException( "No snapshot versions found on reference ["
607 + VersionedReference.toKey( reference ) + "]." );
610 // sort the list to determine to aide in determining the Latest version.
611 List<String> sortedVersions = new ArrayList<String>();
612 sortedVersions.addAll( snapshotVersions );
613 Collections.sort( sortedVersions, new VersionComparator() );
615 String latestVersion = sortedVersions.get( sortedVersions.size() - 1 );
617 if ( VersionUtil.isUniqueSnapshot( latestVersion ) )
619 // The latestVersion will contain the full version string "1.0-alpha-5-20070821.213044-8"
620 // This needs to be broken down into ${base}-${timestamp}-${build_number}
622 Matcher m = VersionUtil.UNIQUE_SNAPSHOT_PATTERN.matcher( latestVersion );
625 metadata.setSnapshotVersion( new SnapshotVersion() );
626 int buildNumber = NumberUtils.toInt( m.group( 3 ), -1 );
627 metadata.getSnapshotVersion().setBuildNumber( buildNumber );
629 Matcher mtimestamp = VersionUtil.TIMESTAMP_PATTERN.matcher( m.group( 2 ) );
630 if ( mtimestamp.matches() )
632 String tsDate = mtimestamp.group( 1 );
633 String tsTime = mtimestamp.group( 2 );
635 long snapshotLastUpdated = toLastUpdatedLong( tsDate + tsTime );
637 lastUpdated = Math.max( lastUpdated, snapshotLastUpdated );
639 metadata.getSnapshotVersion().setTimestamp( m.group( 2 ) );
643 else if ( VersionUtil.isGenericSnapshot( latestVersion ) )
645 // The latestVersion ends with the generic version string.
646 // Example: 1.0-alpha-5-SNAPSHOT
648 metadata.setSnapshotVersion( new SnapshotVersion() );
650 /* Disabled due to decision in [MRM-535].
651 * Do not set metadata.lastUpdated to file.lastModified.
653 * Should this be the last updated timestamp of the file, or in the case of an
654 * archive, the most recent timestamp in the archive?
656 ArtifactReference artifact = getFirstArtifact( managedRepository, reference );
658 if ( artifact == null )
660 throw new IOException( "Not snapshot artifact found to reference in " + reference );
663 File artifactFile = managedRepository.toFile( artifact );
665 if ( artifactFile.exists() )
667 Date lastModified = new Date( artifactFile.lastModified() );
668 metadata.setLastUpdatedTimestamp( lastModified );
674 throw new RepositoryMetadataException( "Unable to process snapshot version <" + latestVersion
675 + "> reference <" + reference + ">" );
680 // Do RELEASE handling.
681 metadata.setVersion( reference.getVersion() );
685 if ( lastUpdated > 0 )
687 metadata.setLastUpdatedTimestamp( toLastUpdatedDate( lastUpdated ) );
690 // Save the metadata model to disk.
691 RepositoryMetadataWriter.write( metadata, metadataFile );
692 checksums.update( metadataFile );
695 private void initConfigVariables()
697 synchronized ( this.artifactPatterns )
699 this.artifactPatterns.clear();
701 this.artifactPatterns.addAll( filetypes.getFileTypePatterns( FileTypes.ARTIFACTS ) );
704 synchronized ( proxies )
706 this.proxies.clear();
708 List<ProxyConnectorConfiguration> proxyConfigs = configuration.getConfiguration().getProxyConnectors();
709 for( ProxyConnectorConfiguration proxyConfig: proxyConfigs )
711 String key = proxyConfig.getSourceRepoId();
713 Set<String> remoteRepoIds = this.proxies.get( key );
715 if ( remoteRepoIds == null )
717 remoteRepoIds = new HashSet<String>();
720 remoteRepoIds.add( proxyConfig.getTargetRepoId() );
722 this.proxies.put( key, remoteRepoIds );
728 * Get the first Artifact found in the provided VersionedReference location.
730 * @param managedRepository the repository to search within.
731 * @param reference the reference to the versioned reference to search within
732 * @return the ArtifactReference to the first artifact located within the versioned reference. or null if
733 * no artifact was found within the versioned reference.
734 * @throws IOException if the versioned reference is invalid (example: doesn't exist, or isn't a directory)
735 * @throws LayoutException
737 public ArtifactReference getFirstArtifact( ManagedRepositoryContent managedRepository, VersionedReference reference )
738 throws LayoutException, IOException
740 String path = toPath( reference );
742 int idx = path.lastIndexOf( '/' );
745 path = path.substring( 0, idx );
748 File repoDir = new File( managedRepository.getRepoRoot(), path );
750 if ( !repoDir.exists() )
752 throw new IOException( "Unable to gather the list of snapshot versions on a non-existant directory: "
753 + repoDir.getAbsolutePath() );
756 if ( !repoDir.isDirectory() )
758 throw new IOException( "Unable to gather the list of snapshot versions on a non-directory: "
759 + repoDir.getAbsolutePath() );
762 File repoFiles[] = repoDir.listFiles();
763 for ( int i = 0; i < repoFiles.length; i++ )
765 if ( repoFiles[i].isDirectory() )
767 // Skip it. it's a directory.
771 String relativePath = PathUtil.getRelative( managedRepository.getRepoRoot(), repoFiles[i] );
773 if ( matchesArtifactPattern( relativePath ) )
775 ArtifactReference artifact = managedRepository.toArtifactReference( relativePath );
781 // No artifact was found.
785 private boolean matchesArtifactPattern( String relativePath )
787 // Correct the slash pattern.
788 relativePath = relativePath.replace( '\\', '/' );
790 Iterator<String> it = this.artifactPatterns.iterator();
791 while ( it.hasNext() )
793 String pattern = it.next();
795 if ( SelectorUtils.matchPath( pattern, relativePath, false ) )