diff options
author | Martin Stockhammer <martin_s@apache.org> | 2021-01-19 09:36:23 +0100 |
---|---|---|
committer | Martin Stockhammer <martin_s@apache.org> | 2021-01-19 09:36:23 +0100 |
commit | b97724c6a70b18f5667b45a20375ff550bd9015c (patch) | |
tree | 0b662774fb60eca9252b0552b6a241ae750d83b5 /archiva-modules/archiva-web | |
parent | 3313a6cee8bc30e0a9b87c83998cfb219bce7306 (diff) | |
download | archiva-b97724c6a70b18f5667b45a20375ff550bd9015c.tar.gz archiva-b97724c6a70b18f5667b45a20375ff550bd9015c.zip |
Refactoring exceptions and adding REST V2 service
Diffstat (limited to 'archiva-modules/archiva-web')
6 files changed, 843 insertions, 0 deletions
diff --git a/archiva-modules/archiva-web/archiva-rest/archiva-rest-api/src/main/java/org/apache/archiva/rest/api/model/v2/MergeConfiguration.java b/archiva-modules/archiva-web/archiva-rest/archiva-rest-api/src/main/java/org/apache/archiva/rest/api/model/v2/MergeConfiguration.java new file mode 100644 index 000000000..c5e60dad8 --- /dev/null +++ b/archiva-modules/archiva-web/archiva-rest/archiva-rest-api/src/main/java/org/apache/archiva/rest/api/model/v2/MergeConfiguration.java @@ -0,0 +1,110 @@ +package org.apache.archiva.rest.api.model.v2;/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import io.swagger.v3.oas.annotations.media.Schema; + +import javax.xml.bind.annotation.XmlRootElement; +import java.io.Serializable; +import java.util.Objects; + +import static org.apache.archiva.indexer.ArchivaIndexManager.DEFAULT_INDEX_PATH; + +/** + * Index merge configuration. + * + * @author Martin Stockhammer <martin_s@apache.org> + * @since 3.0 + */ +@XmlRootElement(name="mergeConfiguration") +@Schema(name="MergeConfiguration", description = "Configuration settings for index merge of remote repositories.") +public class MergeConfiguration implements Serializable +{ + private static final long serialVersionUID = -3629274059574459133L; + + private String mergedIndexPath = DEFAULT_INDEX_PATH; + private int mergedIndexTtlMinutes = 30; + private String indexMergeSchedule = ""; + + @Schema(name="merged_index_path", description = "The path where the merged index is stored. The path is relative to the repository directory of the group.") + public String getMergedIndexPath( ) + { + return mergedIndexPath; + } + + public void setMergedIndexPath( String mergedIndexPath ) + { + this.mergedIndexPath = mergedIndexPath; + } + + @Schema(name="merged_index_ttl_minutes", description = "The Time to Life of the merged index in minutes.") + public int getMergedIndexTtlMinutes( ) + { + return mergedIndexTtlMinutes; + } + + public void setMergedIndexTtlMinutes( int mergedIndexTtlMinutes ) + { + this.mergedIndexTtlMinutes = mergedIndexTtlMinutes; + } + + @Schema(name="index_merge_schedule", description = "Cron expression that defines the times/intervals for index merging.") + public String getIndexMergeSchedule( ) + { + return indexMergeSchedule; + } + + public void setIndexMergeSchedule( String indexMergeSchedule ) + { + this.indexMergeSchedule = indexMergeSchedule; + } + + @Override + public boolean equals( Object o ) + { + if ( this == o ) return true; + if ( o == null || getClass( ) != o.getClass( ) ) return false; + + MergeConfiguration that = (MergeConfiguration) o; + + if ( mergedIndexTtlMinutes != that.mergedIndexTtlMinutes ) return false; + if ( !Objects.equals( mergedIndexPath, that.mergedIndexPath ) ) + return false; + return Objects.equals( indexMergeSchedule, that.indexMergeSchedule ); + } + + @Override + public int hashCode( ) + { + int result = mergedIndexPath != null ? mergedIndexPath.hashCode( ) : 0; + result = 31 * result + mergedIndexTtlMinutes; + result = 31 * result + ( indexMergeSchedule != null ? indexMergeSchedule.hashCode( ) : 0 ); + return result; + } + + @SuppressWarnings( "StringBufferReplaceableByString" ) + @Override + public String toString( ) + { + final StringBuilder sb = new StringBuilder( "MergeConfiguration{" ); + sb.append( "mergedIndexPath='" ).append( mergedIndexPath ).append( '\'' ); + sb.append( ", mergedIndexTtlMinutes=" ).append( mergedIndexTtlMinutes ); + sb.append( ", indexMergeSchedule='" ).append( indexMergeSchedule ).append( '\'' ); + sb.append( '}' ); + return sb.toString( ); + } +} diff --git a/archiva-modules/archiva-web/archiva-rest/archiva-rest-api/src/main/java/org/apache/archiva/rest/api/model/v2/RepositoryGroup.java b/archiva-modules/archiva-web/archiva-rest/archiva-rest-api/src/main/java/org/apache/archiva/rest/api/model/v2/RepositoryGroup.java new file mode 100644 index 000000000..d550f27c9 --- /dev/null +++ b/archiva-modules/archiva-web/archiva-rest/archiva-rest-api/src/main/java/org/apache/archiva/rest/api/model/v2/RepositoryGroup.java @@ -0,0 +1,149 @@ +package org.apache.archiva.rest.api.model.v2;/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import io.swagger.v3.oas.annotations.media.Schema; + +import javax.xml.bind.annotation.XmlRootElement; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * @author Martin Stockhammer <martin_s@apache.org> + */ +@XmlRootElement(name="repositoryGroup") +@Schema(name="RepositoryGroup", description = "Information about a repository group, which combines multiple repositories as one virtual repository.") +public class RepositoryGroup implements Serializable +{ + private static final long serialVersionUID = -7319687481737616081L; + private String id; + private final List<String> repositories = new ArrayList<>( ); + private String location; + MergeConfiguration mergeConfiguration; + + public RepositoryGroup( ) + { + } + + public RepositoryGroup(String id) { + this.id = id; + } + + public static RepositoryGroup of( org.apache.archiva.admin.model.beans.RepositoryGroup modelObj ) { + RepositoryGroup result = new RepositoryGroup( ); + MergeConfiguration mergeConfig = new MergeConfiguration( ); + result.setMergeConfiguration( mergeConfig ); + result.setId( modelObj.getId() ); + result.setLocation( modelObj.getLocation() ); + result.setRepositories( modelObj.getRepositories() ); + mergeConfig.setMergedIndexPath( modelObj.getMergedIndexPath() ); + mergeConfig.setMergedIndexTtlMinutes( modelObj.getMergedIndexTtl( ) ); + mergeConfig.setIndexMergeSchedule( modelObj.getCronExpression( ) ); + return result; + } + + @Schema(description = "The unique id of the repository group.") + public String getId( ) + { + return id; + } + + public void setId( String id ) + { + this.id = id; + } + + @Schema(description = "The list of ids of repositories which are member of the repository group.") + public List<String> getRepositories( ) + { + return repositories; + } + + public void setRepositories( List<String> repositories ) + { + this.repositories.clear(); + this.repositories.addAll( repositories ); + } + + public void addRepository(String repositoryId) { + if (!this.repositories.contains( repositoryId )) { + this.repositories.add( repositoryId ); + } + } + + @Schema(name="merge_configuration",description = "The configuration for index merge.") + public MergeConfiguration getMergeConfiguration( ) + { + return mergeConfiguration; + } + + public void setMergeConfiguration( MergeConfiguration mergeConfiguration ) + { + this.mergeConfiguration = mergeConfiguration; + } + + @Schema(description = "The storage location of the repository. The merged index is stored relative to this location.") + public String getLocation( ) + { + return location; + } + + public void setLocation( String location ) + { + this.location = location; + } + + @Override + public boolean equals( Object o ) + { + if ( this == o ) return true; + if ( o == null || getClass( ) != o.getClass( ) ) return false; + + RepositoryGroup that = (RepositoryGroup) o; + + if ( !Objects.equals( id, that.id ) ) return false; + if ( !repositories.equals( that.repositories ) ) + return false; + if ( !Objects.equals( location, that.location ) ) return false; + return Objects.equals( mergeConfiguration, that.mergeConfiguration ); + } + + @Override + public int hashCode( ) + { + int result = id != null ? id.hashCode( ) : 0; + result = 31 * result + repositories.hashCode( ); + result = 31 * result + ( location != null ? location.hashCode( ) : 0 ); + result = 31 * result + ( mergeConfiguration != null ? mergeConfiguration.hashCode( ) : 0 ); + return result; + } + + @SuppressWarnings( "StringBufferReplaceableByString" ) + @Override + public String toString( ) + { + final StringBuilder sb = new StringBuilder( "RepositoryGroup{" ); + sb.append( "id='" ).append( id ).append( '\'' ); + sb.append( ", repositories=" ).append( repositories ); + sb.append( ", location='" ).append( location ).append( '\'' ); + sb.append( ", mergeConfiguration=" ).append( mergeConfiguration ); + sb.append( '}' ); + return sb.toString( ); + } +} diff --git a/archiva-modules/archiva-web/archiva-rest/archiva-rest-api/src/main/java/org/apache/archiva/rest/api/services/v2/RepositoryGroupService.java b/archiva-modules/archiva-web/archiva-rest/archiva-rest-api/src/main/java/org/apache/archiva/rest/api/services/v2/RepositoryGroupService.java new file mode 100644 index 000000000..7796d1c56 --- /dev/null +++ b/archiva-modules/archiva-web/archiva-rest/archiva-rest-api/src/main/java/org/apache/archiva/rest/api/services/v2/RepositoryGroupService.java @@ -0,0 +1,257 @@ +package org.apache.archiva.rest.api.services.v2; +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.headers.Header; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import org.apache.archiva.components.rest.model.PagedResult; +import org.apache.archiva.redback.authorization.RedbackAuthorization; +import org.apache.archiva.rest.api.model.v2.RepositoryGroup; +import org.apache.archiva.security.common.ArchivaRoleConstants; + +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.DefaultValue; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Response; +import java.util.List; + +import static javax.ws.rs.core.MediaType.APPLICATION_JSON; +import static org.apache.archiva.rest.api.services.v2.Configuration.DEFAULT_PAGE_LIMIT; + +/** + * Endpoint for repository groups that combine multiple repositories into a single virtual repository. + * + * @author Olivier Lamy + * @author Martin Stockhammer + * @since 3.0 + */ +@Path( "/repository_groups" ) +@Schema( name="RepositoryGroups", description = "Managing of repository groups or virtual repositories") +public interface RepositoryGroupService +{ + @Path( "" ) + @GET + @Produces( { APPLICATION_JSON } ) + @RedbackAuthorization( permissions = ArchivaRoleConstants.OPERATION_MANAGE_CONFIGURATION ) + @Operation( summary = "Returns all repository group entries.", + parameters = { + @Parameter(name = "q", description = "Search term"), + @Parameter(name = "offset", description = "The offset of the first element returned"), + @Parameter(name = "limit", description = "Maximum number of items to return in the response"), + @Parameter(name = "orderBy", description = "List of attribute used for sorting (key, value)"), + @Parameter(name = "order", description = "The sort order. Either ascending (asc) or descending (desc)") + }, + security = { + @SecurityRequirement( + name = ArchivaRoleConstants.OPERATION_MANAGE_CONFIGURATION + ) + }, + responses = { + @ApiResponse( responseCode = "200", + description = "If the list could be returned", + content = @Content(mediaType = APPLICATION_JSON, schema = @Schema(implementation = PagedResult.class)) + ), + @ApiResponse( responseCode = "403", description = "Authenticated user is not permitted to gather the information", + content = @Content(mediaType = APPLICATION_JSON, schema = @Schema(implementation = ArchivaRestError.class )) ) + } + ) + PagedResult<RepositoryGroup> getRepositoriesGroups(@QueryParam("q") @DefaultValue( "" ) String searchTerm, + @QueryParam( "offset" ) @DefaultValue( "0" ) Integer offset, + @QueryParam( "limit" ) @DefaultValue( value = DEFAULT_PAGE_LIMIT ) Integer limit, + @QueryParam( "orderBy") @DefaultValue( "key" ) List<String> orderBy, + @QueryParam("order") @DefaultValue( "asc" ) String order) + throws ArchivaRestServiceException; + + @Path( "{repositoryGroupId}" ) + @GET + @Produces( { APPLICATION_JSON } ) + @RedbackAuthorization( permissions = ArchivaRoleConstants.OPERATION_MANAGE_CONFIGURATION ) + @Operation( summary = "Returns a single repository group configuration.", + security = { + @SecurityRequirement( + name = ArchivaRoleConstants.OPERATION_MANAGE_CONFIGURATION + ) + }, + responses = { + @ApiResponse( responseCode = "200", + description = "If the configuration is returned", + content = @Content(mediaType = APPLICATION_JSON, schema = @Schema(implementation = RepositoryGroup.class)) + ), + @ApiResponse( responseCode = "403", description = "Authenticated user is not permitted to gather the information", + content = @Content(mediaType = APPLICATION_JSON, schema = @Schema(implementation = ArchivaRestError.class )) ), + @ApiResponse( responseCode = "404", description = "The repository group with the given id does not exist", + content = @Content(mediaType = APPLICATION_JSON, schema = @Schema(implementation = ArchivaRestError.class )) ) + } + ) + RepositoryGroup getRepositoryGroup( @PathParam( "repositoryGroupId" ) String repositoryGroupId ) + throws ArchivaRestServiceException; + + @Path( "" ) + @POST + @Consumes( { APPLICATION_JSON } ) + @Produces( { APPLICATION_JSON } ) + @RedbackAuthorization( permissions = ArchivaRoleConstants.OPERATION_MANAGE_CONFIGURATION ) + @Operation( summary = "Creates a new group entry.", + requestBody = + @RequestBody(required = true, description = "The configuration of the repository group.", + content = @Content(mediaType = APPLICATION_JSON, schema = @Schema(implementation = RepositoryGroup.class)) + ) + , + security = { + @SecurityRequirement( + name = ArchivaRoleConstants.OPERATION_MANAGE_CONFIGURATION + ) + }, + responses = { + @ApiResponse( responseCode = "201", + description = "If the list could be returned", + content = @Content(mediaType = APPLICATION_JSON, schema = @Schema(implementation = RepositoryGroup.class)) + ), + @ApiResponse( responseCode = "303", description = "The repository group exists already", + headers = { + @Header( name="Location", description = "The URL of existing group", schema = @Schema(type="string")) + } + ), + @ApiResponse( responseCode = "403", description = "Authenticated user is not permitted to gather the information", + content = @Content(mediaType = APPLICATION_JSON, schema = @Schema(implementation = ArchivaRestError.class )) ), + @ApiResponse( responseCode = "422", description = "The body data is not valid", + content = @Content(mediaType = APPLICATION_JSON, schema = @Schema(implementation = ArchivaRestError.class )) ) + } + ) + RepositoryGroup addRepositoryGroup( RepositoryGroup repositoryGroup ) + throws ArchivaRestServiceException; + + @Path( "{repositoryGroupId}" ) + @PUT + @Consumes( { APPLICATION_JSON } ) + @Produces( { APPLICATION_JSON } ) + @RedbackAuthorization( permissions = ArchivaRoleConstants.OPERATION_MANAGE_CONFIGURATION ) + @Operation( summary = "Returns all repository group entries.", + requestBody = + @RequestBody(required = true, description = "The configuration of the repository group.", + content = @Content(mediaType = APPLICATION_JSON, schema = @Schema(implementation = RepositoryGroup.class)) + ) + , + security = { + @SecurityRequirement( + name = ArchivaRoleConstants.OPERATION_MANAGE_CONFIGURATION + ) + }, + responses = { + @ApiResponse( responseCode = "200", + description = "If the group is returned", + content = @Content(mediaType = APPLICATION_JSON, schema = @Schema(implementation = RepositoryGroup.class)) + ), + @ApiResponse( responseCode = "403", description = "Authenticated user is not permitted to gather the information", + content = @Content(mediaType = APPLICATION_JSON, schema = @Schema(implementation = ArchivaRestError.class )) ), + @ApiResponse( responseCode = "404", description = "The group with the given id does not exist", + content = @Content(mediaType = APPLICATION_JSON, schema = @Schema(implementation = ArchivaRestError.class )) ), + @ApiResponse( responseCode = "422", description = "The body data is not valid", + content = @Content(mediaType = APPLICATION_JSON, schema = @Schema(implementation = ArchivaRestError.class )) ) + } + ) + RepositoryGroup updateRepositoryGroup( @PathParam( "repositoryGroupId" ) String groupId, RepositoryGroup repositoryGroup ) + throws ArchivaRestServiceException; + + @Path( "{repositoryGroupId}" ) + @DELETE + @Produces( { APPLICATION_JSON } ) + @RedbackAuthorization( permissions = ArchivaRoleConstants.OPERATION_MANAGE_CONFIGURATION ) + @Operation( summary = "Deletes the repository group entry with the given id.", + security = { + @SecurityRequirement( + name = ArchivaRoleConstants.OPERATION_MANAGE_CONFIGURATION + ) + }, + responses = { + @ApiResponse( responseCode = "200", + description = "If the group was deleted" + ), + @ApiResponse( responseCode = "403", description = "Authenticated user is not permitted to delete the group", + content = @Content(mediaType = APPLICATION_JSON, schema = @Schema(implementation = ArchivaRestError.class )) ), + @ApiResponse( responseCode = "404", description = "The group with the given id does not exist", + content = @Content(mediaType = APPLICATION_JSON, schema = @Schema(implementation = ArchivaRestError.class )) ), + } + ) + Response deleteRepositoryGroup( @PathParam( "repositoryGroupId" ) String repositoryGroupId ) + throws ArchivaRestServiceException; + + @Path( "{repositoryGroupId}/repositories/{repositoryId}" ) + @PUT + @Produces( { APPLICATION_JSON } ) + @RedbackAuthorization( permissions = ArchivaRoleConstants.OPERATION_MANAGE_CONFIGURATION ) + @Operation( summary = "Adds the repository with the given id to the repository group.", + security = { + @SecurityRequirement( + name = ArchivaRoleConstants.OPERATION_MANAGE_CONFIGURATION + ) + }, + responses = { + @ApiResponse( responseCode = "200", + description = "If the repository was added or if it was already part of the group" + ), + @ApiResponse( responseCode = "403", description = "Authenticated user is not permitted to delete the group", + content = @Content(mediaType = APPLICATION_JSON, schema = @Schema(implementation = ArchivaRestError.class )) ), + @ApiResponse( responseCode = "404", description = "The group with the given id does not exist", + content = @Content(mediaType = APPLICATION_JSON, schema = @Schema(implementation = ArchivaRestError.class )) ), + } + ) + RepositoryGroup addRepositoryToGroup( @PathParam( "repositoryGroupId" ) String repositoryGroupId, + @PathParam( "repositoryId" ) String repositoryId ) + throws ArchivaRestServiceException; + + @Path( "{repositoryGroupId}/repositories/{repositoryId}" ) + @DELETE + @Produces( { APPLICATION_JSON } ) + @RedbackAuthorization( permissions = ArchivaRoleConstants.OPERATION_MANAGE_CONFIGURATION ) + @Operation( summary = "Removes the repository with the given id from the repository group.", + security = { + @SecurityRequirement( + name = ArchivaRoleConstants.OPERATION_MANAGE_CONFIGURATION + ) + }, + responses = { + @ApiResponse( responseCode = "200", + description = "If the repository was removed." + ), + @ApiResponse( responseCode = "403", description = "Authenticated user is not permitted to delete the group", + content = @Content(mediaType = APPLICATION_JSON, schema = @Schema(implementation = ArchivaRestError.class )) ), + @ApiResponse( responseCode = "404", description = "The group with the given id does not exist, or the repository was not part of the group.", + content = @Content(mediaType = APPLICATION_JSON, schema = @Schema(implementation = ArchivaRestError.class )) ), + } + ) + RepositoryGroup deleteRepositoryFromGroup( @PathParam( "repositoryGroupId" ) String repositoryGroupId, + @PathParam( "repositoryId" ) String repositoryId ) + throws ArchivaRestServiceException; + + +} diff --git a/archiva-modules/archiva-web/archiva-rest/archiva-rest-api/src/main/java/org/apache/archiva/rest/api/services/v2/package-info.java b/archiva-modules/archiva-web/archiva-rest/archiva-rest-api/src/main/java/org/apache/archiva/rest/api/services/v2/package-info.java new file mode 100644 index 000000000..0cd02b0ef --- /dev/null +++ b/archiva-modules/archiva-web/archiva-rest/archiva-rest-api/src/main/java/org/apache/archiva/rest/api/services/v2/package-info.java @@ -0,0 +1,102 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * <p>This is the V2 REST API of Archiva. It uses JAX-RS annotations for defining the endpoints. + * The API is documented with OpenApi annotations.</p> + * + * <h3>Some design principles of the API and classes:</h3> + * <ul> + * <li>All services use V2 model classes. Internal models are always converted to V2 classes.</li> + * <li>Schema attributes use the snake case syntax (lower case with '_' as divider)</li> + * <li>Return code <code>200</code> and <code>201</code> (POST) is used for successful execution.</li> + * <li>Return code <code>403</code> is used, if the user has not the permission for the action.</li> + * <li>Return code <code>422</code> is used for input that has invalid data.</li> + * </ul> + * + * <h4>Querying entity lists</h4> + * <p>The main entities of a given path are retrieved on the base path. + * Further sub entities or entries may be retrieved via subpaths. + * A single entity is returned by the "{id}" path. Be careful with technical paths that are parallel to the + * id path. Avoid naming conflicts with the id and technical paths. + * Entity attributes may be retrieved by "{id}/{attribute}" path or if there are lists or collections by + * "{id}/mycollection/{subentryid}"</p> + * + * <ul> + * <li><code>GET</code> method is used for retrieving entities on the base path ""</li> + * <li>The query for base entities should always return a paged result and be filterable and sortable</li> + * <li>Query parameters for filtering, ordering and limits should be optional and proper defaults must be set</li> + * <li>Return code <code>200</code> is used for successful retrieval</li> + * <li>This action is idempotent</li> + * </ul> + * + * <h4>Querying single entities</h4> + * <p>Single entities are retrieved on the path "{id}"</p> + * <ul> + * <li><code>GET</code> method is used for retrieving a single entity. The id is always a path parameter.</li> + * <li>Return code <code>200</code> is used for successful retrieval</li> + * <li>Return code <code>404</code> is used if the entity with the given id does not exist</li> + * <li>This action is idempotent</li> + * </ul> + * + * <h4>Creating entities</h4> + * <p>The main entities are created on the base path "".</p> + * <ul> + * <li><code>POST</code> is used for creating new entities</li> + * <li>The <code>POST</code> body must always have a complete definition of the entity.</li> + * <li>A unique <code>id</code> or <code>name</code> attribute is required for entities. If the id is generated during POST, + * it must be returned by response body.</li> + * <li>A successful <code>POST</code> request should always return the entity definition as it would be returned by the GET request.</li> + * <li>Return code <code>201</code> is used for successful creation of the new entity.</li> + * <li>A successful response has a <code>Location</code> header with the URL for retrieving the single created entity.</li> + * <li>Return code <code>303</code> is used, if the entity exists already</li> + * <li>This action is not idempotent</li> + * </ul> + * + * <h4>Updating entities</h4> + * <p>The path for entity update must contain the '{id}' of the entity. The path should be the same as for the GET operation.</p> + * <ul> + * <li><code>PUT</code> is used for updating existing entities</li> + * <li>The body contains a JSON object. Only existing attributes are updated.</li> + * <li>A successful PUT request should return the complete entity definition as it would be returned by the GET request.</li> + * <li>Return code <code>200</code> is used for successful update of the new entity. Even if nothing changed.</li> + * <li>This action is idempotent</li> + * </ul> + * + * <h4>Deleting entities</h4> + * <p>The path for entity deletion must contain the '{id}' of the entity. The path should be the same as + * for the GET operation.</p> + * <ul> + * <li><code>DELETE</code> is used for deleting existing entities</li> + * <li>The successful operation has no request and no response body</li> + * <li>Return code <code>200</code> is used for successful deletion of the new entity.</li> + * <li>This action is not idempotent</li> + * </ul> + * + * <h4>Errors</h4> + * <ul> + * <li>A error uses a return code <code>>=400</code> </li> + * <li>All errors use the same result object ({@link org.apache.archiva.rest.api.services.v2.ArchivaRestError}</li> + * <li>Error messages are returned as keys. Translation is part of the client application.</li> + * </ul> + * + * @author Martin Stockhammer <martin_s@apache.org> + * @since 3.0 + */ +package org.apache.archiva.rest.api.services.v2;
\ No newline at end of file diff --git a/archiva-modules/archiva-web/archiva-rest/archiva-rest-services/src/main/java/org/apache/archiva/rest/services/v2/DefaultRepositoryGroupService.java b/archiva-modules/archiva-web/archiva-rest/archiva-rest-services/src/main/java/org/apache/archiva/rest/services/v2/DefaultRepositoryGroupService.java new file mode 100644 index 000000000..1855ea8a9 --- /dev/null +++ b/archiva-modules/archiva-web/archiva-rest/archiva-rest-services/src/main/java/org/apache/archiva/rest/services/v2/DefaultRepositoryGroupService.java @@ -0,0 +1,214 @@ +package org.apache.archiva.rest.services.v2;/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import org.apache.archiva.admin.model.AuditInformation; +import org.apache.archiva.admin.model.EntityExistsException; +import org.apache.archiva.admin.model.EntityNotFoundException; +import org.apache.archiva.admin.model.RepositoryAdminException; +import org.apache.archiva.admin.model.group.RepositoryGroupAdmin; +import org.apache.archiva.components.rest.model.PagedResult; +import org.apache.archiva.components.rest.util.PagingHelper; +import org.apache.archiva.components.rest.util.QueryHelper; +import org.apache.archiva.redback.rest.services.RedbackAuthenticationThreadLocal; +import org.apache.archiva.redback.rest.services.RedbackRequestInformation; +import org.apache.archiva.redback.users.User; +import org.apache.archiva.rest.api.model.v2.RepositoryGroup; +import org.apache.archiva.rest.api.services.v2.ArchivaRestServiceException; +import org.apache.archiva.rest.api.services.v2.ErrorMessage; +import org.apache.archiva.rest.api.services.v2.RepositoryGroupService; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +/** + * REST V2 Implementation for repository groups. + * + * @author Martin Stockhammer <martin_s@apache.org> + * @since 3.0 + * @see RepositoryGroupService + */ +public class DefaultRepositoryGroupService implements RepositoryGroupService +{ + @Context + HttpServletResponse httpServletResponse; + + @Context + UriInfo uriInfo; + + private static final Logger log = LoggerFactory.getLogger( DefaultRepositoryGroupService.class ); + + private static final QueryHelper<org.apache.archiva.admin.model.beans.RepositoryGroup> QUERY_HELPER = new QueryHelper( new String[]{"id"} ); + private static final PagingHelper PROP_PAGING_HELPER = new PagingHelper( ); + + @Inject + private RepositoryGroupAdmin repositoryGroupAdmin; + + + static + { + QUERY_HELPER.addStringFilter( "id", org.apache.archiva.admin.model.beans.RepositoryGroup::getId ); + QUERY_HELPER.addNullsafeFieldComparator( "id", org.apache.archiva.admin.model.beans.RepositoryGroup::getId ); + } + + + protected AuditInformation getAuditInformation() + { + RedbackRequestInformation redbackRequestInformation = RedbackAuthenticationThreadLocal.get(); + User user = redbackRequestInformation == null ? null : redbackRequestInformation.getUser(); + String remoteAddr = redbackRequestInformation == null ? null : redbackRequestInformation.getRemoteAddr(); + return new AuditInformation( user, remoteAddr ); + } + + @Override + public PagedResult<RepositoryGroup> getRepositoriesGroups( String searchTerm, Integer offset, Integer limit, List<String> orderBy, String order ) throws ArchivaRestServiceException + { + try + { + Predicate<org.apache.archiva.admin.model.beans.RepositoryGroup> filter = QUERY_HELPER.getQueryFilter( searchTerm ); + Comparator<org.apache.archiva.admin.model.beans.RepositoryGroup> ordering = QUERY_HELPER.getComparator( orderBy, QUERY_HELPER.isAscending( order ) ); + int totalCount = Math.toIntExact( repositoryGroupAdmin.getRepositoriesGroups( ).stream( ).filter( filter ).count( ) ); + List<RepositoryGroup> result = repositoryGroupAdmin.getRepositoriesGroups( ).stream( ).filter( filter ).sorted( ordering ).skip( offset ).limit( limit ).map( + RepositoryGroup::of + ).collect( Collectors.toList( ) ); + return new PagedResult<>( totalCount, offset, limit, result ); + } + catch ( RepositoryAdminException e ) + { + log.error( "Repository admin error: {}", e.getMessage( ), e ); + throw new ArchivaRestServiceException( ErrorMessage.of( ErrorKeys.REPOSITORY_ADMIN_ERROR, e.getMessage() ) ); + } catch ( ArithmeticException e ) { + log.error( "Could not convert total count: {}", e.getMessage( ) ); + throw new ArchivaRestServiceException( ErrorMessage.of( ErrorKeys.INVALID_RESULT_SET_ERROR ) ); + } + + } + + @Override + public RepositoryGroup getRepositoryGroup( String repositoryGroupId ) throws ArchivaRestServiceException + { + try + { + org.apache.archiva.admin.model.beans.RepositoryGroup group = repositoryGroupAdmin.getRepositoryGroup( repositoryGroupId ); + return RepositoryGroup.of( group ); + } + catch ( EntityNotFoundException e ) { + throw new ArchivaRestServiceException( ErrorMessage.of( ErrorKeys.REPOSITORY_GROUP_NOT_EXIST, repositoryGroupId ), 404 ); + } + catch ( RepositoryAdminException e ) + { + throw new ArchivaRestServiceException( ErrorMessage.of( ErrorKeys.REPOSITORY_ADMIN_ERROR, e.getMessage() )); + } + } + + private org.apache.archiva.admin.model.beans.RepositoryGroup toModel( RepositoryGroup group) { + org.apache.archiva.admin.model.beans.RepositoryGroup result = new org.apache.archiva.admin.model.beans.RepositoryGroup( ); + result.setId( group.getId( ) ); + result.setLocation( group.getLocation( ) ); + result.setRepositories( new ArrayList<>( group.getRepositories( ) ) ); + result.setMergedIndexPath( group.getMergeConfiguration().getMergedIndexPath() ); + result.setMergedIndexTtl( group.getMergeConfiguration().getMergedIndexTtlMinutes() ); + result.setCronExpression( group.getMergeConfiguration().getIndexMergeSchedule() ); + return result; + } + + @Override + public RepositoryGroup addRepositoryGroup( RepositoryGroup repositoryGroup ) throws ArchivaRestServiceException + { + try + { + Boolean result = repositoryGroupAdmin.addRepositoryGroup( toModel( repositoryGroup ), getAuditInformation( ) ); + if (result) { + org.apache.archiva.admin.model.beans.RepositoryGroup newGroup = repositoryGroupAdmin.getRepositoryGroup( repositoryGroup.getId( ) ); + if (newGroup!=null) { + return RepositoryGroup.of( newGroup ); + } else { + throw new ArchivaRestServiceException( ErrorMessage.of( ErrorKeys.REPOSITORY_GROUP_ADD_FAILED ) ); + } + } else { + throw new ArchivaRestServiceException( ErrorMessage.of( ErrorKeys.REPOSITORY_GROUP_ADD_FAILED ) ); + } + } catch ( EntityExistsException e ) { + httpServletResponse.setHeader( "Location", uriInfo.getAbsolutePathBuilder( ).path( repositoryGroup.getId() ).build( ).toString( ) ); + throw new ArchivaRestServiceException( ErrorMessage.of( ErrorKeys.REPOSITORY_GROUP_EXIST, repositoryGroup.getId( )), 303 ); + } + catch ( RepositoryAdminException e ) + { + if (e.keyExists()) { + throw new ArchivaRestServiceException( ErrorMessage.of( ErrorKeys.PREFIX+e.getKey(), e.getParameters() ) ); + } else + { + throw new ArchivaRestServiceException( ErrorMessage.of( ErrorKeys.REPOSITORY_ADMIN_ERROR, e.getMessage( ) ) ); + } + } + } + + @Override + public RepositoryGroup updateRepositoryGroup( String groupId, RepositoryGroup repositoryGroup ) throws ArchivaRestServiceException + { + org.apache.archiva.admin.model.beans.RepositoryGroup updateGroup = toModel( repositoryGroup ); + try + { + org.apache.archiva.admin.model.beans.RepositoryGroup originGroup = repositoryGroupAdmin.getRepositoryGroup( groupId ); + if ( StringUtils.isEmpty( updateGroup.getId())) { + updateGroup.setId( groupId ); + } + if (StringUtils.isEmpty( updateGroup.getLocation() )) { + updateGroup.setLocation( originGroup.getLocation() ); + } + if (StringUtils.isEmpty( updateGroup.getMergedIndexPath() )) { + updateGroup.setMergedIndexPath( originGroup.getMergedIndexPath() ); + } + repositoryGroupAdmin.updateRepositoryGroup( updateGroup, getAuditInformation( ) ); + return RepositoryGroup.of( repositoryGroupAdmin.getRepositoryGroup( groupId ) ); + } + catch ( RepositoryAdminException e ) + { + log.error( "Repository admin error: {}", e.getMessage( ), e ); + throw new ArchivaRestServiceException( ErrorMessage.of( ErrorKeys.REPOSITORY_ADMIN_ERROR, e.getMessage( ) ) ); + } + } + + @Override + public Response deleteRepositoryGroup( String repositoryGroupId ) throws ArchivaRestServiceException + { + return null; + } + + @Override + public RepositoryGroup addRepositoryToGroup( String repositoryGroupId, String repositoryId ) throws ArchivaRestServiceException + { + return null; + } + + @Override + public RepositoryGroup deleteRepositoryFromGroup( String repositoryGroupId, String repositoryId ) throws org.apache.archiva.rest.api.services.v2.ArchivaRestServiceException + { + return null; + } +} diff --git a/archiva-modules/archiva-web/archiva-rest/archiva-rest-services/src/main/java/org/apache/archiva/rest/services/v2/ErrorKeys.java b/archiva-modules/archiva-web/archiva-rest/archiva-rest-services/src/main/java/org/apache/archiva/rest/services/v2/ErrorKeys.java index 83fec9361..9f65161c7 100644 --- a/archiva-modules/archiva-web/archiva-rest/archiva-rest-services/src/main/java/org/apache/archiva/rest/services/v2/ErrorKeys.java +++ b/archiva-modules/archiva-web/archiva-rest/archiva-rest-services/src/main/java/org/apache/archiva/rest/services/v2/ErrorKeys.java @@ -16,12 +16,19 @@ package org.apache.archiva.rest.services.v2;/* * under the License. */ +import org.apache.archiva.rest.api.services.v2.ErrorMessage; + +import java.util.List; + /** * @author Martin Stockhammer <martin_s@apache.org> */ public interface ErrorKeys { + String PREFIX = "archiva."; + String REPOSITORY_GROUP_PREFIX = PREFIX + "repository_group."; + String INVALID_RESULT_SET_ERROR = "archiva.result_set.invalid"; String REPOSITORY_ADMIN_ERROR = "archiva.repositoryadmin.error"; String LDAP_CF_INIT_FAILED = "archiva.ldap.cf.init.failed"; @@ -38,4 +45,8 @@ public interface ErrorKeys String MISSING_DATA = "archiva.missing.data"; + String REPOSITORY_GROUP_NOT_EXIST = REPOSITORY_GROUP_PREFIX+"notexist"; + String REPOSITORY_GROUP_ADD_FAILED = REPOSITORY_GROUP_PREFIX+"add.failed" ; + String REPOSITORY_GROUP_EXIST = REPOSITORY_GROUP_PREFIX+"exists"; + } |