diff --git a/plugins/nexus-coreui-plugin/src/main/java/org/sonatype/nexus/coreui/TaskComponent.groovy b/plugins/nexus-coreui-plugin/src/main/java/org/sonatype/nexus/coreui/TaskComponent.groovy index 54442c61cf..0bbfa1da28 100644 --- a/plugins/nexus-coreui-plugin/src/main/java/org/sonatype/nexus/coreui/TaskComponent.groovy +++ b/plugins/nexus-coreui-plugin/src/main/java/org/sonatype/nexus/coreui/TaskComponent.groovy @@ -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 readOptionsPurgeTask() + { + return [ + new TaskOptionPurgeXO(name: 'version', description :"Version"), + new TaskOptionPurgeXO(name : 'dateRelease', description: "Release date")] + } } diff --git a/plugins/nexus-coreui-plugin/src/main/java/org/sonatype/nexus/coreui/TaskOptionPurgeXO.groovy b/plugins/nexus-coreui-plugin/src/main/java/org/sonatype/nexus/coreui/TaskOptionPurgeXO.groovy new file mode 100644 index 0000000000..065adb74b9 --- /dev/null +++ b/plugins/nexus-coreui-plugin/src/main/java/org/sonatype/nexus/coreui/TaskOptionPurgeXO.groovy @@ -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 +} diff --git a/plugins/nexus-coreui-plugin/src/main/resources/static/rapture/NX/coreui/controller/Tasks.js b/plugins/nexus-coreui-plugin/src/main/resources/static/rapture/NX/coreui/controller/Tasks.js index 4146c890a1..844a2fad0b 100644 --- a/plugins/nexus-coreui-plugin/src/main/resources/static/rapture/NX/coreui/controller/Tasks.js +++ b/plugins/nexus-coreui-plugin/src/main/resources/static/rapture/NX/coreui/controller/Tasks.js @@ -33,7 +33,8 @@ Ext.define('NX.coreui.controller.Tasks', { ], stores: [ 'Task', - 'TaskType' + 'TaskType', + 'TaskOptionPurge' ], models: [ 'Task' diff --git a/plugins/nexus-coreui-plugin/src/main/resources/static/rapture/NX/coreui/model/TaskOptionPurge.js b/plugins/nexus-coreui-plugin/src/main/resources/static/rapture/NX/coreui/model/TaskOptionPurge.js new file mode 100644 index 0000000000..2bc19f51cd --- /dev/null +++ b/plugins/nexus-coreui-plugin/src/main/resources/static/rapture/NX/coreui/model/TaskOptionPurge.js @@ -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'} + ] +}); diff --git a/plugins/nexus-coreui-plugin/src/main/resources/static/rapture/NX/coreui/store/TaskOptionPurge.js b/plugins/nexus-coreui-plugin/src/main/resources/static/rapture/NX/coreui/store/TaskOptionPurge.js new file mode 100644 index 0000000000..75d033a197 --- /dev/null +++ b/plugins/nexus-coreui-plugin/src/main/resources/static/rapture/NX/coreui/store/TaskOptionPurge.js @@ -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' + } + } +}); diff --git a/plugins/nexus-repository-maven/src/main/java/org/sonatype/nexus/repository/maven/PurgeUnusedReleasesFacet.java b/plugins/nexus-repository-maven/src/main/java/org/sonatype/nexus/repository/maven/PurgeUnusedReleasesFacet.java new file mode 100644 index 0000000000..305a60c6d3 --- /dev/null +++ b/plugins/nexus-repository-maven/src/main/java/org/sonatype/nexus/repository/maven/PurgeUnusedReleasesFacet.java @@ -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); + +} diff --git a/plugins/nexus-repository-maven/src/main/java/org/sonatype/nexus/repository/maven/internal/MavenFacetUtils.java b/plugins/nexus-repository-maven/src/main/java/org/sonatype/nexus/repository/maven/internal/MavenFacetUtils.java index 5a9a12322b..cdd67fd989 100644 --- a/plugins/nexus-repository-maven/src/main/java/org/sonatype/nexus/repository/maven/internal/MavenFacetUtils.java +++ b/plugins/nexus-repository-maven/src/main/java/org/sonatype/nexus/repository/maven/internal/MavenFacetUtils.java @@ -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; + } } diff --git a/plugins/nexus-repository-maven/src/main/java/org/sonatype/nexus/repository/maven/internal/PurgeUnusedReleasesFacetImpl.java b/plugins/nexus-repository-maven/src/main/java/org/sonatype/nexus/repository/maven/internal/PurgeUnusedReleasesFacetImpl.java new file mode 100644 index 0000000000..576ca1adce --- /dev/null +++ b/plugins/nexus-repository-maven/src/main/java/org/sonatype/nexus/repository/maven/internal/PurgeUnusedReleasesFacetImpl.java @@ -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 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 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 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 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 components = tx.findComponents(queryPurgeReleasesBuilder.getWhereClause(), + queryPurgeReleasesBuilder.getQueryParams(), + singletonList(getRepository()), + queryPurgeReleasesBuilder.getQuerySuffix()); + return Lists.newArrayList(components); + + } + + public List 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 components) { + return components.get(components.size() - 1).version(); + } + + private Date getLastComponentReleaseDate(List 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 listNumberOfReleases(final StorageTx tx, + ORID bucketId) { + List 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 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; + + } +} diff --git a/plugins/nexus-repository-maven/src/main/java/org/sonatype/nexus/repository/maven/internal/PurgeUnusedSnapshotsFacetImpl.java b/plugins/nexus-repository-maven/src/main/java/org/sonatype/nexus/repository/maven/internal/PurgeUnusedSnapshotsFacetImpl.java index 5993e8a693..1e4cd7d9af 100644 --- a/plugins/nexus-repository-maven/src/main/java/org/sonatype/nexus/repository/maven/internal/PurgeUnusedSnapshotsFacetImpl.java +++ b/plugins/nexus-repository-maven/src/main/java/org/sonatype/nexus/repository/maven/internal/PurgeUnusedSnapshotsFacetImpl.java @@ -263,25 +263,8 @@ private List 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; } diff --git a/plugins/nexus-repository-maven/src/main/java/org/sonatype/nexus/repository/maven/internal/QueryPurgeReleasesBuilder.java b/plugins/nexus-repository-maven/src/main/java/org/sonatype/nexus/repository/maven/internal/QueryPurgeReleasesBuilder.java new file mode 100644 index 0000000000..6d8c7c0098 --- /dev/null +++ b/plugins/nexus-repository-maven/src/main/java/org/sonatype/nexus/repository/maven/internal/QueryPurgeReleasesBuilder.java @@ -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 queryParams; + private String whereClause; + private String querySuffix; + + + private QueryPurgeReleasesBuilder(Map 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 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 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 + '\'' + + '}'; + } +} diff --git a/plugins/nexus-repository-maven/src/main/java/org/sonatype/nexus/repository/maven/internal/QueryResultForNumberOfReleases.java b/plugins/nexus-repository-maven/src/main/java/org/sonatype/nexus/repository/maven/internal/QueryResultForNumberOfReleases.java new file mode 100644 index 0000000000..c9559699cf --- /dev/null +++ b/plugins/nexus-repository-maven/src/main/java/org/sonatype/nexus/repository/maven/internal/QueryResultForNumberOfReleases.java @@ -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 + + '}'; + } +} diff --git a/plugins/nexus-repository-maven/src/main/java/org/sonatype/nexus/repository/maven/internal/recipes/Maven2HostedRecipe.groovy b/plugins/nexus-repository-maven/src/main/java/org/sonatype/nexus/repository/maven/internal/recipes/Maven2HostedRecipe.groovy index c25f89917a..eb751ca0d3 100644 --- a/plugins/nexus-repository-maven/src/main/java/org/sonatype/nexus/repository/maven/internal/recipes/Maven2HostedRecipe.groovy +++ b/plugins/nexus-repository-maven/src/main/java/org/sonatype/nexus/repository/maven/internal/recipes/Maven2HostedRecipe.groovy @@ -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 + + @Inject + Provider 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) { diff --git a/plugins/nexus-repository-maven/src/main/java/org/sonatype/nexus/repository/maven/tasks/PurgeMavenUnusedReleasesTask.java b/plugins/nexus-repository-maven/src/main/java/org/sonatype/nexus/repository/maven/tasks/PurgeMavenUnusedReleasesTask.java new file mode 100644 index 0000000000..0753b0be9e --- /dev/null +++ b/plugins/nexus-repository-maven/src/main/java/org/sonatype/nexus/repository/maven/tasks/PurgeMavenUnusedReleasesTask.java @@ -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()) ; + } +} diff --git a/plugins/nexus-repository-maven/src/main/java/org/sonatype/nexus/repository/maven/tasks/PurgeMavenUnusedReleasesTaskDescriptor.java b/plugins/nexus-repository-maven/src/main/java/org/sonatype/nexus/repository/maven/tasks/PurgeMavenUnusedReleasesTaskDescriptor.java new file mode 100644 index 0000000000..b779e309c5 --- /dev/null +++ b/plugins/nexus-repository-maven/src/main/java/org/sonatype/nexus/repository/maven/tasks/PurgeMavenUnusedReleasesTaskDescriptor.java @@ -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) + ); + } +} diff --git a/plugins/nexus-repository-maven/src/test/java/org/sonatype/nexus/repository/maven/internal/PurgedUnusedReleasesFacetImplTest.java b/plugins/nexus-repository-maven/src/test/java/org/sonatype/nexus/repository/maven/internal/PurgedUnusedReleasesFacetImplTest.java new file mode 100644 index 0000000000..c726a2027f --- /dev/null +++ b/plugins/nexus-repository-maven/src/test/java/org/sonatype/nexus/repository/maven/internal/PurgedUnusedReleasesFacetImplTest.java @@ -0,0 +1,431 @@ +/* + * 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.base.Supplier; +import com.orientechnologies.orient.core.command.OCommandRequest; +import com.orientechnologies.orient.core.command.script.OCommandScript; +import com.orientechnologies.orient.core.db.document.ODatabaseDocumentTx; +import com.orientechnologies.orient.core.id.ORecordId; +import com.orientechnologies.orient.core.record.impl.ODocument; +import org.joda.time.DateTime; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Matchers; +import org.mockito.Mock; +import org.sonatype.goodies.testsupport.TestSupport; +import org.sonatype.nexus.common.collect.NestedAttributesMap; +import org.sonatype.nexus.common.entity.EntityMetadata; +import org.sonatype.nexus.orient.entity.AttachedEntityMetadata; +import org.sonatype.nexus.orient.entity.EntityAdapter; +import org.sonatype.nexus.repository.Repository; +import org.sonatype.nexus.repository.config.Configuration; +import org.sonatype.nexus.repository.maven.MavenFacet; +import org.sonatype.nexus.repository.storage.*; +import org.sonatype.nexus.transaction.UnitOfWork; + +import java.util.*; + +import static org.fest.assertions.api.Assertions.assertThat; +import static org.mockito.Mockito.*; +import static org.sonatype.nexus.repository.maven.internal.PurgeUnusedReleasesFacetImpl.PAGINATION_LIMIT; +import static org.sonatype.nexus.repository.maven.internal.QueryPurgeReleasesBuilder.DATE_RELEASE_OPTION; +import static org.sonatype.nexus.repository.maven.internal.QueryPurgeReleasesBuilder.VERSION_OPTION; + +public class PurgedUnusedReleasesFacetImplTest extends TestSupport{ + + private static final String GROUP_ID = "org.edf"; + + private static final String ARTIFACT_ID = "demoNexus"; + + @Mock + private StorageTx storageTx; + + @Mock + private StorageFacet storageFacet; + + @Mock + private MavenFacet mavenFacet; + + @Mock + private Repository repository; + + @Mock + private Maven2Format maven2Format; + + @Mock + private Configuration configuration; + + @Mock + private Bucket bucket; + + @Mock + private Component firstComponent; + @Mock + private Component secondComponent; + @Mock + private Component thirdComponent; + @Mock + private Component fourthComponent; + @Mock + private Supplier supplier; + @Mock + private ODatabaseDocumentTx oDatabaseDocumentTx; + @Mock + private OCommandRequest oCommandRequest; + + private ORecordId bucketId; + + private void initFirstRepository() throws Exception { + when(configuration.getRecipeName()).thenReturn("maven2-hosted"); + when(repository.getName()).thenReturn("my-first-repo"); + when(repository.getConfiguration()).thenReturn(configuration); + when(repository.getFormat()).thenReturn(maven2Format); + repository.attach(storageFacet); + when(repository.optionalFacet(StorageFacet.class)).thenReturn(Optional.of(storageFacet)); + when(repository.facet(StorageFacet.class)).thenReturn(storageFacet); + + when(storageFacet.txSupplier()).thenReturn(supplier); + when(storageFacet.txSupplier().get()).thenReturn(storageTx); + + //Bucket of the repository + when(storageTx.findBucket(repository)).thenReturn(bucket); + EntityAdapter owner = mock(EntityAdapter.class); + ODocument document = mock(ODocument.class); + bucketId = new ORecordId(21, 1); + when(document.getIdentity()).thenReturn(bucketId); + EntityMetadata entityMetadata = new AttachedEntityMetadata(owner, document); + when(bucket.getEntityMetadata()).thenReturn(entityMetadata); + + //Initialization of the components + when(firstComponent.group()).thenReturn(GROUP_ID); + when(firstComponent.version()).thenReturn("1.0-20171221.092123"); + when(firstComponent.format()).thenReturn("maven2"); + when(firstComponent.name()).thenReturn("ARTIFACT_ID"); + when(firstComponent.lastUpdated()).thenReturn(new DateTime(2017, 12, 10, 23, 0, 0)); + Map firstAttributesMap = new HashMap<>(); + firstAttributesMap.put("baseVersion", "1.0-SNAPSHOT"); + firstAttributesMap.put("groupId", GROUP_ID); + firstAttributesMap.put("artifactId", "ARTIFACT_ID"); + NestedAttributesMap firstAttributesComponent = new NestedAttributesMap("maven2", firstAttributesMap); + when(firstComponent.attributes()).thenReturn(firstAttributesComponent); + Asset firstassetOfFirstComponent = mock(Asset.class); + when(firstassetOfFirstComponent.name()).thenReturn("org/edf/demoNexus/1.0-SNAPSHOT/demoNexus-1.0-20171221.092011-1.jar"); + Asset sndassetOfFirstComponent = mock(Asset.class); + when(sndassetOfFirstComponent.name()).thenReturn("org/edf/demoNexus/1.0-SNAPSHOT/demoNexus-1.0-20171223.092011-1.jar"); + + when(storageTx.browseAssets(firstComponent)).thenReturn(Arrays.asList(firstassetOfFirstComponent, sndassetOfFirstComponent)); + + when(secondComponent.group()).thenReturn(GROUP_ID); + when(secondComponent.version()).thenReturn("1.0"); + when(secondComponent.format()).thenReturn("maven2"); + when(secondComponent.name()).thenReturn("ARTIFACT_ID"); + when(secondComponent.lastUpdated()).thenReturn(new DateTime(2017, 12, 10, 23, 10, 0)); + Map sndAttributesMap = new HashMap<>(); + sndAttributesMap.put("baseVersion", "1.0"); + sndAttributesMap.put("groupId", GROUP_ID); + sndAttributesMap.put("artifactId", "ARTIFACT_ID"); + NestedAttributesMap sndAttributesComponent = new NestedAttributesMap("maven2", sndAttributesMap); + when(secondComponent.attributes()).thenReturn(sndAttributesComponent); + + Asset firstassetOfSndComponent = mock(Asset.class); + when(firstassetOfSndComponent.name()).thenReturn("org/edf/demoNexus/1.0/demoNexus-1.1.jar"); + Asset sndassetOfSndComponent = mock(Asset.class); + when(sndassetOfSndComponent.name()).thenReturn("org/edf/demoNexus/1.0/demoNexus-1.1.jar.md5"); + Asset thirdassetOfSndComponent = mock(Asset.class); + when(thirdassetOfSndComponent.name()).thenReturn("org/edf/demoNexus/1.0/demoNexus-1.1.jar.sha1"); + + when(storageTx.browseAssets(secondComponent)).thenReturn(Arrays.asList(firstassetOfSndComponent, + sndassetOfSndComponent, thirdassetOfSndComponent)); + + + + when(thirdComponent.group()).thenReturn(GROUP_ID); + when(thirdComponent.version()).thenReturn("1.2"); + when(thirdComponent.format()).thenReturn("maven2"); + when(thirdComponent.name()).thenReturn("ARTIFACT_ID"); + when(thirdComponent.lastUpdated()).thenReturn(new DateTime(2017, 12, 11, 23, 10, 0)); + Map thirdAttributesMap = new HashMap<>(); + thirdAttributesMap.put("baseVersion", "1.2"); + thirdAttributesMap.put("groupId", GROUP_ID); + thirdAttributesMap.put("artifactId", "ARTIFACT_ID"); + NestedAttributesMap thirdAttributesComponent = new NestedAttributesMap("maven2", thirdAttributesMap); + when(thirdComponent.attributes()).thenReturn(thirdAttributesComponent); + + Asset firstassetOfThirdComponent = mock(Asset.class); + when(firstassetOfThirdComponent.name()).thenReturn("org/edf/demoNexus/1.0/demoNexus-1.2.jar"); + Asset sndassetOfThirdComponent = mock(Asset.class); + when(sndassetOfThirdComponent.name()).thenReturn("org/edf/demoNexus/1.0/demoNexus-1.2.pom"); + + when(storageTx.browseAssets(thirdComponent)).thenReturn(Arrays.asList(firstassetOfThirdComponent, + sndassetOfThirdComponent)); + + when(fourthComponent.group()).thenReturn(GROUP_ID); + when(fourthComponent.version()).thenReturn("1.3"); + when(fourthComponent.format()).thenReturn("maven2"); + when(fourthComponent.name()).thenReturn("ARTIFACT_ID"); + when(fourthComponent.lastUpdated()).thenReturn(new DateTime(2017, 12, 12, 23, 10, 0)); + Map fourthAttributesMap = new HashMap<>(); + fourthAttributesMap.put("baseVersion", "1.3"); + fourthAttributesMap.put("groupId", GROUP_ID); + fourthAttributesMap.put("artifactId", "ARTIFACT_ID"); + NestedAttributesMap fourthAttributesComponent = new NestedAttributesMap("maven2", fourthAttributesMap); + when(fourthComponent.formatAttributes()).thenReturn(fourthAttributesComponent); + + Asset firstassetOfFourthComponent = mock(Asset.class); + when(firstassetOfFourthComponent.name()).thenReturn("org/edf/demoNexus/1.0/demoNexus-1.3.jar"); + + when(storageTx.browseAssets(thirdComponent)).thenReturn(Collections.singletonList(firstassetOfFourthComponent)); + + + when(storageTx.browseComponents(bucket)).thenReturn(Arrays.asList(firstComponent, secondComponent, + thirdComponent, + fourthComponent)); + + + } + + + private Component mockComponent(String artifactId, String version, DateTime lastUpdatedDateTime) { + Component component = mock(Component.class); + when(component.group()).thenReturn(GROUP_ID); + when(component.version()).thenReturn(version); + when(component.format()).thenReturn("maven2"); + when(component.name()).thenReturn(artifactId); + when(component.lastUpdated()).thenReturn(lastUpdatedDateTime); + Map firstAttributesMap = new HashMap<>(); + firstAttributesMap.put("baseVersion", "1.0-SNAPSHOT"); + firstAttributesMap.put("groupId", GROUP_ID); + firstAttributesMap.put("artifactId", artifactId); + NestedAttributesMap firstAttributesComponent = new NestedAttributesMap("maven2", firstAttributesMap); + when(component.formatAttributes()).thenReturn(firstAttributesComponent); + when(component.formatAttributes()).thenReturn(firstAttributesComponent); + return component; + } + + private PurgeUnusedReleasesFacetImpl facet = new PurgeUnusedReleasesFacetImpl(); + + @Before + public void setUp() throws Exception { + facet.attach(repository); + + when(storageTx.getDb()).thenReturn(oDatabaseDocumentTx); + when(oDatabaseDocumentTx.command(Matchers.any(OCommandScript.class))).thenReturn(oCommandRequest); + + UnitOfWork.beginBatch(storageTx); + } + + @After + public void tearDown() { + UnitOfWork.end(); + } + + @Test + public void should_return_the_number_of_components_for_a_given_groupId_artifactId_in_a_specific_repository() throws Exception { + //Given + initFirstRepository(); + when(storageTx.countComponents(Matchers.any(String.class), + Matchers.any(Map.class), + Matchers.any(Iterable.class), + Matchers.anyString() + )).thenReturn(3L); + + //When + long nbReleases = facet.countTotalReleases(GROUP_ID, ARTIFACT_ID, bucketId); + + //Then + assertThat(nbReleases).isEqualTo(3); + } + + @Test + public void should_return_the_last_record_id_of_the_component_in_a_component_list() throws Exception { + String lastComponentVersion; + initFirstRepository(); + List components = Arrays.asList(secondComponent, thirdComponent, fourthComponent); + when(storageTx.findComponents(Matchers.any(String.class), + Matchers.any(Map.class), + Matchers.any(Iterable.class), + Matchers.anyString() + )).thenReturn(components); + when(fourthComponent.version()).thenReturn("2.4"); + + //When + lastComponentVersion = facet.getLastComponentVersion(components); + + //Then + assertThat(lastComponentVersion).isEqualTo("2.4"); + } + + + + @Test + public void should_return_the_releases_component_for_the_given_groupId_artifactId() throws Exception { + + //Given + initFirstRepository(); + when(storageTx.findComponents(Matchers.any(String.class), + Matchers.any(Map.class), + Matchers.any(Iterable.class), + Matchers.anyString() + )).thenReturn(Arrays.asList(secondComponent, thirdComponent, fourthComponent)); + + //When + + List components = facet.retrieveReleases(GROUP_ID, ARTIFACT_ID, VERSION_OPTION, 10, bucketId); + + //Then + assertThat(components.size()).isEqualTo(3); + assertThat(components).contains(secondComponent, thirdComponent, fourthComponent); + } + + @Test + public void test_the_purge_of_unused_releases_when_the_option_is_version() throws Exception { + //Given + initFirstRepository(); + when(repository.facet(MavenFacet.class)).thenReturn(mavenFacet); + + + Component fifthComponent = mockComponent("demoNexus", "1.0.5", new DateTime(2017, 12, 10, 23, 0, 0)); + + List components = Arrays.asList( + mockComponent("demoNexus", "1.0.1", new DateTime(2017, 12, 10, 23, 0, 0)), + mockComponent("demoNexus", "1.0.2", new DateTime(2017, 12, 10, 23, 0, 0)), + mockComponent("demoNexus", "1.0.3", new DateTime(2017, 12, 10, 23, 0, 0)), + mockComponent("demoNexus", "1.0.4", new DateTime(2017, 12, 10, 23, 0, 0)), + fifthComponent, + mockComponent("demoNexus", "1.0.6", new DateTime(2017, 12, 10, 23, 0, 0)), + mockComponent("demoNexus", "1.0.7", new DateTime(2017, 12, 10, 23, 0, 0)), + mockComponent("demoNexus", "1.0.8", new DateTime(2017, 12, 10, 23, 0, 0)), + mockComponent("demoNexus", "1.0.9", new DateTime(2017, 12, 10, 23, 0, 0)), + mockComponent("demoNexus", "1.1.0", new DateTime(2017, 12, 10, 23, 0, 0)), + mockComponent("demoNexus", "1.1.1", new DateTime(2017, 12, 10, 23, 0, 0)), + mockComponent("demoNexus", "1.1.2", new DateTime(2017, 12, 10, 23, 0, 0)), + mockComponent("demoNexus", "1.1.3", new DateTime(2017, 12, 10, 23, 0, 0)), + mockComponent("demoNexus", "1.1.4", new DateTime(2017, 12, 10, 23, 0, 0)), + mockComponent("demoNexus", "1.1.5", new DateTime(2017, 12, 10, 23, 0, 0)), + mockComponent("demoNexus", "1.2", new DateTime(2017, 12, 10, 23, 0, 0)), + fourthComponent); + when(storageTx.findComponents(Matchers.any(String.class), + Matchers.any(Map.class), + Matchers.any(Iterable.class), + Matchers.eq("order by attributes.maven2.baseVersion asc limit " + 11) + )).thenReturn(components); + when(storageTx.findComponents(Matchers.any(String.class), + Matchers.any(Map.class), + Matchers.any(Iterable.class), + Matchers.eq("order by attributes.maven2.baseVersion desc limit " + PAGINATION_LIMIT) + )).thenReturn(components.subList(0,PAGINATION_LIMIT)); + when(oCommandRequest.execute()).thenReturn( + Arrays.asList(new ODocument().field("groupId","org.edf").field("artifactId","demoNexus") + .field("count", 17L) + )); + + + //When + facet.purgeUnusedReleases(6, VERSION_OPTION); + + + //Then + verify(storageTx, times(3)).findComponents(Matchers.any(String.class), + Matchers.any(Map.class), + Matchers.any(Iterable.class), + Matchers.anyString()); + } + + @Test + public void test_the_purge_of_unused_releases_when_the_option_is_release_date() throws Exception { + //Given + initFirstRepository(); + when(repository.facet(MavenFacet.class)).thenReturn(mavenFacet); + + + Component fifthComponent = mockComponent("demoNexus", "1.2", new DateTime(2018, 1, 1, 23, 0, 0)); + + List components = Arrays.asList(mockComponent("demoNexus", "1.0", new DateTime(2017, 12, 10, 23, 0, 0)), + mockComponent("demoNexus", "3.0", new DateTime(2017, 12, 10, 23, 0, 0)), + mockComponent("demoNexus", "2.0", new DateTime(2017, 12, 10, 23, 0, 0)), + mockComponent("demoNexus", "1.1", new DateTime(2017, 12, 10, 23, 0, 0)), + fifthComponent); + when(storageTx.findComponents(Matchers.any(String.class), + Matchers.any(Map.class), + Matchers.any(Iterable.class), + Matchers.eq("order by last_updated asc limit " + 4) + )).thenReturn(components); + when(storageTx.findComponents(Matchers.any(String.class), + Matchers.any(Map.class), + Matchers.any(Iterable.class), + Matchers.eq("order by last_updated desc limit " + PAGINATION_LIMIT) + )).thenReturn(components.subList(0,4)); + + when(oCommandRequest.execute()).thenReturn( + Arrays.asList(new ODocument().field("groupId","org.edf").field("artifactId","demoNexus") + .field("count", 5L))); + + + //When + facet.purgeUnusedReleases(1, DATE_RELEASE_OPTION); + + + //Then + verify(storageTx, times(2)).findComponents(Matchers.any(String.class), + Matchers.any(Map.class), + Matchers.any(Iterable.class), + Matchers.anyString()); + } + + @Test + public void test_the_purge_of_unused_releases_when_the_number_of_releases_to_keep_is_higher_than_the_total_components() throws Exception { + //Given + initFirstRepository(); + + when(oCommandRequest.execute()).thenReturn( + Arrays.asList(new ODocument().field("groupId","org.edf").field("artifactId","demoNexus") + .field("count", 16L), + new ODocument().field("groupId","org.edf").field("artifactId","demoTest") + .field("count", 16L)) + ); + + //When + facet.purgeUnusedReleases(16, VERSION_OPTION); + + + //Then + verify(storageTx, times(0)).findComponents(Matchers.any(String.class), + Matchers.any(Map.class), + Matchers.any(Iterable.class), + Matchers.anyString()); + } + + @Test + public void list_the_number_of_releases_for_a_couple_groupId_artifactId_in_a_specific_bucket() throws Exception { + //Given + initFirstRepository(); + + when(oCommandRequest.execute()).thenReturn( + Arrays.asList(new ODocument().field("groupId","org.edf").field("artifactId","demoNexus") + .field("count", 5L), + new ODocument().field("groupId","org.edf").field("artifactId","demoTest") + .field("count", 2L)) + ); + //When + List queryResultForNumberOfReleasesList = facet.listNumberOfReleases(storageTx, bucketId); + + //Then + assertThat(queryResultForNumberOfReleasesList.size()).isEqualTo(2); + assertThat(queryResultForNumberOfReleasesList.get(0)).isEqualTo(new QueryResultForNumberOfReleases(GROUP_ID, "demoNexus", 5L)); + assertThat(queryResultForNumberOfReleasesList.get(1)).isEqualTo(new QueryResultForNumberOfReleases(GROUP_ID, "demoTest", 2L)); + + + } +} diff --git a/plugins/nexus-repository-maven/src/test/java/org/sonatype/nexus/repository/maven/tasks/PurgeMavenUnusedReleasesTaskTest.groovy b/plugins/nexus-repository-maven/src/test/java/org/sonatype/nexus/repository/maven/tasks/PurgeMavenUnusedReleasesTaskTest.groovy new file mode 100644 index 0000000000..2b198dc16a --- /dev/null +++ b/plugins/nexus-repository-maven/src/test/java/org/sonatype/nexus/repository/maven/tasks/PurgeMavenUnusedReleasesTaskTest.groovy @@ -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()) + + } + + +}