diff --git a/docs/COMPOSER_USER_DOCUMENTATION.md b/docs/COMPOSER_USER_DOCUMENTATION.md index 563dd3d3..7a107d44 100644 --- a/docs/COMPOSER_USER_DOCUMENTATION.md +++ b/docs/COMPOSER_USER_DOCUMENTATION.md @@ -41,6 +41,19 @@ in detail. Minimal configuration steps are: - Define URL for 'Remote storage' e.g. [https://packagist.org/](https://packagist.org/) - Select a 'Blob store' for 'Storage' +### Hosting Composer Repositories + +Creating a Composer hosted repository allows you to register packages in the repository manager. The hosted +repository acts as an authoritative location for these components. + +To add a hosted Composer repository, create a new repository with the recipe 'composer (hosted)' as +documented in [Repository Management](https://help.sonatype.com/display/NXRM3/Configuration#Configuration-RepositoryManagement). + +Minimal configuration steps are: + +- Define 'Name' - e.g. `composer-hosted` +- Select 'Blob store' for 'Storage' + ### Configuring Composer The least-invasive way of configuring the Composer client to communicate with Nexus is to update the `repositories` @@ -65,3 +78,25 @@ that all requests are directed to your Nexus repository manager. You can browse Composer repositories in the user interface inspecting the components and assets and their details, as described in [Browsing Repositories and Repository Groups](https://help.sonatype.com/display/NXRM3/Browsing+Repositories+and+Repository+Groups). + +### Publishing Composer Packages + +If you are authoring your own packages and want to distribute them to other users in your organization, you have +to upload them to a hosted repository on the repository manager. The consumers can then download it via the +repository. + +A Composer package should consist of a zipped archive of the sources containing a `composer.json` file. You should be +able to determine the vendor and project of the package from the `composer.json` file. The version can be determined +based on your own particular development process (for example, the version is not required in `composer.json` files and +may instead be something like a Git tag for a release depending on your local arrangements and preferences). + +With this information known, the package can be uploaded to your hosted Composer repository (replacing the credentials, +source filename, and vendor, project, and version path segments to match your particular distributable): + +`curl -v --user 'user:pass' --upload-file example.zip http://localhost:8081/repository/composer-hosted/packages/upload/vendor/project/version` + +*Note that the relevant information for vendor, project, and version is obtained from the URL you use to upload the zip, +not the composer.json file contained in the archive.* This flexible upload mechanism allows you to avoid changing the +contents of your `composer.json` in order to upload to Nexus. For example, you could write a script to check out new +tags from your Git repo, construct the appropriate upload URL, then push the tagged releases from your Git repo to your +Nexus hosted repository. diff --git a/src/main/java/org/sonatype/nexus/repository/composer/internal/ComposerContentFacet.java b/src/main/java/org/sonatype/nexus/repository/composer/internal/ComposerContentFacet.java index 69b75b39..0c8ad4f6 100644 --- a/src/main/java/org/sonatype/nexus/repository/composer/internal/ComposerContentFacet.java +++ b/src/main/java/org/sonatype/nexus/repository/composer/internal/ComposerContentFacet.java @@ -19,6 +19,7 @@ import org.sonatype.nexus.repository.Facet; import org.sonatype.nexus.repository.cache.CacheInfo; import org.sonatype.nexus.repository.view.Content; +import org.sonatype.nexus.repository.view.Payload; /** * Content facet used for getting assets from storage and putting assets into storage for a Composer-format repository. @@ -30,7 +31,7 @@ public interface ComposerContentFacet @Nullable Content get(String path) throws IOException; - Content put(String path, Content content, AssetKind assetKind) throws IOException; + Content put(String path, Payload payload, AssetKind assetKind) throws IOException; void setCacheInfo(String path, Content content, CacheInfo cacheInfo) throws IOException; } diff --git a/src/main/java/org/sonatype/nexus/repository/composer/internal/ComposerContentFacetImpl.java b/src/main/java/org/sonatype/nexus/repository/composer/internal/ComposerContentFacetImpl.java index d5ac239e..09fdcde9 100644 --- a/src/main/java/org/sonatype/nexus/repository/composer/internal/ComposerContentFacetImpl.java +++ b/src/main/java/org/sonatype/nexus/repository/composer/internal/ComposerContentFacetImpl.java @@ -38,6 +38,7 @@ import org.sonatype.nexus.repository.transaction.TransactionalTouchBlob; import org.sonatype.nexus.repository.transaction.TransactionalTouchMetadata; import org.sonatype.nexus.repository.view.Content; +import org.sonatype.nexus.repository.view.Payload; import org.sonatype.nexus.repository.view.payloads.BlobPayload; import org.sonatype.nexus.transaction.UnitOfWork; @@ -92,18 +93,18 @@ public Content get(final String path) throws IOException { } @Override - public Content put(final String path, final Content content, final AssetKind assetKind) throws IOException { + public Content put(final String path, final Payload payload, final AssetKind assetKind) throws IOException { StorageFacet storageFacet = facet(StorageFacet.class); - try (TempBlob tempBlob = storageFacet.createTempBlob(content, hashAlgorithms)) { + try (TempBlob tempBlob = storageFacet.createTempBlob(payload, hashAlgorithms)) { switch (assetKind) { case ZIPBALL: - return doPutContent(path, tempBlob, content, assetKind); + return doPutContent(path, tempBlob, payload, assetKind); case PACKAGES: - return doPutMetadata(path, tempBlob, content, assetKind); + return doPutMetadata(path, tempBlob, payload, assetKind); case LIST: - return doPutMetadata(path, tempBlob, content, assetKind); + return doPutMetadata(path, tempBlob, payload, assetKind); case PROVIDER: - return doPutMetadata(path, tempBlob, content, assetKind); + return doPutMetadata(path, tempBlob, payload, assetKind); default: throw new IllegalStateException("Unexpected asset kind: " + assetKind); } @@ -129,7 +130,7 @@ public void setCacheInfo(final String path, final Content content, final CacheIn @TransactionalStoreBlob protected Content doPutMetadata(final String path, final TempBlob tempBlob, - final Content content, + final Payload payload, final AssetKind assetKind) throws IOException { @@ -137,13 +138,16 @@ protected Content doPutMetadata(final String path, Asset asset = getOrCreateAsset(path, assetKind); - Content.applyToAsset(asset, Content.maintainLastModified(asset, content.getAttributes())); + if (payload instanceof Content) { + Content.applyToAsset(asset, Content.maintainLastModified(asset, ((Content) payload).getAttributes())); + } + AssetBlob assetBlob = tx.setBlob( asset, path, tempBlob, null, - content.getContentType(), + payload.getContentType(), false ); @@ -172,7 +176,7 @@ public Asset getOrCreateAsset(final String path, final AssetKind assetKind) { @TransactionalStoreBlob protected Content doPutContent(final String path, final TempBlob tempBlob, - final Content content, + final Payload payload, final AssetKind assetKind) throws IOException { @@ -185,13 +189,16 @@ protected Content doPutContent(final String path, Asset asset = getOrCreateAsset(path, assetKind, group, name, version); - Content.applyToAsset(asset, Content.maintainLastModified(asset, content.getAttributes())); + if (payload instanceof Content) { + Content.applyToAsset(asset, Content.maintainLastModified(asset, ((Content) payload).getAttributes())); + } + AssetBlob assetBlob = tx.setBlob( asset, path, tempBlob, null, - content.getContentType(), + payload.getContentType(), false ); diff --git a/src/main/java/org/sonatype/nexus/repository/composer/internal/ComposerHostedDownloadHandler.java b/src/main/java/org/sonatype/nexus/repository/composer/internal/ComposerHostedDownloadHandler.java new file mode 100644 index 00000000..56013e14 --- /dev/null +++ b/src/main/java/org/sonatype/nexus/repository/composer/internal/ComposerHostedDownloadHandler.java @@ -0,0 +1,65 @@ +/* + * Sonatype Nexus (TM) Open Source Version + * Copyright (c) 2018-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.composer.internal; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.inject.Named; +import javax.inject.Singleton; + +import org.sonatype.nexus.repository.Repository; +import org.sonatype.nexus.repository.http.HttpResponses; +import org.sonatype.nexus.repository.view.Content; +import org.sonatype.nexus.repository.view.Context; +import org.sonatype.nexus.repository.view.Handler; +import org.sonatype.nexus.repository.view.Response; + +import static org.sonatype.nexus.repository.composer.internal.ComposerPathUtils.buildZipballPath; +import static org.sonatype.nexus.repository.composer.internal.ComposerPathUtils.getProjectToken; +import static org.sonatype.nexus.repository.composer.internal.ComposerPathUtils.getVendorToken; + +/** + * Download handler for Composer hosted repositories. + */ +@Named +@Singleton +public class ComposerHostedDownloadHandler + implements Handler +{ + @Nonnull + @Override + public Response handle(@Nonnull final Context context) throws Exception { + Repository repository = context.getRepository(); + ComposerHostedFacet hostedFacet = repository.facet(ComposerHostedFacet.class); + AssetKind assetKind = context.getAttributes().require(AssetKind.class); + switch (assetKind) { + case PACKAGES: + return HttpResponses.ok(hostedFacet.getPackagesJson()); + case LIST: + throw new IllegalStateException("Unsupported assetKind: " + assetKind); + case PROVIDER: + return HttpResponses.ok(hostedFacet.getProviderJson(getVendorToken(context), getProjectToken(context))); + case ZIPBALL: + return responseFor(hostedFacet.getZipball(buildZipballPath(context))); + default: + throw new IllegalStateException("Unexpected assetKind: " + assetKind); + } + } + + private Response responseFor(@Nullable final Content content) { + if (content == null) { + return HttpResponses.notFound(); + } + return HttpResponses.ok(content); + } +} diff --git a/src/main/java/org/sonatype/nexus/repository/composer/internal/ComposerHostedFacet.java b/src/main/java/org/sonatype/nexus/repository/composer/internal/ComposerHostedFacet.java new file mode 100644 index 00000000..5b265b38 --- /dev/null +++ b/src/main/java/org/sonatype/nexus/repository/composer/internal/ComposerHostedFacet.java @@ -0,0 +1,38 @@ +/* + * Sonatype Nexus (TM) Open Source Version + * Copyright (c) 2018-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.composer.internal; + +import java.io.IOException; + +import javax.annotation.Nullable; + +import org.sonatype.nexus.repository.Facet; +import org.sonatype.nexus.repository.view.Content; +import org.sonatype.nexus.repository.view.Payload; + +/** + * Interface defining the features supported by Composer repository hosted facets. + */ +@Facet.Exposed +public interface ComposerHostedFacet + extends Facet +{ + void upload(String path, Payload payload) throws IOException; + + Content getPackagesJson() throws IOException; + + Content getProviderJson(String vendor, String project) throws IOException; + + @Nullable + Content getZipball(String path) throws IOException; +} diff --git a/src/main/java/org/sonatype/nexus/repository/composer/internal/ComposerHostedFacetImpl.java b/src/main/java/org/sonatype/nexus/repository/composer/internal/ComposerHostedFacetImpl.java new file mode 100644 index 00000000..c41634eb --- /dev/null +++ b/src/main/java/org/sonatype/nexus/repository/composer/internal/ComposerHostedFacetImpl.java @@ -0,0 +1,84 @@ +/* + * Sonatype Nexus (TM) Open Source Version + * Copyright (c) 2018-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.composer.internal; + +import java.io.IOException; + +import javax.inject.Inject; +import javax.inject.Named; + +import org.sonatype.nexus.repository.FacetSupport; +import org.sonatype.nexus.repository.storage.Query; +import org.sonatype.nexus.repository.storage.StorageTx; +import org.sonatype.nexus.repository.transaction.TransactionalTouchMetadata; +import org.sonatype.nexus.repository.view.Content; +import org.sonatype.nexus.repository.view.Payload; +import org.sonatype.nexus.transaction.UnitOfWork; + +import com.google.common.annotations.VisibleForTesting; + +import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.Collections.singletonList; +import static org.sonatype.nexus.repository.storage.ComponentEntityAdapter.P_GROUP; +import static org.sonatype.nexus.repository.storage.MetadataNodeEntityAdapter.P_NAME; + +/** + * Default implementation of a Composer hosted facet. + */ +@Named +public class ComposerHostedFacetImpl + extends FacetSupport + implements ComposerHostedFacet +{ + private final ComposerJsonProcessor composerJsonProcessor; + + @Inject + public ComposerHostedFacetImpl(final ComposerJsonProcessor composerJsonProcessor) { + this.composerJsonProcessor = checkNotNull(composerJsonProcessor); + } + + @Override + public void upload(final String path, final Payload payload) throws IOException { + content().put(path, payload, AssetKind.ZIPBALL); + } + + @Override + public Content getZipball(final String path) throws IOException { + return content().get(path); + } + + @Override + @TransactionalTouchMetadata + public Content getPackagesJson() throws IOException { + StorageTx tx = UnitOfWork.currentTx(); + return composerJsonProcessor + .generatePackagesFromComponents(getRepository(), tx.browseComponents(tx.findBucket(getRepository()))); + } + + @Override + @TransactionalTouchMetadata + public Content getProviderJson(final String vendor, final String project) throws IOException { + StorageTx tx = UnitOfWork.currentTx(); + return composerJsonProcessor.buildProviderJson(getRepository(), + tx.findComponents(buildQuery(vendor, project), singletonList(getRepository()))); + } + + @VisibleForTesting + protected Query buildQuery(final String vendor, final String project) { + return Query.builder().where(P_GROUP).eq(vendor).and(P_NAME).eq(project).build(); + } + + private ComposerContentFacet content() { + return getRepository().facet(ComposerContentFacet.class); + } +} diff --git a/src/main/java/org/sonatype/nexus/repository/composer/internal/ComposerHostedRecipe.groovy b/src/main/java/org/sonatype/nexus/repository/composer/internal/ComposerHostedRecipe.groovy new file mode 100644 index 00000000..7031f287 --- /dev/null +++ b/src/main/java/org/sonatype/nexus/repository/composer/internal/ComposerHostedRecipe.groovy @@ -0,0 +1,136 @@ +/* + * Sonatype Nexus (TM) Open Source Version + * Copyright (c) 2018-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.composer.internal + +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.http.HttpHandlers +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 static org.sonatype.nexus.repository.composer.internal.AssetKind.PACKAGES +import static org.sonatype.nexus.repository.composer.internal.AssetKind.PROVIDER +import static org.sonatype.nexus.repository.composer.internal.AssetKind.ZIPBALL + +/** + * Recipe for creating a Composer hosted repository. + */ +@Named(ComposerHostedRecipe.NAME) +@Singleton +class ComposerHostedRecipe + extends ComposerRecipeSupport +{ + public static final String NAME = 'composer-hosted' + + @Inject + Provider hostedFacet + + @Inject + ComposerHostedUploadHandler uploadHandler + + @Inject + ComposerHostedDownloadHandler downloadHandler + + @Inject + ComposerHostedRecipe(@Named(HostedType.NAME) final Type type, @Named(ComposerFormat.NAME) final Format format) { + super(type, format) + } + + @Override + void apply(@Nonnull final Repository repository) throws Exception { + repository.attach(contentFacet.get()) + repository.attach(securityFacet.get()) + repository.attach(configure(viewFacet.get())) + repository.attach(componentMaintenanceFacet.get()) + repository.attach(hostedFacet.get()) + repository.attach(storageFacet.get()) + repository.attach(searchFacet.get()) + repository.attach(attributesFacet.get()) + } + + /** + * Configure {@link ViewFacet}. + */ + private ViewFacet configure(final ConfigurableViewFacet facet) { + Router.Builder builder = new Router.Builder() + + builder.route(packagesMatcher() + .handler(timingHandler) + .handler(assetKindHandler.rcurry(PACKAGES)) + .handler(securityHandler) + .handler(exceptionHandler) + .handler(handlerContributor) + .handler(conditionalRequestHandler) + .handler(partialFetchHandler) + .handler(contentHeadersHandler) + .handler(unitOfWorkHandler) + .handler(downloadHandler) + .create()) + + builder.route(providerMatcher() + .handler(timingHandler) + .handler(assetKindHandler.rcurry(PROVIDER)) + .handler(securityHandler) + .handler(exceptionHandler) + .handler(handlerContributor) + .handler(conditionalRequestHandler) + .handler(partialFetchHandler) + .handler(contentHeadersHandler) + .handler(unitOfWorkHandler) + .handler(downloadHandler) + .create()) + + builder.route(zipballMatcher() + .handler(timingHandler) + .handler(assetKindHandler.rcurry(ZIPBALL)) + .handler(securityHandler) + .handler(exceptionHandler) + .handler(handlerContributor) + .handler(conditionalRequestHandler) + .handler(partialFetchHandler) + .handler(contentHeadersHandler) + .handler(unitOfWorkHandler) + .handler(downloadHandler) + .create()) + + builder.route(uploadMatcher() + .handler(timingHandler) + .handler(assetKindHandler.rcurry(ZIPBALL)) + .handler(securityHandler) + .handler(exceptionHandler) + .handler(handlerContributor) + .handler(conditionalRequestHandler) + .handler(partialFetchHandler) + .handler(contentHeadersHandler) + .handler(unitOfWorkHandler) + .handler(uploadHandler) + .create()) + + addBrowseUnsupportedRoute(builder) + + builder.defaultHandlers(HttpHandlers.notFound()) + + facet.configure(builder.create()) + + return facet + } +} diff --git a/src/main/java/org/sonatype/nexus/repository/composer/internal/ComposerHostedUploadHandler.java b/src/main/java/org/sonatype/nexus/repository/composer/internal/ComposerHostedUploadHandler.java new file mode 100644 index 00000000..f26690ff --- /dev/null +++ b/src/main/java/org/sonatype/nexus/repository/composer/internal/ComposerHostedUploadHandler.java @@ -0,0 +1,50 @@ +/* + * Sonatype Nexus (TM) Open Source Version + * Copyright (c) 2018-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.composer.internal; + +import javax.annotation.Nonnull; +import javax.inject.Named; +import javax.inject.Singleton; + +import org.sonatype.nexus.repository.Repository; +import org.sonatype.nexus.repository.http.HttpResponses; +import org.sonatype.nexus.repository.view.Context; +import org.sonatype.nexus.repository.view.Handler; +import org.sonatype.nexus.repository.view.Payload; +import org.sonatype.nexus.repository.view.Request; +import org.sonatype.nexus.repository.view.Response; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Upload handler for Composer hosted repositories. + */ +@Named +@Singleton +public class ComposerHostedUploadHandler + implements Handler +{ + @Nonnull + @Override + public Response handle(@Nonnull final Context context) throws Exception { + String path = ComposerPathUtils.buildZipballPath(context); + Request request = checkNotNull(context.getRequest()); + Payload payload = checkNotNull(request.getPayload()); + + Repository repository = context.getRepository(); + ComposerHostedFacet hostedFacet = repository.facet(ComposerHostedFacet.class); + + hostedFacet.upload(path, payload); + return HttpResponses.ok(); + } +} diff --git a/src/main/java/org/sonatype/nexus/repository/composer/internal/ComposerJsonProcessor.java b/src/main/java/org/sonatype/nexus/repository/composer/internal/ComposerJsonProcessor.java index 2dd02859..e3bfa6a2 100644 --- a/src/main/java/org/sonatype/nexus/repository/composer/internal/ComposerJsonProcessor.java +++ b/src/main/java/org/sonatype/nexus/repository/composer/internal/ComposerJsonProcessor.java @@ -14,16 +14,19 @@ import java.io.IOException; import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; import java.util.stream.Collectors; +import java.util.stream.StreamSupport; import javax.inject.Named; import javax.inject.Singleton; import org.sonatype.nexus.repository.Repository; +import org.sonatype.nexus.repository.storage.Component; import org.sonatype.nexus.repository.view.Content; import org.sonatype.nexus.repository.view.ContentTypes; import org.sonatype.nexus.repository.view.Payload; @@ -31,8 +34,13 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.hash.Hashing; +import org.joda.time.DateTimeZone; +import org.joda.time.format.DateTimeFormat; +import org.joda.time.format.DateTimeFormatter; import static com.google.common.base.Preconditions.checkState; +import static org.sonatype.nexus.repository.composer.internal.ComposerPathUtils.buildZipballPath; /** * Class encapsulating JSON processing for Composer-format repositories, including operations for parsing JSON indexes @@ -44,6 +52,8 @@ public class ComposerJsonProcessor { private static final String REWRITE_URL = "%s/%s/%s/%s-%s.zip"; + private static final String PACKAGE_JSON_PATH = "/p/%package%.json"; + private static final String VENDOR_AND_PROJECT = "%s/%s"; private static final String DIST_KEY = "dist"; @@ -64,25 +74,51 @@ public class ComposerJsonProcessor private static final String URL_KEY = "url"; + private static final String NAME_KEY = "name"; + + private static final String VERSION_KEY = "version"; + + private static final String TIME_KEY = "time"; + + private static final String UID_KEY = "uid"; + private static final String ZIP_TYPE = "zip"; private static final ObjectMapper mapper = new ObjectMapper(); + private static final DateTimeFormatter timeFormatter = DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ssZZ"); + /** * Generates a packages.json file (inclusive of all projects) based on the list.json provided as a payload. Expected * usage is to "go remote" on the current repository to fetch a list.json copy, then pass it to this method to build * the packages.json for the client to use. */ - public Content generatePackagesJson(final Repository repository, final Payload payload) throws IOException { + public Content generatePackagesFromList(final Repository repository, final Payload payload) throws IOException { // TODO: Parse using JSON tokens rather than loading all this into memory, it "should" work but I'd be careful. Map listJson = parseJson(payload); - Collection packageNames = (Collection) listJson.get(PACKAGE_NAMES_KEY); - Map> packages = packageNames.stream() - .collect(Collectors.toMap((each) -> each, (each) -> Collections.singletonMap(SHA256_KEY, null))); + return buildPackagesJson(repository, (Collection) listJson.get(PACKAGE_NAMES_KEY)); + } + /** + * Generates a packages.json file (inclusive of all projects) based on the components provided. Expected usage is + * for a hosted repository to be queried for its components, which are then provided to this method to build the + * packages.json for the client to use. + */ + public Content generatePackagesFromComponents(final Repository repository, final Iterable components) + throws IOException + { + return buildPackagesJson(repository, StreamSupport.stream(components.spliterator(), false) + .map(component -> component.group() + "/" + component.name()).collect(Collectors.toList())); + } + + /** + * Builds a packages.json file as a {@code Content} instance containing the actual JSON for the given providers. + */ + private Content buildPackagesJson(final Repository repository, final Collection names) throws IOException { Map packagesJson = new LinkedHashMap<>(); - packagesJson.put(PROVIDERS_URL_KEY, repository.getUrl() + "/p/%package%.json"); - packagesJson.put(PROVIDERS_KEY, packages); + packagesJson.put(PROVIDERS_URL_KEY, repository.getUrl() + PACKAGE_JSON_PATH); + packagesJson.put(PROVIDERS_KEY, names.stream() + .collect(Collectors.toMap((each) -> each, (each) -> Collections.singletonMap(SHA256_KEY, null)))); return new Content(new StringPayload(mapper.writeValueAsString(packagesJson), ContentTypes.APPLICATION_JSON)); } @@ -115,6 +151,50 @@ public Payload rewriteProviderJson(final Repository repository, final Payload pa return new StringPayload(mapper.writeValueAsString(json), payload.getContentType()); } + /** + * Builds a provider JSON file for a list of components. This minimal subset will contain the packages entries with + * the name, version, and dist information for each component. A timestamp derived from the component's last updated + * field and a uid derived from the component group/name/version and last updated time is also included in the JSON. + */ + public Content buildProviderJson(final Repository repository, final Iterable components) throws IOException { + Map> packages = new LinkedHashMap<>(); + for (Component component : components) { + String vendor = component.group(); + String project = component.name(); + String version = component.version(); + + String name = vendor + "/" + project; + String time = component.requireLastUpdated().withZone(DateTimeZone.UTC).toString(timeFormatter); + + Map dist = new LinkedHashMap<>(); + dist.put(URL_KEY, repository.getUrl() + "/" + buildZipballPath(vendor, project, version)); + dist.put(TYPE_KEY, ZIP_TYPE); + + Map pkg = new LinkedHashMap<>(); + pkg.put(NAME_KEY, name); + pkg.put(VERSION_KEY, version); + pkg.put(DIST_KEY, dist); + pkg.put(TIME_KEY, time); + pkg.put(UID_KEY, Hashing.md5().newHasher() + .putString(vendor, StandardCharsets.UTF_8) + .putString(project, StandardCharsets.UTF_8) + .putString(version, StandardCharsets.UTF_8) + .putString(time, StandardCharsets.UTF_8) + .hash() + .asInt()); + + if (!packages.containsKey(name)) { + packages.put(name, new LinkedHashMap<>()); + } + + Map packagesForName = packages.get(name); + packagesForName.put(version, pkg); + } + + return new Content(new StringPayload(mapper.writeValueAsString(Collections.singletonMap(PACKAGES_KEY, packages)), + ContentTypes.APPLICATION_JSON)); + } + /** * Obtains the dist URL for a particular vendor/project and version within a provider JSON payload. */ diff --git a/src/main/java/org/sonatype/nexus/repository/composer/internal/ComposerPathUtils.java b/src/main/java/org/sonatype/nexus/repository/composer/internal/ComposerPathUtils.java new file mode 100644 index 00000000..5eb16b3d --- /dev/null +++ b/src/main/java/org/sonatype/nexus/repository/composer/internal/ComposerPathUtils.java @@ -0,0 +1,110 @@ +/* + * Sonatype Nexus (TM) Open Source Version + * Copyright (c) 2018-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.composer.internal; + +import java.util.Map; + +import org.sonatype.nexus.repository.view.Context; +import org.sonatype.nexus.repository.view.matchers.token.TokenMatcher; + +import org.eclipse.sisu.Nullable; + +import static com.google.common.base.Preconditions.checkNotNull; +import static org.sonatype.nexus.repository.composer.internal.ComposerRecipeSupport.NAME_TOKEN; +import static org.sonatype.nexus.repository.composer.internal.ComposerRecipeSupport.PROJECT_TOKEN; +import static org.sonatype.nexus.repository.composer.internal.ComposerRecipeSupport.VENDOR_TOKEN; +import static org.sonatype.nexus.repository.composer.internal.ComposerRecipeSupport.VERSION_TOKEN; + +/** + * Utility class containing methods for working with Composer routes and paths. + */ +public final class ComposerPathUtils +{ + private static final String ZIPBALL_PATH = "%s/%s/%s/%s.zip"; + + private static final String PROVIDER_JSON_PATH = "p/%s/%s.json"; + + private static final String NAME_PATTERN = "%s-%s-%s"; + + /** + * Returns the vendor token from a path in a context. The vendor token must be present or the operation will fail. + */ + public static String getVendorToken(final Context context) { + TokenMatcher.State state = context.getAttributes().require(TokenMatcher.State.class); + return checkNotNull(state.getTokens().get(VENDOR_TOKEN)); + } + + /** + * Returns the project token from a path in a context. The project token must be present or the operation will fail. + */ + public static String getProjectToken(final Context context) { + TokenMatcher.State state = context.getAttributes().require(TokenMatcher.State.class); + return checkNotNull(state.getTokens().get(PROJECT_TOKEN)); + } + + /** + * Builds the path to a zipball based on the path contained in a particular context. For download routes the full + * path including the name token will be present and will be constructed accordingly. For upload routes the full + * path will not be known because the filename will not be present, so the name portion will be constructed from + * the vendor, project, and version information contained in the other path segments. + */ + public static String buildZipballPath(final Context context) { + TokenMatcher.State state = context.getAttributes().require(TokenMatcher.State.class); + Map tokens = state.getTokens(); + return buildZipballPath( + tokens.get(VENDOR_TOKEN), + tokens.get(PROJECT_TOKEN), + tokens.get(VERSION_TOKEN), + tokens.get(NAME_TOKEN)); + } + + /** + * Builds the zipball path based on the provided vendor, project, and version. The filename will be constructed based + * on the values of those parameters. + */ + public static String buildZipballPath(final String vendor, final String project, final String version) { + return buildZipballPath(vendor, project, version, null); + } + + private static String buildZipballPath(final String vendor, + final String project, + final String version, + @Nullable final String name) + { + return String.format(ZIPBALL_PATH, vendor, project, version, + name == null ? String.format(NAME_PATTERN, vendor, project, version) : name); + } + + /** + * Builds the path to a provider json file based on the path contained in a particular context. The vendor token and + * the project token must be present in the context in order to successfully generate the path. + */ + public static String buildProviderPath(final Context context) { + TokenMatcher.State state = context.getAttributes().require(TokenMatcher.State.class); + Map tokens = state.getTokens(); + return buildProviderPath(tokens.get(VENDOR_TOKEN), tokens.get(PROJECT_TOKEN)); + } + + /** + * Builds the path to a provider json file based on the specified vendor and project tokens. + */ + public static String buildProviderPath(final String vendor, final String project) { + checkNotNull(vendor); + checkNotNull(project); + return String.format(PROVIDER_JSON_PATH, vendor, project); + } + + private ComposerPathUtils() { + // empty + } +} diff --git a/src/main/java/org/sonatype/nexus/repository/composer/internal/ComposerProxyFacetImpl.java b/src/main/java/org/sonatype/nexus/repository/composer/internal/ComposerProxyFacetImpl.java index c82e5ca2..8f5085fa 100644 --- a/src/main/java/org/sonatype/nexus/repository/composer/internal/ComposerProxyFacetImpl.java +++ b/src/main/java/org/sonatype/nexus/repository/composer/internal/ComposerProxyFacetImpl.java @@ -36,6 +36,11 @@ import com.google.common.base.Throwables; import static com.google.common.base.Preconditions.checkNotNull; +import static org.sonatype.nexus.repository.composer.internal.ComposerPathUtils.buildProviderPath; +import static org.sonatype.nexus.repository.composer.internal.ComposerPathUtils.buildZipballPath; +import static org.sonatype.nexus.repository.composer.internal.ComposerRecipeSupport.PROJECT_TOKEN; +import static org.sonatype.nexus.repository.composer.internal.ComposerRecipeSupport.VENDOR_TOKEN; +import static org.sonatype.nexus.repository.composer.internal.ComposerRecipeSupport.VERSION_TOKEN; import static org.sonatype.nexus.repository.http.HttpMethods.GET; /** @@ -49,18 +54,6 @@ public class ComposerProxyFacetImpl private static final String LIST_JSON = "packages/list.json"; - private static final String PROVIDER_JSON = "p/%s/%s.json"; - - private static final String ZIPBALL = "%s/%s/%s/%s.zip"; - - private static final String VENDOR_TOKEN = "vendor"; - - private static final String PROJECT_TOKEN = "project"; - - private static final String VERSION_TOKEN = "version"; - - private static final String NAME_TOKEN = "name"; - private final ComposerJsonProcessor composerJsonProcessor; @Inject @@ -84,9 +77,9 @@ protected Content getCachedContent(final Context context) throws IOException { case LIST: return content().get(LIST_JSON); case PROVIDER: - return content().get(providerPath(context)); + return content().get(buildProviderPath(context)); case ZIPBALL: - return content().get(zipballPath(context)); + return content().get(buildZipballPath(context)); default: throw new IllegalStateException(); } @@ -101,9 +94,9 @@ protected Content store(final Context context, final Content content) throws IOE case LIST: return content().put(LIST_JSON, content, assetKind); case PROVIDER: - return content().put(providerPath(context), content, assetKind); + return content().put(buildProviderPath(context), content, assetKind); case ZIPBALL: - return content().put(zipballPath(context), content, assetKind); + return content().put(buildZipballPath(context), content, assetKind); default: throw new IllegalStateException(); } @@ -122,10 +115,10 @@ protected void indicateVerified(final Context context, final Content content, fi content().setCacheInfo(LIST_JSON, content, cacheInfo); break; case PROVIDER: - content().setCacheInfo(providerPath(context), content, cacheInfo); + content().setCacheInfo(buildProviderPath(context), content, cacheInfo); break; case ZIPBALL: - content().setCacheInfo(zipballPath(context), content, cacheInfo); + content().setCacheInfo(buildZipballPath(context), content, cacheInfo); break; default: throw new IllegalStateException(); @@ -156,7 +149,7 @@ private Content generatePackagesJson(final Context context) throws IOException { Request request = new Request.Builder().action(GET).path("/" + LIST_JSON).build(); Response response = getRepository().facet(ViewFacet.class).dispatch(request, context); Payload payload = checkNotNull(response.getPayload()); - return composerJsonProcessor.generatePackagesJson(getRepository(), payload); + return composerJsonProcessor.generatePackagesFromList(getRepository(), payload); } catch (IOException e) { throw new UncheckedIOException(e); @@ -175,7 +168,7 @@ private String getZipballUrl(final Context context) { String project = tokens.get(PROJECT_TOKEN); String version = tokens.get(VERSION_TOKEN); - Request request = new Request.Builder().action(GET).path("/" + String.format(PROVIDER_JSON, vendor, project)) + Request request = new Request.Builder().action(GET).path("/" + buildProviderPath(vendor, project)) .attribute(ComposerProviderHandler.DO_NOT_REWRITE, "true").build(); Response response = getRepository().facet(ViewFacet.class).dispatch(request, context); Payload payload = checkNotNull(response.getPayload()); @@ -190,22 +183,6 @@ private String getZipballUrl(final Context context) { } } - private String providerPath(final Context context) { - TokenMatcher.State state = context.getAttributes().require(TokenMatcher.State.class); - Map tokens = state.getTokens(); - return String.format(PROVIDER_JSON, tokens.get(VENDOR_TOKEN), tokens.get(PROJECT_TOKEN)); - } - - private String zipballPath(final Context context) { - TokenMatcher.State state = context.getAttributes().require(TokenMatcher.State.class); - Map tokens = state.getTokens(); - return String.format(ZIPBALL, - tokens.get(VENDOR_TOKEN), - tokens.get(PROJECT_TOKEN), - tokens.get(VERSION_TOKEN), - tokens.get(NAME_TOKEN)); - } - private ComposerContentFacet content() { return getRepository().facet(ComposerContentFacet.class); } diff --git a/src/main/java/org/sonatype/nexus/repository/composer/internal/ComposerProxyRecipe.groovy b/src/main/java/org/sonatype/nexus/repository/composer/internal/ComposerProxyRecipe.groovy index 7416fbb7..4d2725fc 100644 --- a/src/main/java/org/sonatype/nexus/repository/composer/internal/ComposerProxyRecipe.groovy +++ b/src/main/java/org/sonatype/nexus/repository/composer/internal/ComposerProxyRecipe.groovy @@ -27,7 +27,6 @@ import org.sonatype.nexus.repository.http.HttpHandlers import org.sonatype.nexus.repository.httpclient.HttpClientFacet import org.sonatype.nexus.repository.proxy.ProxyHandler import org.sonatype.nexus.repository.purge.PurgeUnusedFacet -import org.sonatype.nexus.repository.storage.SingleAssetComponentMaintenance import org.sonatype.nexus.repository.types.ProxyType import org.sonatype.nexus.repository.view.ConfigurableViewFacet import org.sonatype.nexus.repository.view.Router @@ -46,15 +45,9 @@ class ComposerProxyRecipe @Inject Provider proxyFacet - @Inject - Provider httpClientFacet - @Inject Provider negativeCacheFacet - @Inject - Provider componentMaintenanceFacet - @Inject Provider purgeUnusedFacet @@ -64,6 +57,9 @@ class ComposerProxyRecipe @Inject ProxyHandler proxyHandler + @Inject + Provider httpClientFacet + @Inject ComposerProviderHandler composerProviderHandler @@ -94,6 +90,7 @@ class ComposerProxyRecipe .handler(timingHandler) .handler(assetKindHandler.rcurry(AssetKind.PACKAGES)) .handler(securityHandler) + .handler(exceptionHandler) .handler(handlerContributor) .handler(negativeCacheHandler) .handler(conditionalRequestHandler) @@ -107,6 +104,7 @@ class ComposerProxyRecipe .handler(timingHandler) .handler(assetKindHandler.rcurry(AssetKind.LIST)) .handler(securityHandler) + .handler(exceptionHandler) .handler(handlerContributor) .handler(negativeCacheHandler) .handler(conditionalRequestHandler) @@ -120,6 +118,7 @@ class ComposerProxyRecipe .handler(timingHandler) .handler(assetKindHandler.rcurry(AssetKind.PROVIDER)) .handler(securityHandler) + .handler(exceptionHandler) .handler(handlerContributor) .handler(negativeCacheHandler) .handler(conditionalRequestHandler) @@ -134,6 +133,7 @@ class ComposerProxyRecipe .handler(timingHandler) .handler(assetKindHandler.rcurry(AssetKind.ZIPBALL)) .handler(securityHandler) + .handler(exceptionHandler) .handler(handlerContributor) .handler(negativeCacheHandler) .handler(conditionalRequestHandler) diff --git a/src/main/java/org/sonatype/nexus/repository/composer/internal/ComposerRecipeSupport.groovy b/src/main/java/org/sonatype/nexus/repository/composer/internal/ComposerRecipeSupport.groovy index f1243ad8..6b591133 100644 --- a/src/main/java/org/sonatype/nexus/repository/composer/internal/ComposerRecipeSupport.groovy +++ b/src/main/java/org/sonatype/nexus/repository/composer/internal/ComposerRecipeSupport.groovy @@ -22,6 +22,7 @@ import org.sonatype.nexus.repository.attributes.AttributesFacet import org.sonatype.nexus.repository.http.PartialFetchHandler import org.sonatype.nexus.repository.search.SearchFacet import org.sonatype.nexus.repository.security.SecurityHandler +import org.sonatype.nexus.repository.storage.SingleAssetComponentMaintenance import org.sonatype.nexus.repository.storage.StorageFacet import org.sonatype.nexus.repository.storage.UnitOfWorkHandler import org.sonatype.nexus.repository.view.ConfigurableViewFacet @@ -29,6 +30,7 @@ import org.sonatype.nexus.repository.view.Context import org.sonatype.nexus.repository.view.Route.Builder import org.sonatype.nexus.repository.view.handlers.ConditionalRequestHandler import org.sonatype.nexus.repository.view.handlers.ContentHeadersHandler +import org.sonatype.nexus.repository.view.handlers.ExceptionHandler import org.sonatype.nexus.repository.view.handlers.HandlerContributor import org.sonatype.nexus.repository.view.handlers.TimingHandler import org.sonatype.nexus.repository.view.matchers.ActionMatcher @@ -38,6 +40,7 @@ import org.sonatype.nexus.repository.view.matchers.token.TokenMatcher import static org.sonatype.nexus.repository.http.HttpMethods.GET import static org.sonatype.nexus.repository.http.HttpMethods.HEAD +import static org.sonatype.nexus.repository.http.HttpMethods.PUT /** * Abstract superclass containing methods and constants common to most Composer repository recipes. @@ -45,6 +48,14 @@ import static org.sonatype.nexus.repository.http.HttpMethods.HEAD abstract class ComposerRecipeSupport extends RecipeSupport { + public static final String VENDOR_TOKEN = 'vendor' + + public static final String PROJECT_TOKEN = 'project' + + public static final String VERSION_TOKEN = 'version' + + public static final String NAME_TOKEN = 'name' + @Inject Provider contentFacet @@ -63,6 +74,12 @@ abstract class ComposerRecipeSupport @Inject Provider attributesFacet + @Inject + Provider componentMaintenanceFacet + + @Inject + ExceptionHandler exceptionHandler + @Inject TimingHandler timingHandler @@ -124,4 +141,12 @@ abstract class ComposerRecipeSupport new TokenMatcher('/{vendor:.+}/{project:.+}/{version:.+}/{name:.+}.zip') )) } + + static Builder uploadMatcher() { + new Builder().matcher( + LogicMatchers.and( + new ActionMatcher(PUT), + new TokenMatcher('/packages/upload/{vendor:.+}/{project:.+}/{version:.+}') + )) + } } diff --git a/src/main/resources/static/rapture/NX/composer/view/repository/recipe/ComposerHosted.js b/src/main/resources/static/rapture/NX/composer/view/repository/recipe/ComposerHosted.js new file mode 100644 index 00000000..f20681ce --- /dev/null +++ b/src/main/resources/static/rapture/NX/composer/view/repository/recipe/ComposerHosted.js @@ -0,0 +1,39 @@ +/* + * Sonatype Nexus (TM) Open Source Version + * Copyright (c) 2018-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*/ + +/** + * Configuration settings in the UI for a Composer hosted recipe. + */ +Ext.define('NX.composer.view.repository.recipe.ComposerHosted', { + extend: 'NX.coreui.view.repository.RepositorySettingsForm', + alias: 'widget.nx-coreui-repository-composer-hosted', + requires: [ + 'NX.coreui.view.repository.facet.StorageFacet', + 'NX.coreui.view.repository.facet.StorageFacetHosted' + ], + + /** + * @override + */ + initComponent: function () { + var me = this; + + me.items = [ + {xtype: 'nx-coreui-repository-storage-facet'}, + {xtype: 'nx-coreui-repository-storage-hosted-facet', writePolicy: 'ALLOW'} + ]; + + me.callParent(); + } +}); diff --git a/src/test/java/org/sonatype/nexus/repository/composer/internal/ComposerHostedDownloadHandlerTest.java b/src/test/java/org/sonatype/nexus/repository/composer/internal/ComposerHostedDownloadHandlerTest.java new file mode 100644 index 00000000..c968ca8b --- /dev/null +++ b/src/test/java/org/sonatype/nexus/repository/composer/internal/ComposerHostedDownloadHandlerTest.java @@ -0,0 +1,149 @@ +/* + * Sonatype Nexus (TM) Open Source Version + * Copyright (c) 2018-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.composer.internal; + +import java.util.Map; + +import org.sonatype.goodies.testsupport.TestSupport; +import org.sonatype.nexus.common.collect.AttributesMap; +import org.sonatype.nexus.repository.Repository; +import org.sonatype.nexus.repository.view.Content; +import org.sonatype.nexus.repository.view.Context; +import org.sonatype.nexus.repository.view.Payload; +import org.sonatype.nexus.repository.view.Response; +import org.sonatype.nexus.repository.view.matchers.token.TokenMatcher; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.mockito.Mock; + +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.when; +import static org.sonatype.nexus.repository.composer.internal.AssetKind.LIST; +import static org.sonatype.nexus.repository.composer.internal.AssetKind.PACKAGES; +import static org.sonatype.nexus.repository.composer.internal.AssetKind.PROVIDER; +import static org.sonatype.nexus.repository.composer.internal.AssetKind.ZIPBALL; +import static org.sonatype.nexus.repository.composer.internal.ComposerRecipeSupport.NAME_TOKEN; +import static org.sonatype.nexus.repository.composer.internal.ComposerRecipeSupport.PROJECT_TOKEN; +import static org.sonatype.nexus.repository.composer.internal.ComposerRecipeSupport.VENDOR_TOKEN; +import static org.sonatype.nexus.repository.composer.internal.ComposerRecipeSupport.VERSION_TOKEN; + +public class ComposerHostedDownloadHandlerTest + extends TestSupport +{ + private static final String VENDOR = "testvendor"; + + private static final String PROJECT = "testproject"; + + private static final String VERSION = "testversion"; + + private static final String NAME = "testvendor-testproject-testversion"; + + private static final String ZIPBALL_PATH = "testvendor/testproject/testversion/testvendor-testproject-testversion.zip"; + + @Rule + public ExpectedException exception = ExpectedException.none(); + + @Mock + private Map tokens; + + @Mock + private TokenMatcher.State state; + + @Mock + private ComposerHostedFacet composerHostedFacet; + + @Mock + private Repository repository; + + @Mock + private Context context; + + @Mock + private Content content; + + @Mock + private Payload payload; + + @Mock + private AttributesMap attributes; + + private ComposerHostedDownloadHandler underTest = new ComposerHostedDownloadHandler(); + + @Before + public void setUp() { + when(repository.facet(ComposerHostedFacet.class)).thenReturn(composerHostedFacet); + when(context.getRepository()).thenReturn(repository); + when(context.getAttributes()).thenReturn(attributes); + when(attributes.require(TokenMatcher.State.class)).thenReturn(state); + when(state.getTokens()).thenReturn(tokens); + } + + @Test + public void testHandlePackages() throws Exception { + when(attributes.require(AssetKind.class)).thenReturn(PACKAGES); + when(composerHostedFacet.getPackagesJson()).thenReturn(content); + Response response = underTest.handle(context); + assertThat(response.getStatus().getCode(), is(200)); + assertThat(response.getPayload(), is(content)); + } + + @Test + public void testHandleProvider() throws Exception { + when(attributes.require(AssetKind.class)).thenReturn(PROVIDER); + when(tokens.get(VENDOR_TOKEN)).thenReturn(VENDOR); + when(tokens.get(PROJECT_TOKEN)).thenReturn(PROJECT); + when(composerHostedFacet.getProviderJson(VENDOR, PROJECT)).thenReturn(content); + Response response = underTest.handle(context); + assertThat(response.getStatus().getCode(), is(200)); + assertThat(response.getPayload(), is(content)); + } + + @Test + public void testHandleList() throws Exception { + exception.expect(IllegalStateException.class); + exception.expectMessage("Unsupported assetKind: " + LIST); + when(attributes.require(AssetKind.class)).thenReturn(LIST); + underTest.handle(context); + } + + @Test + public void testHandleZipballPresent() throws Exception { + when(attributes.require(AssetKind.class)).thenReturn(ZIPBALL); + when(tokens.get(VENDOR_TOKEN)).thenReturn(VENDOR); + when(tokens.get(PROJECT_TOKEN)).thenReturn(PROJECT); + when(tokens.get(VERSION_TOKEN)).thenReturn(VERSION); + when(tokens.get(NAME_TOKEN)).thenReturn(NAME); + when(composerHostedFacet.getZipball(ZIPBALL_PATH)).thenReturn(content); + Response response = underTest.handle(context); + assertThat(response.getStatus().getCode(), is(200)); + assertThat(response.getPayload(), is(content)); + } + + @Test + public void testHandleZipballAbsent() throws Exception { + when(attributes.require(AssetKind.class)).thenReturn(ZIPBALL); + when(tokens.get(VENDOR_TOKEN)).thenReturn(VENDOR); + when(tokens.get(PROJECT_TOKEN)).thenReturn(PROJECT); + when(tokens.get(VERSION_TOKEN)).thenReturn(VERSION); + when(tokens.get(NAME_TOKEN)).thenReturn(NAME); + when(composerHostedFacet.getZipball(ZIPBALL_PATH)).thenReturn(null); + Response response = underTest.handle(context); + assertThat(response.getStatus().getCode(), is(404)); + assertThat(response.getPayload(), is(nullValue())); + } +} diff --git a/src/test/java/org/sonatype/nexus/repository/composer/internal/ComposerHostedFacetImplTest.java b/src/test/java/org/sonatype/nexus/repository/composer/internal/ComposerHostedFacetImplTest.java new file mode 100644 index 00000000..11dfdc4e --- /dev/null +++ b/src/test/java/org/sonatype/nexus/repository/composer/internal/ComposerHostedFacetImplTest.java @@ -0,0 +1,128 @@ +/* + * Sonatype Nexus (TM) Open Source Version + * Copyright (c) 2018-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.composer.internal; + +import org.sonatype.goodies.testsupport.TestSupport; +import org.sonatype.nexus.repository.Repository; +import org.sonatype.nexus.repository.storage.Bucket; +import org.sonatype.nexus.repository.storage.Component; +import org.sonatype.nexus.repository.storage.Query; +import org.sonatype.nexus.repository.storage.StorageTx; +import org.sonatype.nexus.repository.view.Content; +import org.sonatype.nexus.repository.view.Payload; +import org.sonatype.nexus.transaction.UnitOfWork; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; + +import static java.util.Collections.singletonList; +import static org.hamcrest.Matchers.hasEntry; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.sonatype.nexus.repository.composer.internal.AssetKind.ZIPBALL; + +public class ComposerHostedFacetImplTest + extends TestSupport +{ + private static final String VENDOR = "test-vendor"; + + private static final String PROJECT = "test-project"; + + private static final String ZIPBALL_PATH = "test-vendor/test-project/version/vendor-project-version.zip"; + + @Mock + private Repository repository; + + @Mock + private Bucket bucket; + + @Mock + private ComposerContentFacet composerContentFacet; + + @Mock + private ComposerJsonProcessor composerJsonProcessor; + + @Mock + private Payload payload; + + @Mock + private Content content; + + @Mock + private StorageTx tx; + + @Mock + private Iterable components; + + @Mock + private Query query; + + private ComposerHostedFacetImpl underTest; + + @Before + public void setUp() throws Exception { + when(repository.facet(ComposerContentFacet.class)).thenReturn(composerContentFacet); + when(tx.findBucket(repository)).thenReturn(bucket); + + underTest = spy(new ComposerHostedFacetImpl(composerJsonProcessor)); + underTest.attach(repository); + + UnitOfWork.beginBatch(tx); + } + + @After + public void tearDown() { + UnitOfWork.end(); + } + + @Test + public void testUpload() throws Exception { + underTest.upload(ZIPBALL_PATH, payload); + verify(composerContentFacet).put(ZIPBALL_PATH, payload, ZIPBALL); + } + + @Test + public void testGetZipball() throws Exception { + when(composerContentFacet.get(ZIPBALL_PATH)).thenReturn(content); + assertThat(underTest.getZipball(ZIPBALL_PATH), is(content)); + } + + @Test + public void testGetPackagesJson() throws Exception { + when(tx.browseComponents(bucket)).thenReturn(components); + when(composerJsonProcessor.generatePackagesFromComponents(repository, components)).thenReturn(content); + assertThat(underTest.getPackagesJson(), is(content)); + } + + @Test + public void testGetProviderJson() throws Exception { + when(underTest.buildQuery(VENDOR, PROJECT)).thenReturn(query); + when(tx.findComponents(eq(query), eq(singletonList(repository)))).thenReturn(components); + when(composerJsonProcessor.buildProviderJson(repository, components)).thenReturn(content); + assertThat(underTest.getProviderJson(VENDOR, PROJECT), is(content)); + } + + @Test + public void testBuildQuery() throws Exception { + Query result = underTest.buildQuery(VENDOR, PROJECT); + assertThat(result.getWhere(), is("group = :p0 AND name = :p1")); + assertThat(result.getParameters(), hasEntry("p0", VENDOR)); + assertThat(result.getParameters(), hasEntry("p1", PROJECT)); + } +} diff --git a/src/test/java/org/sonatype/nexus/repository/composer/internal/ComposerHostedUploadHandlerTest.java b/src/test/java/org/sonatype/nexus/repository/composer/internal/ComposerHostedUploadHandlerTest.java new file mode 100644 index 00000000..c2b580fd --- /dev/null +++ b/src/test/java/org/sonatype/nexus/repository/composer/internal/ComposerHostedUploadHandlerTest.java @@ -0,0 +1,88 @@ +/* + * Sonatype Nexus (TM) Open Source Version + * Copyright (c) 2018-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.composer.internal; + +import java.util.Map; + +import org.sonatype.goodies.testsupport.TestSupport; +import org.sonatype.nexus.common.collect.AttributesMap; +import org.sonatype.nexus.repository.Repository; +import org.sonatype.nexus.repository.view.Context; +import org.sonatype.nexus.repository.view.Payload; +import org.sonatype.nexus.repository.view.Request; +import org.sonatype.nexus.repository.view.Response; +import org.sonatype.nexus.repository.view.matchers.token.TokenMatcher; + +import org.junit.Test; +import org.mockito.Mock; + +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.sonatype.nexus.repository.composer.internal.ComposerRecipeSupport.PROJECT_TOKEN; +import static org.sonatype.nexus.repository.composer.internal.ComposerRecipeSupport.VENDOR_TOKEN; +import static org.sonatype.nexus.repository.composer.internal.ComposerRecipeSupport.VERSION_TOKEN; + +public class ComposerHostedUploadHandlerTest + extends TestSupport +{ + private ComposerHostedUploadHandler underTest = new ComposerHostedUploadHandler(); + + @Mock + private Map tokens; + + @Mock + private TokenMatcher.State state; + + @Mock + private ComposerHostedFacet composerHostedFacet; + + @Mock + private Repository repository; + + @Mock + private Context context; + + @Mock + private Request request; + + @Mock + private Payload payload; + + @Mock + private AttributesMap attributes; + + @Test + public void testHandle() throws Exception { + when(repository.facet(ComposerHostedFacet.class)).thenReturn(composerHostedFacet); + when(request.getPayload()).thenReturn(payload); + when(context.getRepository()).thenReturn(repository); + when(context.getAttributes()).thenReturn(attributes); + when(context.getRequest()).thenReturn(request); + + when(attributes.require(TokenMatcher.State.class)).thenReturn(state); + when(state.getTokens()).thenReturn(tokens); + when(tokens.get(VENDOR_TOKEN)).thenReturn("testvendor"); + when(tokens.get(PROJECT_TOKEN)).thenReturn("testproject"); + when(tokens.get(VERSION_TOKEN)).thenReturn("testversion"); + + Response response = underTest.handle(context); + assertThat(response.getStatus().getCode(), is(200)); + assertThat(response.getPayload(), is(nullValue())); + + verify(composerHostedFacet) + .upload("testvendor/testproject/testversion/testvendor-testproject-testversion.zip", payload); + } +} diff --git a/src/test/java/org/sonatype/nexus/repository/composer/internal/ComposerJsonProcessorTest.java b/src/test/java/org/sonatype/nexus/repository/composer/internal/ComposerJsonProcessorTest.java index c665e9d7..3015c9ad 100644 --- a/src/test/java/org/sonatype/nexus/repository/composer/internal/ComposerJsonProcessorTest.java +++ b/src/test/java/org/sonatype/nexus/repository/composer/internal/ComposerJsonProcessorTest.java @@ -19,14 +19,18 @@ import org.sonatype.goodies.testsupport.TestSupport; import org.sonatype.nexus.repository.Repository; +import org.sonatype.nexus.repository.storage.Component; import org.sonatype.nexus.repository.view.Content; import org.sonatype.nexus.repository.view.Payload; import com.google.common.io.CharStreams; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; import org.junit.Test; import org.mockito.Mock; import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Arrays.asList; import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertThat; import static org.mockito.Mockito.when; @@ -41,6 +45,18 @@ public class ComposerJsonProcessorTest @Mock private Payload payload; + @Mock + private Component component1; + + @Mock + private Component component2; + + @Mock + private Component component3; + + @Mock + private Component component4; + @Test public void generatePackagesFromList() throws Exception { String listJson = readStreamToString(getClass().getResourceAsStream("generatePackagesFromList.list.json")); @@ -50,9 +66,27 @@ public void generatePackagesFromList() throws Exception { when(payload.openInputStream()).thenReturn(new ByteArrayInputStream(listJson.getBytes(UTF_8))); ComposerJsonProcessor underTest = new ComposerJsonProcessor(); - Content output = underTest.generatePackagesJson(repository, payload); + Content output = underTest.generatePackagesFromList(repository, payload); + + assertEquals(packagesJson, readStreamToString(output.openInputStream()), true); + } + + @Test + public void generatePackagesFromComponents() throws Exception { + String packagesJson = readStreamToString(getClass().getResourceAsStream("generatePackagesFromComponents.json")); + + when(repository.getUrl()).thenReturn("http://nexus.repo/base/repo"); + + when(component1.group()).thenReturn("vendor1"); + when(component1.name()).thenReturn("project1"); + + when(component2.group()).thenReturn("vendor2"); + when(component2.name()).thenReturn("project2"); + + ComposerJsonProcessor underTest = new ComposerJsonProcessor(); + Content output = underTest.generatePackagesFromComponents(repository, asList(component1, component2)); - assertEquals(readStreamToString(output.openInputStream()), packagesJson, false); + assertEquals(packagesJson, readStreamToString(output.openInputStream()), true); } @Test @@ -66,7 +100,39 @@ public void rewriteProviderJson() throws Exception { ComposerJsonProcessor underTest = new ComposerJsonProcessor(); Payload output = underTest.rewriteProviderJson(repository, payload); - assertEquals(readStreamToString(output.openInputStream()), outputJson, false); + assertEquals(outputJson, readStreamToString(output.openInputStream()), true); + } + + @Test + public void buildProviderJson() throws Exception { + String outputJson = readStreamToString(getClass().getResourceAsStream("buildProviderJson.json")); + + when(repository.getUrl()).thenReturn("http://nexus.repo/base/repo"); + + when(component1.group()).thenReturn("vendor1"); + when(component1.name()).thenReturn("project1"); + when(component1.version()).thenReturn("1.0.0"); + when(component1.requireLastUpdated()).thenReturn(new DateTime(392056200000L, DateTimeZone.forOffsetHours(-4))); + + when(component2.group()).thenReturn("vendor1"); + when(component2.name()).thenReturn("project1"); + when(component2.version()).thenReturn("2.0.0"); + when(component2.requireLastUpdated()).thenReturn(new DateTime(1210869000000L, DateTimeZone.forOffsetHours(-4))); + + when(component3.group()).thenReturn("vendor2"); + when(component3.name()).thenReturn("project2"); + when(component3.version()).thenReturn("3.0.0"); + when(component3.requireLastUpdated()).thenReturn(new DateTime(300558600000L, DateTimeZone.forOffsetHours(-4))); + + when(component4.group()).thenReturn("vendor2"); + when(component4.name()).thenReturn("project2"); + when(component4.version()).thenReturn("4.0.0"); + when(component4.requireLastUpdated()).thenReturn(new DateTime(1210869000000L, DateTimeZone.forOffsetHours(-4))); + + ComposerJsonProcessor underTest = new ComposerJsonProcessor(); + Content output = underTest.buildProviderJson(repository, asList(component1, component2, component3, component4)); + + assertEquals(outputJson, readStreamToString(output.openInputStream()), true); } @Test diff --git a/src/test/java/org/sonatype/nexus/repository/composer/internal/ComposerPathUtilsTest.java b/src/test/java/org/sonatype/nexus/repository/composer/internal/ComposerPathUtilsTest.java new file mode 100644 index 00000000..410a1688 --- /dev/null +++ b/src/test/java/org/sonatype/nexus/repository/composer/internal/ComposerPathUtilsTest.java @@ -0,0 +1,102 @@ +/* + * Sonatype Nexus (TM) Open Source Version + * Copyright (c) 2018-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.composer.internal; + +import java.util.Map; + +import org.sonatype.goodies.testsupport.TestSupport; +import org.sonatype.nexus.common.collect.AttributesMap; +import org.sonatype.nexus.repository.view.Context; +import org.sonatype.nexus.repository.view.matchers.token.TokenMatcher; + +import org.junit.Test; +import org.mockito.Mock; + +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.when; +import static org.sonatype.nexus.repository.composer.internal.ComposerRecipeSupport.NAME_TOKEN; +import static org.sonatype.nexus.repository.composer.internal.ComposerRecipeSupport.PROJECT_TOKEN; +import static org.sonatype.nexus.repository.composer.internal.ComposerRecipeSupport.VENDOR_TOKEN; +import static org.sonatype.nexus.repository.composer.internal.ComposerRecipeSupport.VERSION_TOKEN; + +public class ComposerPathUtilsTest + extends TestSupport +{ + @Mock + private Context context; + + @Mock + private AttributesMap contextAttributes; + + @Mock + private TokenMatcher.State state; + + @Mock + private Map tokens; + + @Test + public void buildZipballPathFromContextWithNameToken() { + when(context.getAttributes()).thenReturn(contextAttributes); + when(contextAttributes.require(TokenMatcher.State.class)).thenReturn(state); + when(state.getTokens()).thenReturn(tokens); + + when(tokens.get(VENDOR_TOKEN)).thenReturn("testvendor"); + when(tokens.get(PROJECT_TOKEN)).thenReturn("testproject"); + when(tokens.get(VERSION_TOKEN)).thenReturn("1.2.3"); + when(tokens.get(NAME_TOKEN)).thenReturn("name"); + + assertThat(ComposerPathUtils.buildZipballPath(context), is("testvendor/testproject/1.2.3/name.zip")); + } + + @Test + public void buildZipballPathFromContextWithoutNameToken() { + when(context.getAttributes()).thenReturn(contextAttributes); + when(contextAttributes.require(TokenMatcher.State.class)).thenReturn(state); + when(state.getTokens()).thenReturn(tokens); + + when(tokens.get(VENDOR_TOKEN)).thenReturn("testvendor"); + when(tokens.get(PROJECT_TOKEN)).thenReturn("testproject"); + when(tokens.get(VERSION_TOKEN)).thenReturn("1.2.3"); + + assertThat(ComposerPathUtils.buildZipballPath(context), + is("testvendor/testproject/1.2.3/testvendor-testproject-1.2.3.zip")); + } + + @Test + public void buildZipballPathFromValues() { + when(context.getAttributes()).thenReturn(contextAttributes); + when(contextAttributes.require(TokenMatcher.State.class)).thenReturn(state); + when(state.getTokens()).thenReturn(tokens); + + assertThat(ComposerPathUtils.buildZipballPath("testvendor", "testproject", "1.2.3"), + is("testvendor/testproject/1.2.3/testvendor-testproject-1.2.3.zip")); + } + + @Test + public void buildProviderPathFromTokens() { + when(context.getAttributes()).thenReturn(contextAttributes); + when(contextAttributes.require(TokenMatcher.State.class)).thenReturn(state); + when(state.getTokens()).thenReturn(tokens); + + when(tokens.get(VENDOR_TOKEN)).thenReturn("testvendor"); + when(tokens.get(PROJECT_TOKEN)).thenReturn("testproject"); + + assertThat(ComposerPathUtils.buildProviderPath(context), is("p/testvendor/testproject.json")); + } + + @Test + public void buildProviderPathFromValues() { + assertThat(ComposerPathUtils.buildProviderPath("testvendor", "testproject"), is("p/testvendor/testproject.json")); + } +} diff --git a/src/test/java/org/sonatype/nexus/repository/composer/internal/ComposerProxyFacetImplTest.java b/src/test/java/org/sonatype/nexus/repository/composer/internal/ComposerProxyFacetImplTest.java index 98e082eb..6e67d9cd 100644 --- a/src/test/java/org/sonatype/nexus/repository/composer/internal/ComposerProxyFacetImplTest.java +++ b/src/test/java/org/sonatype/nexus/repository/composer/internal/ComposerProxyFacetImplTest.java @@ -208,7 +208,7 @@ public void storePackages() throws Exception { when(composerContentFacet.put(PACKAGES_PATH, content, PACKAGES)).thenReturn(content); when(viewFacet.dispatch(any(Request.class), eq(context))).thenReturn(response); - when(composerJsonProcessor.generatePackagesJson(repository, payload)).thenReturn(content); + when(composerJsonProcessor.generatePackagesFromList(repository, payload)).thenReturn(content); assertThat(underTest.store(context, content), is(content)); diff --git a/src/test/resources/org/sonatype/nexus/repository/composer/internal/buildProviderJson.json b/src/test/resources/org/sonatype/nexus/repository/composer/internal/buildProviderJson.json new file mode 100644 index 00000000..65842b56 --- /dev/null +++ b/src/test/resources/org/sonatype/nexus/repository/composer/internal/buildProviderJson.json @@ -0,0 +1,48 @@ +{ + "packages" : { + "vendor1/project1" : { + "1.0.0" : { + "name": "vendor1/project1", + "version": "1.0.0", + "dist" : { + "url" : "http://nexus.repo/base/repo/vendor1/project1/1.0.0/vendor1-project1-1.0.0.zip", + "type" : "zip" + }, + "uid" : 1345846687, + "time" : "1982-06-04T16:30:00+00:00" + }, + "2.0.0" : { + "name": "vendor1/project1", + "version": "2.0.0", + "dist" : { + "url" : "http://nexus.repo/base/repo/vendor1/project1/2.0.0/vendor1-project1-2.0.0.zip", + "type" : "zip" + }, + "uid" : 1700253429, + "time" : "2008-05-15T16:30:00+00:00" + } + }, + "vendor2/project2" : { + "3.0.0" : { + "name": "vendor2/project2", + "version": "3.0.0", + "dist" : { + "url" : "http://nexus.repo/base/repo/vendor2/project2/3.0.0/vendor2-project2-3.0.0.zip", + "type" : "zip" + }, + "uid" : 1398386772, + "time" : "1979-07-11T16:30:00+00:00" + }, + "4.0.0" : { + "name": "vendor2/project2", + "version": "4.0.0", + "dist" : { + "url" : "http://nexus.repo/base/repo/vendor2/project2/4.0.0/vendor2-project2-4.0.0.zip", + "type" : "zip" + }, + "uid" : 237122898, + "time" : "2008-05-15T16:30:00+00:00" + } + } + } +} diff --git a/src/test/resources/org/sonatype/nexus/repository/composer/internal/generatePackagesFromComponents.json b/src/test/resources/org/sonatype/nexus/repository/composer/internal/generatePackagesFromComponents.json new file mode 100644 index 00000000..a74ab968 --- /dev/null +++ b/src/test/resources/org/sonatype/nexus/repository/composer/internal/generatePackagesFromComponents.json @@ -0,0 +1,11 @@ +{ + "providers-url": "http://nexus.repo/base/repo/p/%package%.json", + "providers": { + "vendor1/project1": { + "sha256": null + }, + "vendor2/project2": { + "sha256": null + } + } +}