Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Job for purging unused releases #29

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Job for purging unused releases
NANTCHOUANG Cyrille authored and NANTCHOUANG Cyrille committed Jan 24, 2018
commit 9d375a3b64a18268f14bdc63ae1399fc69b4c11a
Original file line number Diff line number Diff line change
@@ -470,4 +470,18 @@ class TaskComponent
throw e
}
}

/**
* Retrieve a list of versions used for the purge of maven releases
*/
@DirectMethod
@Timed
@ExceptionMetered
@RequiresPermissions('nexus:tasks:read')
List<TaskOptionPurgeXO> readOptionsPurgeTask()
{
return [
new TaskOptionPurgeXO(name: 'version', description :"Version"),
new TaskOptionPurgeXO(name : 'dateRelease', description: "Release date")]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Sonatype Nexus (TM) Open Source Version
* Copyright (c) 2008-present Sonatype, Inc.
* All rights reserved. Includes the third-party code listed at http://links.sonatype.com/products/nexus/oss/attributions.
*
* This program and the accompanying materials are made available under the terms of the Eclipse Public License Version 1.0,
* which accompanies this distribution and is available at http://www.eclipse.org/legal/epl-v10.html.
*
* Sonatype Nexus (TM) Professional Version is available from Sonatype, Inc. "Sonatype" and "Sonatype Nexus" are trademarks
* of Sonatype, Inc. Apache Maven is a trademark of the Apache Software Foundation. M2eclipse is a trademark of the
* Eclipse Foundation. All other trademarks are the property of their respective owners.
*/
package org.sonatype.nexus.coreui

import groovy.transform.ToString

@ToString(includePackage = false, includeNames = true)
class TaskOptionPurgeXO {

String name

String description
}
Original file line number Diff line number Diff line change
@@ -33,7 +33,8 @@ Ext.define('NX.coreui.controller.Tasks', {
],
stores: [
'Task',
'TaskType'
'TaskType',
'TaskOptionPurge'
],
models: [
'Task'
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Sonatype Nexus (TM) Open Source Version
* Copyright (c) 2008-present Sonatype, Inc.
* All rights reserved. Includes the third-party code listed at http://links.sonatype.com/products/nexus/oss/attributions.
*
* This program and the accompanying materials are made available under the terms of the Eclipse Public License Version 1.0,
* which accompanies this distribution and is available at http://www.eclipse.org/legal/epl-v10.html.
*
* Sonatype Nexus (TM) Professional Version is available from Sonatype, Inc. "Sonatype" and "Sonatype Nexus" are trademarks
* of Sonatype, Inc. Apache Maven is a trademark of the Apache Software Foundation. M2eclipse is a trademark of the
* Eclipse Foundation. All other trademarks are the property of their respective owners.
*/
/**
* Task type model.
*
* @since 3.7
*/
Ext.define('NX.coreui.model.TaskOptionPurge', {
extend: 'Ext.data.Model',
fields: [
{name: 'name', type: 'string', sortType: 'asUCText'},
{name: 'description', type: 'string', sortType: 'asUCText'}
]
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Sonatype Nexus (TM) Open Source Version
* Copyright (c) 2008-present Sonatype, Inc.
* All rights reserved. Includes the third-party code listed at http://links.sonatype.com/products/nexus/oss/attributions.
*
* This program and the accompanying materials are made available under the terms of the Eclipse Public License Version 1.0,
* which accompanies this distribution and is available at http://www.eclipse.org/legal/epl-v10.html.
*
* Sonatype Nexus (TM) Professional Version is available from Sonatype, Inc. "Sonatype" and "Sonatype Nexus" are trademarks
* of Sonatype, Inc. Apache Maven is a trademark of the Apache Software Foundation. M2eclipse is a trademark of the
* Eclipse Foundation. All other trademarks are the property of their respective owners.
*/
/*global Ext, NX*/

/**
* Task type store.
*
* @since 3.0
*/
Ext.define('NX.coreui.store.TaskOptionPurge', {
extend: 'Ext.data.Store',
model: 'NX.coreui.model.TaskOptionPurge',

proxy: {
type: 'direct',
paramsAsHash: false,

api: {
read: 'NX.direct.coreui_Task.readOptionsPurgeTask'
},

reader: {
type: 'json',
root: 'data',
idProperty : 'name',
successProperty: 'success'
}
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Sonatype Nexus (TM) Open Source Version
* Copyright (c) 2008-present Sonatype, Inc.
* All rights reserved. Includes the third-party code listed at http://links.sonatype.com/products/nexus/oss/attributions.
*
* This program and the accompanying materials are made available under the terms of the Eclipse Public License Version 1.0,
* which accompanies this distribution and is available at http://www.eclipse.org/legal/epl-v10.html.
*
* Sonatype Nexus (TM) Professional Version is available from Sonatype, Inc. "Sonatype" and "Sonatype Nexus" are trademarks
* of Sonatype, Inc. Apache Maven is a trademark of the Apache Software Foundation. M2eclipse is a trademark of the
* Eclipse Foundation. All other trademarks are the property of their respective owners.
*/
package org.sonatype.nexus.repository.maven;

import org.sonatype.nexus.repository.Facet;

/**
* Facet for purging unused Maven releases.
*
* @since 3.7
*/
@Facet.Exposed
public interface PurgeUnusedReleasesFacet extends Facet {


/**
* Purge the unused releases and keep the number of releases in parameter
* @param numberOfReleasesToKeep - the number of releases to keep
* @param option - Option used for purge
*/
void purgeUnusedReleases(int numberOfReleasesToKeep, String option);

}
Original file line number Diff line number Diff line change
@@ -27,12 +27,14 @@
import javax.annotation.Nonnull;
import javax.annotation.Nullable;

import org.sonatype.nexus.common.collect.NestedAttributesMap;
import org.sonatype.nexus.common.hash.HashAlgorithm;
import org.sonatype.nexus.repository.Repository;
import org.sonatype.nexus.repository.maven.MavenFacet;
import org.sonatype.nexus.repository.maven.MavenPath;
import org.sonatype.nexus.repository.maven.MavenPath.Coordinates;
import org.sonatype.nexus.repository.maven.MavenPath.HashType;
import org.sonatype.nexus.repository.maven.internal.hosted.metadata.MetadataUtils;
import org.sonatype.nexus.repository.storage.Asset;
import org.sonatype.nexus.repository.storage.Bucket;
import org.sonatype.nexus.repository.storage.Component;
@@ -47,10 +49,13 @@
import com.google.common.hash.HashCode;
import com.google.common.hash.HashingOutputStream;
import org.joda.time.DateTime;
import org.sonatype.nexus.transaction.UnitOfWork;

import static java.util.Collections.singletonList;
import static org.sonatype.nexus.common.app.VersionComparator.version;
import static org.sonatype.nexus.repository.maven.internal.Attributes.P_ARTIFACT_ID;
import static org.sonatype.nexus.repository.maven.internal.Attributes.P_BASE_VERSION;
import static org.sonatype.nexus.repository.maven.internal.Attributes.P_GROUP_ID;
import static org.sonatype.nexus.repository.maven.internal.Constants.SNAPSHOT_VERSION_SUFFIX;
import static org.sonatype.nexus.repository.storage.ComponentEntityAdapter.P_GROUP;
import static org.sonatype.nexus.repository.storage.ComponentEntityAdapter.P_VERSION;
@@ -219,4 +224,27 @@ public static boolean deleteWithHashes(final MavenFacet mavenFacet, final MavenP
}
return mavenFacet.delete(paths.toArray(new MavenPath[paths.size()]));
}

public static String deleteComponent(final StorageTx tx,
MavenFacet facet,
Component component) {
tx.deleteComponent(component);

NestedAttributesMap attributes = component.formatAttributes();
String groupId = attributes.get(P_GROUP_ID, String.class);
String artifactId = attributes.get(P_ARTIFACT_ID, String.class);
String baseVersion = attributes.get(P_BASE_VERSION, String.class);

try {
// We have to delete all metadata through GAV levels and rebuild in the next step, as the MetadataRebuilder
// isn't meant to remove metadata that has been orphaned by the deletion of a component
MavenFacetUtils.deleteWithHashes(facet, MetadataUtils.metadataPath(groupId, artifactId, baseVersion));
MavenFacetUtils.deleteWithHashes(facet, MetadataUtils.metadataPath(groupId, artifactId, null));
MavenFacetUtils.deleteWithHashes(facet, MetadataUtils.metadataPath(groupId, null, null));
}
catch (IOException e) {
throw new RuntimeException(e);
}
return groupId;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
/*
* Sonatype Nexus (TM) Open Source Version
* Copyright (c) 2008-present Sonatype, Inc.
* All rights reserved. Includes the third-party code listed at http://links.sonatype.com/products/nexus/oss/attributions.
*
* This program and the accompanying materials are made available under the terms of the Eclipse Public License Version 1.0,
* which accompanies this distribution and is available at http://www.eclipse.org/legal/epl-v10.html.
*
* Sonatype Nexus (TM) Professional Version is available from Sonatype, Inc. "Sonatype" and "Sonatype Nexus" are trademarks
* of Sonatype, Inc. Apache Maven is a trademark of the Apache Software Foundation. M2eclipse is a trademark of the
* Eclipse Foundation. All other trademarks are the property of their respective owners.
*/
package org.sonatype.nexus.repository.maven.internal;

import com.google.common.collect.Lists;
import com.orientechnologies.orient.core.command.script.OCommandScript;
import com.orientechnologies.orient.core.id.ORID;
import com.orientechnologies.orient.core.record.impl.ODocument;
import org.sonatype.nexus.common.stateguard.Guarded;
import org.sonatype.nexus.repository.FacetSupport;
import org.sonatype.nexus.repository.maven.MavenFacet;
import org.sonatype.nexus.repository.maven.PurgeUnusedReleasesFacet;
import org.sonatype.nexus.repository.storage.Component;
import org.sonatype.nexus.repository.storage.Query;
import org.sonatype.nexus.repository.storage.StorageFacet;
import org.sonatype.nexus.repository.storage.StorageTx;
import org.sonatype.nexus.repository.transaction.TransactionalDeleteBlob;
import org.sonatype.nexus.repository.transaction.TransactionalStoreMetadata;
import org.sonatype.nexus.scheduling.CancelableHelper;
import org.sonatype.nexus.scheduling.TaskInterruptedException;
import org.sonatype.nexus.transaction.UnitOfWork;

import javax.inject.Named;
import java.util.*;

import static java.util.Collections.*;
import static org.sonatype.nexus.orient.entity.AttachedEntityHelper.id;
import static org.sonatype.nexus.repository.FacetSupport.State.STARTED;
import static org.sonatype.nexus.repository.maven.internal.QueryPurgeReleasesBuilder.DATE_RELEASE_OPTION;
import static org.sonatype.nexus.repository.maven.internal.QueryPurgeReleasesBuilder.VERSION_OPTION;

@Named
public class PurgeUnusedReleasesFacetImpl extends FacetSupport
implements PurgeUnusedReleasesFacet {


private static final String MESSAGE_PURGE_NOT_EXECUTED = "The purge of the releases {}.{} in the repository {} cannot be done because the number of existing releases is below the number of releases to keep";

static final int PAGINATION_LIMIT = 10;

private final String REQUEST_TOTAL_RELEASES_BY_RELEASE = "select count(*), attributes.maven2.groupId as groupId, attributes.maven2.artifactId as artifactId from component where bucket = %s " +
" group by attributes.maven2.artifactId, attributes.maven2.groupId limit %s";


@Override
@Guarded(by = STARTED)
public void purgeUnusedReleases(int numberOfReleasesToKeep, String option) {
if (optionalFacet(StorageFacet.class).isPresent()) {
TransactionalStoreMetadata.operation.withDb(facet(StorageFacet.class).txSupplier()).call(() -> {
final StorageTx tx = UnitOfWork.currentTx();
ORID bucketId = id(Objects.requireNonNull(tx.findBucket(getRepository())));
List<QueryResultForNumberOfReleases> listReleases = listNumberOfReleases(tx, bucketId);
for (QueryResultForNumberOfReleases q : listReleases) {
long nbReleasesToPurge = q.getCount() - numberOfReleasesToKeep;
String repositoryName = getRepository().getName();
if (nbReleasesToPurge <= 0) {
log.debug(MESSAGE_PURGE_NOT_EXECUTED, q.getGroupId(), q.getArtifactId(), repositoryName);
} else {
log.debug("Number of releases to purge for the repository {} : {} ", nbReleasesToPurge, repositoryName);
process(tx, q.getGroupId(), q.getArtifactId(), option, nbReleasesToPurge, bucketId);
}
}

return null;
});
}
}


/**
* Processing the purge
* @param groupId - Group Id of the component
* @param artifactId - Artifact Id of the component
* @param option - Option used to order by for purge
* @param bucketId - The bucket id
*/
private void process(final StorageTx tx, String groupId, String artifactId, String option, long nbReleasesToPurge, ORID bucketId) {
//First retrieve the last component of the releases which have been not purged
String lastComponentVersion = null;
Date lastReleaseDate = null;
List<Component> components = retrieveReleases(groupId, artifactId, option, nbReleasesToPurge, bucketId);
if (VERSION_OPTION.equals(option)) {
lastComponentVersion = getLastComponentVersion(components);
} else if (DATE_RELEASE_OPTION.equals(option)) {
lastReleaseDate = getLastComponentReleaseDate(components);
}
//
int n = 0;

while (n < nbReleasesToPurge && !isCanceled()) {
List<Component> filteredComponents = retrieveReleases(groupId,
artifactId,
option,
PAGINATION_LIMIT,
lastComponentVersion,
lastReleaseDate,
"desc",
bucketId);
int totalComponents = filteredComponents.size();
log.debug("{} components will be purged ", totalComponents);
lastComponentVersion = getLastComponentVersion(filteredComponents);

for (Component component : filteredComponents) {
if (isCanceled()) {
break;
}
deleteComponent(component);
}

tx.commit();
tx.begin();

n += totalComponents;
}
}

private List<Component> retrieveReleases(String groupId,
String artifactId,
String option,
long pagination,
String lastComponentVersion,
Date lastReleaseDate,
String order,
ORID bucketId) {
StorageTx tx = UnitOfWork.currentTx();
QueryPurgeReleasesBuilder queryPurgeReleasesBuilder = null;
if (VERSION_OPTION.equals(option)) {
queryPurgeReleasesBuilder = QueryPurgeReleasesBuilder.buildQueryForVersionOption(bucketId,
groupId, artifactId, lastComponentVersion, pagination, order);
} else if (DATE_RELEASE_OPTION.equals(option)) {
queryPurgeReleasesBuilder = QueryPurgeReleasesBuilder.buildQueryForReleaseDateOption(bucketId,
groupId, artifactId, lastReleaseDate, pagination, order);
}

log.debug("Query executed {} ", Objects.requireNonNull(queryPurgeReleasesBuilder).toString());

Iterable<Component> components = tx.findComponents(queryPurgeReleasesBuilder.getWhereClause(),
queryPurgeReleasesBuilder.getQueryParams(),
singletonList(getRepository()),
queryPurgeReleasesBuilder.getQuerySuffix());
return Lists.newArrayList(components);

}

public List<Component> retrieveReleases(String groupId, String artifactId, String option, long pagination, ORID bucketId) {
return retrieveReleases(groupId, artifactId, option, pagination, null, null, "asc", bucketId);
}

public long countTotalReleases(String groupId, String artifactId, ORID bucketId) {
long nbComponents;
StorageTx tx = UnitOfWork.currentTx();
QueryPurgeReleasesBuilder queryPurgeReleasesBuilder = QueryPurgeReleasesBuilder.buildQueryForCount(bucketId,
groupId,
artifactId);
nbComponents = tx.countComponents(queryPurgeReleasesBuilder.getWhereClause(),
queryPurgeReleasesBuilder.getQueryParams(),
singletonList(getRepository()),
queryPurgeReleasesBuilder.getQuerySuffix());
log.debug("Total number of releases components for {} {} int the repository {} : {} ", groupId, artifactId, getRepository().getName(), nbComponents);
return nbComponents;
}


@TransactionalDeleteBlob
private void deleteComponent(final Component component) {
log.debug("Deleting unused released component {}", component);
StorageTx tx = UnitOfWork.currentTx();
MavenFacetUtils.deleteComponent(tx, facet(MavenFacet.class), component);
}

public String getLastComponentVersion(List<Component> components) {
return components.get(components.size() - 1).version();
}

private Date getLastComponentReleaseDate(List<Component> components) {
return components.get(components.size() - 1).lastUpdated().toDate();
}

private boolean isCanceled() {
try {
CancelableHelper.checkCancellation();
return false;
} catch (TaskInterruptedException e) {
log.warn("Purge unused Maven releases job is canceled");
return true;
}
}

/**
* List the count of releases group by groupId/artifactId in a repository
* @param tx
* @param bucketId
* @return
*/
public List<QueryResultForNumberOfReleases> listNumberOfReleases(final StorageTx tx,
ORID bucketId) {
List<QueryResultForNumberOfReleases> releases = new ArrayList<>();
long totalComponents = tx.countComponents(Query.builder().where("1").eq(1).build(), Collections.singletonList(getRepository()));
log.debug("Bucket Id {} , total components {}", bucketId, totalComponents);
String query = String.format(REQUEST_TOTAL_RELEASES_BY_RELEASE, bucketId,
totalComponents != 0 ? totalComponents : -1);
List<ODocument> documentList = tx.getDb().command(new OCommandScript("sql", query)).execute();

for (ODocument document : documentList) {
releases.add(new QueryResultForNumberOfReleases(document.field("groupId"),
document.field("artifactId"),
document.field("count")));
}
return releases;

}
}
Original file line number Diff line number Diff line change
@@ -263,25 +263,8 @@ private List<Component> findNextPageOfUnusedSnapshots(final StorageTx tx,

private String deleteComponent(final Component component) {
log.debug("Deleting unused snapshot component {}", component);
MavenFacet facet = facet(MavenFacet.class);
final StorageTx tx = UnitOfWork.currentTx();
tx.deleteComponent(component);

NestedAttributesMap attributes = component.formatAttributes();
String groupId = attributes.get(P_GROUP_ID, String.class);
String artifactId = attributes.get(P_ARTIFACT_ID, String.class);
String baseVersion = attributes.get(P_BASE_VERSION, String.class);

try {
// We have to delete all metadata through GAV levels and rebuild in the next step, as the MetadataRebuilder
// isn't meant to remove metadata that has been orphaned by the deletion of a component
MavenFacetUtils.deleteWithHashes(facet, MetadataUtils.metadataPath(groupId, artifactId, baseVersion));
MavenFacetUtils.deleteWithHashes(facet, MetadataUtils.metadataPath(groupId, artifactId, null));
MavenFacetUtils.deleteWithHashes(facet, MetadataUtils.metadataPath(groupId, null, null));
}
catch (IOException e) {
throw new RuntimeException(e);
}
String groupId = MavenFacetUtils.deleteComponent(tx, facet(MavenFacet.class), component);
return groupId;
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/*
* Sonatype Nexus (TM) Open Source Version
* Copyright (c) 2008-present Sonatype, Inc.
* All rights reserved. Includes the third-party code listed at http://links.sonatype.com/products/nexus/oss/attributions.
*
* This program and the accompanying materials are made available under the terms of the Eclipse Public License Version 1.0,
* which accompanies this distribution and is available at http://www.eclipse.org/legal/epl-v10.html.
*
* Sonatype Nexus (TM) Professional Version is available from Sonatype, Inc. "Sonatype" and "Sonatype Nexus" are trademarks
* of Sonatype, Inc. Apache Maven is a trademark of the Apache Software Foundation. M2eclipse is a trademark of the
* Eclipse Foundation. All other trademarks are the property of their respective owners.
*/
package org.sonatype.nexus.repository.maven.internal;

import com.orientechnologies.orient.core.id.ORID;
import org.sonatype.nexus.repository.Repository;
import org.sonatype.nexus.repository.storage.StorageTx;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
* Class used for build orient db sql request for purge unused releases
*/
public class QueryPurgeReleasesBuilder {

static final String VERSION_OPTION = "version";
static final String DATE_RELEASE_OPTION = "dateRelease";

private Map<String, Object> queryParams;
private String whereClause;
private String querySuffix;


private QueryPurgeReleasesBuilder(Map<String, Object> queryParams, String whereClause, String querySuffix) {
this.queryParams = queryParams;
this.whereClause = whereClause;
this.querySuffix = querySuffix;
}

/**
* Build a query with thoses parameters
*
* @param bucketId
* @param groupId - Group Id of the release
* @param artifactId - Artifact Id of the release
* @param pagination - Pagination limit
* @param isCount - Boolean which defines if is a count request
* @param orderBy - The criteria for the order
* @param sort - The criteria for the sort
* @return the query builded
*/
private static QueryPurgeReleasesBuilder buildQuery(ORID bucketId, String groupId,
String artifactId,
Long pagination,
Boolean isCount,
String orderBy,
String sort) {
Map<String, Object> queryParameters = new HashMap<>();
queryParameters.put("bucketId", bucketId);
queryParameters.put("groupId", groupId);
queryParameters.put("artifactId", artifactId);
queryParameters.put("criteriaSnapshot", "%SNAPSHOT%");
String whereClause = "bucket = :bucketId and attributes.maven2.groupId = :groupId and " +
" attributes.maven2.artifactId = :artifactId and not(attributes.maven2.baseVersion.toUpperCase() like :criteriaSnapshot)";
StringBuilder querySuffixBuilder = new StringBuilder("");
if (!isCount) {
querySuffixBuilder.append(orderBy);
querySuffixBuilder.append(sort);
querySuffixBuilder.append(" limit ");
querySuffixBuilder.append(pagination);
}



return new QueryPurgeReleasesBuilder(queryParameters, whereClause, isCount ? null : querySuffixBuilder.toString());
}

public static QueryPurgeReleasesBuilder buildQueryForVersionOption(ORID bucketId, String groupId,
String artifactId,
String lastComponentVersion,
Long pagination,
String sort) {
QueryPurgeReleasesBuilder buildedQuery = buildQuery(bucketId,
groupId,
artifactId,
pagination, false,
"order by attributes.maven2.baseVersion ",
sort);
if (lastComponentVersion != null) {
buildedQuery.addFilterInQueryBuilder("lastComponentVersion", lastComponentVersion,
" and version <= : lastComponentVersion");
}
return buildedQuery;
}

public static QueryPurgeReleasesBuilder buildQueryForReleaseDateOption(ORID bucketId,
String groupId,
String artifactId,
Date lastReleaseDate,
Long pagination,
String sort) {
QueryPurgeReleasesBuilder buildedQuery = buildQuery(bucketId,
groupId, artifactId, pagination, false, "order by last_updated ", sort);
if (lastReleaseDate != null) {
buildedQuery.addFilterInQueryBuilder("lastReleaseDate", lastReleaseDate, " and last_updated <= :lastReleaseDate");
}
return buildedQuery;
}

public static QueryPurgeReleasesBuilder buildQueryForCount(ORID bucketId, String groupId, String artifactId) {
return buildQuery(bucketId, groupId, artifactId, null, true, null, null);
}

public Map<String, Object> getQueryParams() {
return queryParams;
}

public String getQuerySuffix() {
return querySuffix;
}

public String getWhereClause() {
return whereClause;
}

private void addFilterInQueryBuilder(String filteredItem, Object filteredData, String suffixWhereClause) {
queryParams.put(filteredItem, filteredData);
whereClause += suffixWhereClause;
}

@Override
public String toString() {
return "QueryPurgeReleasesBuilder{" +
"queryParams=" + queryParams +
", whereClause='" + whereClause + '\'' +
", querySuffix='" + querySuffix + '\'' +
'}';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Sonatype Nexus (TM) Open Source Version
* Copyright (c) 2008-present Sonatype, Inc.
* All rights reserved. Includes the third-party code listed at http://links.sonatype.com/products/nexus/oss/attributions.
*
* This program and the accompanying materials are made available under the terms of the Eclipse Public License Version 1.0,
* which accompanies this distribution and is available at http://www.eclipse.org/legal/epl-v10.html.
*
* Sonatype Nexus (TM) Professional Version is available from Sonatype, Inc. "Sonatype" and "Sonatype Nexus" are trademarks
* of Sonatype, Inc. Apache Maven is a trademark of the Apache Software Foundation. M2eclipse is a trademark of the
* Eclipse Foundation. All other trademarks are the property of their respective owners.
*/
package org.sonatype.nexus.repository.maven.internal;

import java.util.Objects;

public class QueryResultForNumberOfReleases {

private final String groupId;
private final String artifactId;
private final Long count;


public QueryResultForNumberOfReleases(String groupId, String artifactId, Long count) {
this.groupId = groupId;
this.artifactId = artifactId;
this.count = count;
}

public String getGroupId() {
return groupId;
}

public String getArtifactId() {
return artifactId;
}

public Long getCount() {
return count;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
QueryResultForNumberOfReleases that = (QueryResultForNumberOfReleases) o;
return count == that.count &&
Objects.equals(groupId, that.groupId) &&
Objects.equals(artifactId, that.artifactId);
}

@Override
public int hashCode() {

return Objects.hash(groupId, artifactId, count);
}

@Override
public String toString() {
return "QueryResultForNumberOfReleases{" +
"groupId='" + groupId + '\'' +
", artifactId='" + artifactId + '\'' +
", count=" + count +
'}';
}
}
Original file line number Diff line number Diff line change
@@ -12,32 +12,29 @@
*/
package org.sonatype.nexus.repository.maven.internal.recipes

import javax.annotation.Nonnull
import javax.inject.Inject
import javax.inject.Named
import javax.inject.Provider
import javax.inject.Singleton

import org.sonatype.nexus.repository.Format
import org.sonatype.nexus.repository.Repository
import org.sonatype.nexus.repository.Type
import org.sonatype.nexus.repository.maven.MavenPathParser
import org.sonatype.nexus.repository.maven.PurgeUnusedReleasesFacet
import org.sonatype.nexus.repository.maven.PurgeUnusedSnapshotsFacet
import org.sonatype.nexus.repository.maven.RemoveSnapshotsFacet
import org.sonatype.nexus.repository.maven.internal.Maven2Format
import org.sonatype.nexus.repository.maven.internal.MavenSecurityFacet
import org.sonatype.nexus.repository.maven.internal.VersionPolicyHandler
import org.sonatype.nexus.repository.maven.internal.hosted.ArchetypeCatalogHandler
import org.sonatype.nexus.repository.maven.internal.hosted.HostedHandler
import org.sonatype.nexus.repository.maven.internal.hosted.MavenHostedComponentMaintenanceFacet
import org.sonatype.nexus.repository.maven.internal.hosted.MavenHostedFacetImpl
import org.sonatype.nexus.repository.maven.internal.hosted.MavenHostedIndexFacet
import org.sonatype.nexus.repository.maven.internal.hosted.*
import org.sonatype.nexus.repository.search.SearchFacet
import org.sonatype.nexus.repository.types.HostedType
import org.sonatype.nexus.repository.view.ConfigurableViewFacet
import org.sonatype.nexus.repository.view.Router
import org.sonatype.nexus.repository.view.ViewFacet

import javax.annotation.Nonnull
import javax.inject.Inject
import javax.inject.Named
import javax.inject.Provider
import javax.inject.Singleton

import static org.sonatype.nexus.repository.http.HttpHandlers.notFound

/**
@@ -79,6 +76,10 @@ class Maven2HostedRecipe
@Inject
Provider<RemoveSnapshotsFacet> removeSnapshotsFacet


@Inject
Provider<PurgeUnusedReleasesFacet> mavenPurgeReleasedFacet

@Inject
Maven2HostedRecipe(@Named(HostedType.NAME) final Type type,
@Named(Maven2Format.NAME) final Format format,
@@ -101,6 +102,7 @@ class Maven2HostedRecipe
repository.attach(mavenPurgeSnapshotsFacet.get())
repository.attach(removeSnapshotsFacet.get())
repository.attach(configure(viewFacet.get()))
repository.attach(mavenPurgeReleasedFacet.get())
}

private ViewFacet configure(final ConfigurableViewFacet facet) {
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Sonatype Nexus (TM) Open Source Version
* Copyright (c) 2008-present Sonatype, Inc.
* All rights reserved. Includes the third-party code listed at http://links.sonatype.com/products/nexus/oss/attributions.
*
* This program and the accompanying materials are made available under the terms of the Eclipse Public License Version 1.0,
* which accompanies this distribution and is available at http://www.eclipse.org/legal/epl-v10.html.
*
* Sonatype Nexus (TM) Professional Version is available from Sonatype, Inc. "Sonatype" and "Sonatype Nexus" are trademarks
* of Sonatype, Inc. Apache Maven is a trademark of the Apache Software Foundation. M2eclipse is a trademark of the
* Eclipse Foundation. All other trademarks are the property of their respective owners.
*/
package org.sonatype.nexus.repository.maven.tasks;

import com.google.common.base.Strings;
import org.sonatype.nexus.repository.Format;
import org.sonatype.nexus.repository.Repository;
import org.sonatype.nexus.repository.RepositoryTaskSupport;
import org.sonatype.nexus.repository.Type;
import org.sonatype.nexus.repository.maven.MavenFacet;
import org.sonatype.nexus.repository.maven.PurgeUnusedReleasesFacet;
import org.sonatype.nexus.repository.maven.VersionPolicy;
import org.sonatype.nexus.repository.maven.internal.Maven2Format;
import org.sonatype.nexus.repository.types.HostedType;
import org.sonatype.nexus.scheduling.Cancelable;

import javax.inject.Inject;
import javax.inject.Named;

import static com.google.common.base.Preconditions.checkNotNull;

@Named
public class PurgeMavenUnusedReleasesTask extends RepositoryTaskSupport
implements Cancelable
{

public static final String NUMBER_RELEASES_TO_KEEP = "numberOfReleasesToKeep";

public static final String PURGE_UNUSED_MAVEN_RELEASES_MESSAGE = "Purge unused Maven releases versions in this repository %s";
public static final String OPTION_FOR_PURGE_ID = "optionForPurge";

private final Type hostedType;

private final Format maven2Format;

@Inject
public PurgeMavenUnusedReleasesTask(
@Named(HostedType.NAME) final Type hostedType,
@Named(Maven2Format.NAME) final Format maven2Format)
{
this.hostedType = checkNotNull(hostedType);
this.maven2Format = checkNotNull(maven2Format);
}

@Override
protected void execute(final Repository repository) {
String option = !Strings.isNullOrEmpty(getConfiguration().getString(OPTION_FOR_PURGE_ID)) ? getConfiguration().getString(OPTION_FOR_PURGE_ID) : "version";
int numberOfReleasesToKeep = getConfiguration().getInteger(NUMBER_RELEASES_TO_KEEP, 1);
repository.facet(PurgeUnusedReleasesFacet.class).purgeUnusedReleases(numberOfReleasesToKeep, option);

}

@Override
protected boolean appliesTo(final Repository repository) {
return maven2Format.equals(repository.getFormat())
&& hostedType.equals(repository.getType())
&& (repository.facet(MavenFacet.class).getVersionPolicy() == VersionPolicy.RELEASE
|| repository.facet(MavenFacet.class).getVersionPolicy() == VersionPolicy.MIXED);
}

@Override
public String getMessage() {
return String.format(PURGE_UNUSED_MAVEN_RELEASES_MESSAGE,
getRepositoryField()) ;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* Sonatype Nexus (TM) Open Source Version
* Copyright (c) 2008-present Sonatype, Inc.
* All rights reserved. Includes the third-party code listed at http://links.sonatype.com/products/nexus/oss/attributions.
*
* This program and the accompanying materials are made available under the terms of the Eclipse Public License Version 1.0,
* which accompanies this distribution and is available at http://www.eclipse.org/legal/epl-v10.html.
*
* Sonatype Nexus (TM) Professional Version is available from Sonatype, Inc. "Sonatype" and "Sonatype Nexus" are trademarks
* of Sonatype, Inc. Apache Maven is a trademark of the Apache Software Foundation. M2eclipse is a trademark of the
* Eclipse Foundation. All other trademarks are the property of their respective owners.
*/
package org.sonatype.nexus.repository.maven.tasks;

import org.sonatype.nexus.formfields.*;
import org.sonatype.nexus.repository.maven.PurgeUnusedReleasesFacet;
import org.sonatype.nexus.repository.maven.VersionPolicy;
import org.sonatype.nexus.repository.maven.internal.Maven2Format;
import org.sonatype.nexus.repository.types.HostedType;
import org.sonatype.nexus.scheduling.TaskDescriptorSupport;

import javax.inject.Named;
import javax.inject.Singleton;

import static org.sonatype.nexus.repository.RepositoryTaskSupport.REPOSITORY_NAME_FIELD_ID;
import static org.sonatype.nexus.repository.maven.tasks.PurgeMavenUnusedReleasesTask.*;

/**
* Task descriptor for {@link PurgeMavenUnusedReleasesTask}.
*
* @since 3.7.0
*/
@Named
@Singleton
public class PurgeMavenUnusedReleasesTaskDescriptor extends TaskDescriptorSupport {

public static final String TASK_NAME = "Purge unused Maven releases";

public static final String TYPE_ID = "repository.maven.purge-unused-releases";

public static final Number LAST_USED_INIT_VALUE = 1;

public static final Number LAST_USED_MIN_VALUE = 1;

public PurgeMavenUnusedReleasesTaskDescriptor() {
super(TYPE_ID,
PurgeMavenUnusedReleasesTask.class,
TASK_NAME,
VISIBLE,
EXPOSED,
new RepositoryCombobox(
REPOSITORY_NAME_FIELD_ID,
"Repository",
"Select the hosted maven repository to purge unused releases versions from",
FormField.MANDATORY
).includingAnyOfFacets(PurgeUnusedReleasesFacet.class).includingAnyOfFormats(Maven2Format.NAME)
.includingAnyOfTypes(HostedType.NAME).includeAnEntryForAllRepositories().excludingAnyOfVersionPolicies(VersionPolicy.SNAPSHOT.name())
,
new ComboboxFormField(OPTION_FOR_PURGE_ID,
"Option used for the purge",
"Select the option which going to be used for the purge",
true)
.withStoreApi("coreui_Task.readOptionsPurgeTask")
.withIdMapping("name").withNameMapping("description"),
new NumberTextFormField(
NUMBER_RELEASES_TO_KEEP,
"Number of releases",
"Number of releases to keep",
FormField.MANDATORY
).withInitialValue(LAST_USED_INIT_VALUE).withMinimumValue(LAST_USED_MIN_VALUE)
);
}
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* Sonatype Nexus (TM) Open Source Version
* Copyright (c) 2008-present Sonatype, Inc.
* All rights reserved. Includes the third-party code listed at http://links.sonatype.com/products/nexus/oss/attributions.
*
* This program and the accompanying materials are made available under the terms of the Eclipse Public License Version 1.0,
* which accompanies this distribution and is available at http://www.eclipse.org/legal/epl-v10.html.
*
* Sonatype Nexus (TM) Professional Version is available from Sonatype, Inc. "Sonatype" and "Sonatype Nexus" are trademarks
* of Sonatype, Inc. Apache Maven is a trademark of the Apache Software Foundation. M2eclipse is a trademark of the
* Eclipse Foundation. All other trademarks are the property of their respective owners.
*/
package org.sonatype.nexus.repository.maven.tasks

import org.junit.Before
import org.junit.Test
import org.mockito.Matchers
import org.sonatype.goodies.testsupport.TestSupport
import org.sonatype.nexus.repository.Repository
import org.sonatype.nexus.repository.RepositoryTaskSupport
import org.sonatype.nexus.repository.manager.RepositoryManager
import org.sonatype.nexus.repository.maven.PurgeUnusedReleasesFacet
import org.sonatype.nexus.repository.maven.internal.Maven2Format
import org.sonatype.nexus.repository.types.GroupType
import org.sonatype.nexus.repository.types.HostedType
import org.sonatype.nexus.scheduling.TaskConfiguration

import static org.mockito.Mockito.mock
import static org.mockito.Mockito.times
import static org.mockito.Mockito.verify
import static org.mockito.Mockito.when
import static org.sonatype.nexus.repository.maven.tasks.PurgeMavenUnusedReleasesTask.PURGE_UNUSED_MAVEN_RELEASES_MESSAGE

class PurgeMavenUnusedReleasesTaskTest extends TestSupport {

public PurgeMavenUnusedReleasesTask task

private Repository repository

@Before
void setup() {
task = new PurgeMavenUnusedReleasesTask(new HostedType(),
new Maven2Format())
TaskConfiguration configuration = new TaskConfiguration()
configuration.setId(PurgeMavenUnusedReleasesTaskDescriptor.TYPE_ID)
configuration.setTypeId(PurgeMavenUnusedReleasesTaskDescriptor.TYPE_ID)
configuration.setString(RepositoryTaskSupport.REPOSITORY_NAME_FIELD_ID, "my-maven-repo")
task.configure(configuration)

repository = mock(Repository.class)
HostedType hostedType = mock(HostedType.class)
when(repository.getType()).thenReturn(hostedType)
when(repository.getName()).thenReturn("my-maven-repo")
}

@Test
void "Test message of the task"() {


when:
String message = task.getMessage()

then:
message == String.format(PURGE_UNUSED_MAVEN_RELEASES_MESSAGE,
"my-maven-repo")
}

@Test
void "Test verify if the task could be apply on the repository of type"() {

when:
Boolean appliesTo = task.appliesTo(repository)

then:
appliesTo == false

and:
Maven2Format maven2Format = mock(Maven2Format.class)
when(repository.getFormat()).thenReturn(maven2Format)

then:
appliesTo == true
}

@Test
void "Test the execution of the purge when the task is executed"() {

given:
RepositoryManager repositoryManager = mock(RepositoryManager.class)
Maven2Format maven2Format = mock(Maven2Format.class)
when(repository.getFormat()).thenReturn(maven2Format)
HostedType hostedType = mock(HostedType.class)
when(repository.getType()).thenReturn(hostedType)
when(repositoryManager.get("my-maven-repo")).thenReturn(repository)
task.install(repositoryManager, new HostedType())
PurgeUnusedReleasesFacet purgeUnusedReleasesFacet = mock(PurgeUnusedReleasesFacet.class)
when(repository.facet(PurgeUnusedReleasesFacet.class)).thenReturn(purgeUnusedReleasesFacet)


when:
task.execute(repository)

then:
verify(purgeUnusedReleasesFacet, times(1)).purgeUnusedReleases( Matchers.anyInt(), Matchers.anyString())

}


}