/**
* Copyright (c) 2008-2011 Sonatype, Inc.
* All rights reserved. Includes the third-party code listed at http://www.sonatype.com/products/nexus/attributions.
*
* This program is free software: you can redistribute it and/or modify it only under the terms of the GNU Affero General
* Public License Version 3 as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License Version 3
* for more details.
*
* You should have received a copy of the GNU Affero General Public License Version 3 along with this program. If not, see
* http://www.gnu.org/licenses.
*
* Sonatype Nexus (TM) Open Source Version is available from Sonatype, Inc. Sonatype and Sonatype Nexus are trademarks of
* Sonatype, Inc. Apache Maven is a trademark of the Apache Foundation. M2Eclipse is a trademark of the Eclipse Foundation.
* All other trademarks are the property of their respective owners.
*/
package org.sonatype.nexus.maven.tasks;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.TreeSet;
import org.apache.maven.index.artifact.Gav;
import org.codehaus.plexus.component.annotations.Component;
import org.codehaus.plexus.component.annotations.Requirement;
import org.codehaus.plexus.logging.AbstractLogEnabled;
import org.sonatype.aether.util.version.GenericVersionScheme;
import org.sonatype.aether.version.InvalidVersionSpecificationException;
import org.sonatype.aether.version.Version;
import org.sonatype.aether.version.VersionScheme;
import org.sonatype.nexus.proxy.IllegalOperationException;
import org.sonatype.nexus.proxy.ItemNotFoundException;
import org.sonatype.nexus.proxy.NoSuchRepositoryException;
import org.sonatype.nexus.proxy.ResourceStoreRequest;
import org.sonatype.nexus.proxy.StorageException;
import org.sonatype.nexus.proxy.item.RepositoryItemUid;
import org.sonatype.nexus.proxy.item.StorageCollectionItem;
import org.sonatype.nexus.proxy.item.StorageFileItem;
import org.sonatype.nexus.proxy.item.StorageItem;
import org.sonatype.nexus.proxy.maven.MavenHostedRepository;
import org.sonatype.nexus.proxy.maven.MavenProxyRepository;
import org.sonatype.nexus.proxy.maven.MavenRepository;
import org.sonatype.nexus.proxy.maven.RecreateMavenMetadataWalkerProcessor;
import org.sonatype.nexus.proxy.maven.RepositoryPolicy;
import org.sonatype.nexus.proxy.registry.ContentClass;
import org.sonatype.nexus.proxy.registry.RepositoryRegistry;
import org.sonatype.nexus.proxy.repository.GroupRepository;
import org.sonatype.nexus.proxy.repository.LocalStatus;
import org.sonatype.nexus.proxy.repository.Repository;
import org.sonatype.nexus.proxy.storage.UnsupportedStorageOperationException;
import org.sonatype.nexus.proxy.walker.AbstractWalkerProcessor;
import org.sonatype.nexus.proxy.walker.DefaultWalkerContext;
import org.sonatype.nexus.proxy.walker.DottedStoreWalkerFilter;
import org.sonatype.nexus.proxy.walker.Walker;
import org.sonatype.nexus.proxy.walker.WalkerContext;
import org.sonatype.nexus.proxy.walker.WalkerException;
import org.sonatype.nexus.util.ItemPathUtils;
/**
* The Class SnapshotRemoverJob. After a succesful run, the job guarantees that there will remain at least
* minCountOfSnapshotsToKeep (but maybe more) snapshots per one snapshot collection by removing all older from
* removeSnapshotsOlderThanDays. If should remove snaps if their release counterpart exists, the whole GAV will be
* removed.
*
* @author cstamas
*/
@Component( role = SnapshotRemover.class )
public class DefaultSnapshotRemover
extends AbstractLogEnabled
implements SnapshotRemover
{
@Requirement
private RepositoryRegistry repositoryRegistry;
@Requirement
private Walker walker;
@Requirement( hint = "maven2" )
private ContentClass contentClass;
private VersionScheme versionScheme = new GenericVersionScheme();
protected RepositoryRegistry getRepositoryRegistry()
{
return repositoryRegistry;
}
public SnapshotRemovalResult removeSnapshots( SnapshotRemovalRequest request )
throws NoSuchRepositoryException, IllegalArgumentException
{
SnapshotRemovalResult result = new SnapshotRemovalResult();
logDetails( request );
if ( request.getRepositoryId() != null )
{
Repository repository = getRepositoryRegistry().getRepository( request.getRepositoryId() );
if ( !process( request, result, repository ) )
{
throw new IllegalArgumentException( "The repository with ID=" + repository.getId()
+ " is not valid for Snapshot Removal Task!" );
}
}
else
{
for ( Repository repository : getRepositoryRegistry().getRepositories() )
{
process( request, result, repository );
}
}
return result;
}
private void process( SnapshotRemovalRequest request, SnapshotRemovalResult result, GroupRepository group )
{
for ( Repository repository : group.getMemberRepositories() )
{
process( request, result, repository );
}
}
private boolean process( SnapshotRemovalRequest request, SnapshotRemovalResult result, Repository repository )
{
// only from maven repositories, stay silent for others and simply skip
if ( !repository.getRepositoryContentClass().isCompatible( contentClass ) )
{
getLogger().debug( "Skipping '" + repository.getId() + "' is not a maven 2 repository" );
return false;
}
if ( LocalStatus.OUT_OF_SERVICE.equals( repository.getLocalStatus() ) )
{
getLogger().debug( "Skipping '" + repository.getId() + "' the repository is out of service" );
return false;
}
if ( repository.getRepositoryKind().isFacetAvailable( GroupRepository.class ) )
{
process( request, result, repository.adaptToFacet( GroupRepository.class ) );
}
else if ( repository.getRepositoryKind().isFacetAvailable( MavenRepository.class ) )
{
result.addResult( removeSnapshotsFromMavenRepository( repository.adaptToFacet( MavenRepository.class ),
request ) );
}
return true;
}
/**
* Removes the snapshots from maven repository.
*
* @param repository the repository
* @throws Exception the exception
*/
protected SnapshotRemovalRepositoryResult removeSnapshotsFromMavenRepository( MavenRepository repository,
SnapshotRemovalRequest request )
{
SnapshotRemovalRepositoryResult result = new SnapshotRemovalRepositoryResult( repository.getId(), 0, 0, true );
if ( !repository.getLocalStatus().shouldServiceRequest() )
{
return result;
}
// we are already processed here, so skip repo
if ( request.isProcessedRepo( repository.getId() ) )
{
return new SnapshotRemovalRepositoryResult( repository.getId(), true );
}
request.addProcessedRepo( repository.getId() );
// if this is not snap repo, do nothing
if ( !RepositoryPolicy.SNAPSHOT.equals( repository.getRepositoryPolicy() ) )
{
return result;
}
if ( getLogger().isDebugEnabled() )
{
getLogger().debug(
"Collecting deletable snapshots on repository " + repository.getId() + " from storage directory "
+ repository.getLocalUrl() );
}
request.getMetadataRebuildPaths().clear();
// create a walker to collect deletables and let it loose on collections only
SnapshotRemoverWalkerProcessor snapshotRemoveProcessor =
new SnapshotRemoverWalkerProcessor( repository, request );
DefaultWalkerContext ctxMain =
new DefaultWalkerContext( repository, new ResourceStoreRequest( "/" ), new DottedStoreWalkerFilter() );
ctxMain.getProcessors().add( snapshotRemoveProcessor );
walker.walk( ctxMain );
if ( ctxMain.getStopCause() != null )
{
result.setSuccessful( false );
}
// and collect results
result.setDeletedSnapshots( snapshotRemoveProcessor.getDeletedSnapshots() );
result.setDeletedFiles( snapshotRemoveProcessor.getDeletedFiles() );
if ( getLogger().isDebugEnabled() )
{
getLogger().debug(
"Collected and deleted " + snapshotRemoveProcessor.getDeletedSnapshots()
+ " snapshots with alltogether " + snapshotRemoveProcessor.getDeletedFiles()
+ " files on repository " + repository.getId() );
}
repository.expireCaches( new ResourceStoreRequest( RepositoryItemUid.PATH_ROOT ) );
RecreateMavenMetadataWalkerProcessor metadataRebuildProcessor =
new RecreateMavenMetadataWalkerProcessor( getLogger() );
for ( String path : request.getMetadataRebuildPaths() )
{
DefaultWalkerContext ctxMd =
new DefaultWalkerContext( repository, new ResourceStoreRequest( path ), new DottedStoreWalkerFilter() );
ctxMd.getProcessors().add( metadataRebuildProcessor );
try
{
walker.walk( ctxMd );
}
catch ( WalkerException e )
{
if ( !( e.getCause() instanceof ItemNotFoundException ) )
{
// do not ignore it
throw e;
}
}
}
return result;
}
private void logDetails( SnapshotRemovalRequest request )
{
if ( request.getRepositoryId() != null )
{
getLogger().info( "Removing old SNAPSHOT deployments from " + request.getRepositoryId() + " repository." );
}
else
{
getLogger().info( "Removing old SNAPSHOT deployments from all repositories." );
}
if ( getLogger().isDebugEnabled() )
{
getLogger().debug( "With parameters: " );
getLogger().debug( " MinCountOfSnapshotsToKeep: " + request.getMinCountOfSnapshotsToKeep() );
getLogger().debug( " RemoveSnapshotsOlderThanDays: " + request.getRemoveSnapshotsOlderThanDays() );
getLogger().debug( " RemoveIfReleaseExists: " + request.isRemoveIfReleaseExists() );
}
}
private class SnapshotRemoverWalkerProcessor
extends AbstractWalkerProcessor
{
private final MavenRepository repository;
private final SnapshotRemovalRequest request;
private final Map<Version, List<StorageFileItem>> remainingSnapshotsAndFiles =
new HashMap<Version, List<StorageFileItem>>();
private final Map<Version, List<StorageFileItem>> deletableSnapshotsAndFiles =
new HashMap<Version, List<StorageFileItem>>();
private final long dateThreshold;
private boolean shouldProcessCollection;
private boolean removeWholeGAV;
private int deletedSnapshots = 0;
private int deletedFiles = 0;
public SnapshotRemoverWalkerProcessor( MavenRepository repository, SnapshotRemovalRequest request )
{
this.repository = repository;
this.request = request;
int days = request.getRemoveSnapshotsOlderThanDays();
if ( days > 0 )
{
this.dateThreshold = System.currentTimeMillis() - ( days * 86400000L );
}
else
{
this.dateThreshold = -1;
}
}
protected void addStorageFileItemToMap( Map<Version, List<StorageFileItem>> map, Gav gav, StorageFileItem item )
{
Version key = null;
try
{
key = versionScheme.parseVersion( gav.getVersion() );
}
catch ( InvalidVersionSpecificationException e )
{
try
{
key = versionScheme.parseVersion( "0.0-SNAPSHOT" );
}
catch ( InvalidVersionSpecificationException e1 )
{
// nah
}
}
if ( !map.containsKey( key ) )
{
map.put( key, new ArrayList<StorageFileItem>() );
}
map.get( key ).add( item );
}
@Override
public void processItem( WalkerContext context, StorageItem item )
throws Exception
{
}
@Override
public void onCollectionExit( WalkerContext context, StorageCollectionItem coll )
{
try
{
doOnCollectionExit( context, coll );
}
catch ( Exception e )
{
// we always simply log the exception and continue
getLogger().warn( "SnapshotRemover is failed to process path: '" + coll.getPath() + "'.", e );
}
}
public void doOnCollectionExit( WalkerContext context, StorageCollectionItem coll )
throws Exception
{
if ( getLogger().isDebugEnabled() )
{
getLogger().debug( "onCollectionExit() :: " + coll.getRepositoryItemUid().toString() );
}
shouldProcessCollection = coll.getPath().endsWith( "SNAPSHOT" );
if ( !shouldProcessCollection )
{
return;
}
deletableSnapshotsAndFiles.clear();
remainingSnapshotsAndFiles.clear();
removeWholeGAV = false;
Gav gav = null;
Collection<StorageItem> items;
items = repository.list( false, coll );
HashSet<Long> versionsToRemove = new HashSet<Long>();
// gathering the facts
for ( StorageItem item : items )
{
if ( !item.isVirtual() && !StorageCollectionItem.class.isAssignableFrom( item.getClass() ) )
{
gav =
( (MavenRepository) coll.getRepositoryItemUid().getRepository() ).getGavCalculator().pathToGav(
item.getPath() );
if ( gav != null )
{
// if we find a pom, check for delete on release
if ( !gav.isHash() && !gav.isSignature() && gav.getExtension().equals( "pom" ) )
{
if ( request.isRemoveIfReleaseExists()
&& releaseExistsForSnapshot( gav, item.getItemContext() ) )
{
getLogger().debug( "Found POM and release exists, removing whole gav." );
removeWholeGAV = true;
// Will break out and junk whole gav
break;
}
}
item.getItemContext().put( Gav.class.getName(), gav );
if ( gav.getSnapshotTimeStamp() != null )
{
getLogger().debug( "Using GAV snapshot timestamp" );
long itemTimestamp = gav.getSnapshotTimeStamp().longValue();
getLogger().debug( "NOW is " + itemTimestamp );
// If this timestamp is already marked to be removed, junk it
if ( versionsToRemove.contains( new Long( itemTimestamp ) ) )
{
addStorageFileItemToMap( deletableSnapshotsAndFiles, gav, (StorageFileItem) item );
}
else
{
getLogger().debug( "itemTimestamp=" + itemTimestamp + ", dateTreshold=" + dateThreshold );
// if dateTreshold is not used (zero days) OR
// if itemTimestamp is less then dateTreshold (NB: both are positive!)
// below will the retentionCount overrule if needed this
if ( -1 == dateThreshold || itemTimestamp < dateThreshold )
{
versionsToRemove.add( new Long( itemTimestamp ) );
addStorageFileItemToMap( deletableSnapshotsAndFiles, gav, (StorageFileItem) item );
}
else
{
addStorageFileItemToMap( remainingSnapshotsAndFiles, gav, (StorageFileItem) item );
}
}
}
else
{
// If no timestamp on gav, then it is a non-unique snapshot
// and should _not_ be removed
getLogger().debug( "GAV Snapshot timestamp not available, skipping non-unique snapshot" );
addStorageFileItemToMap( remainingSnapshotsAndFiles, gav, (StorageFileItem) item );
}
}
}
}
// and doing the work here
if ( removeWholeGAV )
{
try
{
for ( StorageItem item : items )
{
try
{
// preserve possible subdirs
if ( !( item instanceof StorageCollectionItem ) )
{
repository.deleteItem( false, new ResourceStoreRequest( item ) );
}
}
catch ( ItemNotFoundException e )
{
if ( getLogger().isDebugEnabled() )
{
getLogger().debug(
"Could not delete whole GAV " + coll.getRepositoryItemUid().toString(), e );
}
}
}
}
catch ( Exception e )
{
getLogger().warn( "Could not delete whole GAV " + coll.getRepositoryItemUid().toString(), e );
}
}
else
{
// and now check some things
if ( remainingSnapshotsAndFiles.size() < request.getMinCountOfSnapshotsToKeep() )
{
// do something
if ( remainingSnapshotsAndFiles.size() + deletableSnapshotsAndFiles.size() < request.getMinCountOfSnapshotsToKeep() )
{
// delete nothing, since there is less snapshots in total as allowed
deletableSnapshotsAndFiles.clear();
}
else
{
TreeSet<Version> keys = new TreeSet<Version>( deletableSnapshotsAndFiles.keySet() );
while ( !keys.isEmpty()
&& remainingSnapshotsAndFiles.size() < request.getMinCountOfSnapshotsToKeep() )
{
Version keyToMove = keys.last();
if ( remainingSnapshotsAndFiles.containsKey( keyToMove ) )
{
remainingSnapshotsAndFiles.get( keyToMove ).addAll(
deletableSnapshotsAndFiles.get( keyToMove ) );
}
else
{
remainingSnapshotsAndFiles.put( keyToMove, deletableSnapshotsAndFiles.get( keyToMove ) );
}
deletableSnapshotsAndFiles.remove( keyToMove );
keys.remove( keyToMove );
}
}
}
// NEXUS-814: is this GAV have remaining artifacts?
boolean gavHasMoreTimestampedSnapshots = remainingSnapshotsAndFiles.size() > 0;
for ( Version key : deletableSnapshotsAndFiles.keySet() )
{
List<StorageFileItem> files = deletableSnapshotsAndFiles.get( key );
deletedSnapshots++;
for ( StorageFileItem file : files )
{
try
{
// NEXUS-814: mark that we are deleting a TS snapshot, but there are still remaining
// ones in repository.
if ( gavHasMoreTimestampedSnapshots )
{
file.getItemContext().put( MORE_TS_SNAPSHOTS_EXISTS_FOR_GAV, Boolean.TRUE );
}
gav = (Gav) file.getItemContext().get( Gav.class.getName() );
repository.deleteItem( false, new ResourceStoreRequest( file ) );
deletedFiles++;
}
catch ( ItemNotFoundException e )
{
if ( getLogger().isDebugEnabled() )
{
getLogger().debug( "Could not delete file:", e );
}
}
catch ( Exception e )
{
getLogger().info( "Could not delete file:", e );
}
}
}
}
removeDirectoryIfEmpty( coll );
updateMetadataIfNecessary( context, coll );
}
private void updateMetadataIfNecessary( WalkerContext context, StorageCollectionItem coll )
throws Exception
{
// all snapshot files are deleted
if ( !deletableSnapshotsAndFiles.isEmpty() && remainingSnapshotsAndFiles.isEmpty() )
{
String parentPath = ItemPathUtils.getParentPath( coll.getPath() );
request.getMetadataRebuildPaths().add( parentPath );
}
else
{
request.getMetadataRebuildPaths().add( coll.getPath() );
}
}
private void removeDirectoryIfEmpty( StorageCollectionItem coll )
throws StorageException, IllegalOperationException, UnsupportedStorageOperationException
{
try
{
if ( repository.list( false, coll ).size() > 0 )
{
return;
}
if ( getLogger().isDebugEnabled() )
{
getLogger().debug(
"Removing the empty directory leftover: UID=" + coll.getRepositoryItemUid().toString() );
}
repository.deleteItem( false, new ResourceStoreRequest( coll ) );
}
catch ( ItemNotFoundException e )
{
// silent, this happens if whole GAV is removed and the dir is removed too
}
}
public boolean releaseExistsForSnapshot( Gav snapshotGav, Map<String, Object> context )
{
for ( Repository repository : repositoryRegistry.getRepositories() )
{
// we need to filter for:
// repository that is MavenRepository and is hosted or proxy
// repository that has release policy
if ( repository.getRepositoryKind().isFacetAvailable( MavenHostedRepository.class )
|| repository.getRepositoryKind().isFacetAvailable( MavenProxyRepository.class ) )
{
// actually, we don't care is it proxy or hosted, we only need to filter out groups and other
// "composite" reposes like shadows
MavenRepository mrepository = repository.adaptToFacet( MavenRepository.class );
// look in release reposes only
if ( mrepository.isUserManaged()
&& RepositoryPolicy.RELEASE.equals( mrepository.getRepositoryPolicy() ) )
{
try
{
String releaseVersion = null;
// NEXUS-3148
if ( snapshotGav.getBaseVersion().endsWith( "-SNAPSHOT" ) )
{
// "-SNAPSHOT" :== 9 chars
releaseVersion =
snapshotGav.getBaseVersion().substring( 0,
snapshotGav.getBaseVersion().length() - 9 );
}
else
{
// "SNAPSHOT" :== 8 chars
releaseVersion =
snapshotGav.getBaseVersion().substring( 0,
snapshotGav.getBaseVersion().length() - 8 );
}
Gav releaseGav =
new Gav( snapshotGav.getGroupId(), snapshotGav.getArtifactId(), releaseVersion,
snapshotGav.getClassifier(), snapshotGav.getExtension(), null, null, null,
false, null, false, null );
String path = mrepository.getGavCalculator().gavToPath( releaseGav );
ResourceStoreRequest req = new ResourceStoreRequest( path, true );
req.getRequestContext().putAll( context );
mrepository.retrieveItem( false, req );
return true;
}
catch ( ItemNotFoundException e )
{
// nothing
}
catch ( Exception e )
{
// nothing
}
}
}
}
return false;
}
public int getDeletedSnapshots()
{
return deletedSnapshots;
}
public int getDeletedFiles()
{
return deletedFiles;
}
}
}