diff --git a/src/main/java/org/commonjava/indy/service/repository/controller/AdminController.java b/src/main/java/org/commonjava/indy/service/repository/controller/AdminController.java index 3a0e793a..156cf404 100644 --- a/src/main/java/org/commonjava/indy/service/repository/controller/AdminController.java +++ b/src/main/java/org/commonjava/indy/service/repository/controller/AdminController.java @@ -15,6 +15,9 @@ */ package org.commonjava.indy.service.repository.controller; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.core.Response; import org.apache.commons.lang3.StringUtils; import org.commonjava.event.common.EventMetadata; import org.commonjava.indy.service.repository.audit.ChangeSummary; @@ -35,9 +38,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; -import jakarta.ws.rs.core.Response; import java.util.ArrayList; import java.util.List; @@ -259,4 +259,19 @@ public List getDisabledRemoteRepositories() return disabledArtifactStores; } + public boolean addConstituentToGroup( final StoreKey key, final StoreKey member ) + throws IndyWorkflowException + { + try + { + return storeManager.addConstituentToGroup( key, member ); + } + catch ( final IndyDataException e ) + { + throw new IndyWorkflowException( INTERNAL_SERVER_ERROR.getStatusCode(), + "Failed to add member {} into Group {}. Reason: {}", e, member, key, + e.getMessage() ); + } + } + } diff --git a/src/main/java/org/commonjava/indy/service/repository/data/AbstractStoreDataManager.java b/src/main/java/org/commonjava/indy/service/repository/data/AbstractStoreDataManager.java index 39675a35..d57ff50c 100644 --- a/src/main/java/org/commonjava/indy/service/repository/data/AbstractStoreDataManager.java +++ b/src/main/java/org/commonjava/indy/service/repository/data/AbstractStoreDataManager.java @@ -15,6 +15,7 @@ */ package org.commonjava.indy.service.repository.data; +import jakarta.inject.Inject; import org.commonjava.event.common.EventMetadata; import org.commonjava.event.store.StoreUpdateType; import org.commonjava.indy.service.repository.audit.ChangeSummary; @@ -32,7 +33,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import jakarta.inject.Inject; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; @@ -48,9 +48,9 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import static jakarta.ws.rs.core.Response.Status.METHOD_NOT_ALLOWED; import static java.util.Collections.emptySet; import static java.util.Collections.singletonMap; -import static jakarta.ws.rs.core.Response.Status.METHOD_NOT_ALLOWED; import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; import static org.commonjava.indy.service.repository.data.StoreUpdateAction.DELETE; @@ -617,4 +617,8 @@ protected CacheProducer getCacheProducer() { return null; } + + @Override + public abstract boolean addConstituentToGroup( final StoreKey key, final StoreKey member ) + throws IndyDataException; } diff --git a/src/main/java/org/commonjava/indy/service/repository/data/StoreDataManager.java b/src/main/java/org/commonjava/indy/service/repository/data/StoreDataManager.java index 2d88af77..2344d4cd 100644 --- a/src/main/java/org/commonjava/indy/service/repository/data/StoreDataManager.java +++ b/src/main/java/org/commonjava/indy/service/repository/data/StoreDataManager.java @@ -165,4 +165,6 @@ Set affectedBy( Collection keys ) Set getArtifactStoresByPkgAndType( String packageType, StoreType storeType ); + boolean addConstituentToGroup( final StoreKey key, final StoreKey member ) + throws IndyDataException; } diff --git a/src/main/java/org/commonjava/indy/service/repository/data/cassandra/CassandraStoreDataManager.java b/src/main/java/org/commonjava/indy/service/repository/data/cassandra/CassandraStoreDataManager.java index d89aa244..fe9cbb81 100644 --- a/src/main/java/org/commonjava/indy/service/repository/data/cassandra/CassandraStoreDataManager.java +++ b/src/main/java/org/commonjava/indy/service/repository/data/cassandra/CassandraStoreDataManager.java @@ -17,6 +17,8 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; import org.commonjava.indy.model.core.PathStyle; import org.commonjava.indy.service.repository.audit.ChangeSummary; import org.commonjava.indy.service.repository.change.event.StoreEventDispatcher; @@ -24,6 +26,7 @@ import org.commonjava.indy.service.repository.data.annotations.ClusterStoreDataManager; import org.commonjava.indy.service.repository.data.infinispan.CacheHandle; import org.commonjava.indy.service.repository.data.infinispan.CacheProducer; +import org.commonjava.indy.service.repository.exception.IndyDataException; import org.commonjava.indy.service.repository.model.AbstractRepository; import org.commonjava.indy.service.repository.model.ArtifactStore; import org.commonjava.indy.service.repository.model.Group; @@ -34,8 +37,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; @@ -45,6 +46,7 @@ import java.util.Optional; import java.util.Set; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -673,4 +675,29 @@ protected CacheProducer getCacheProducer() return cacheProducer; } + @Override + public boolean addConstituentToGroup( final StoreKey key, final StoreKey member ) + throws IndyDataException + { + AtomicReference error = new AtomicReference<>(); + Boolean res = opLocks.lockAnd( key, LOCK_TIMEOUT_SECONDS, k -> storeQuery.addConstituentToGroup( k, member ), + ( k, lock ) -> { + error.set( new IndyDataException( + "Failed to lock: %s for Group member add after %d seconds.", k, + LOCK_TIMEOUT_SECONDS ) ); + return false; + } ); + + if ( res == null ) + { + throw new IndyDataException( "Add Group member failed due to tryLock timeout." ); + } + IndyDataException ex = error.get(); + if ( ex != null ) + { + throw ex; + } + return res; + } + } diff --git a/src/main/java/org/commonjava/indy/service/repository/data/cassandra/CassandraStoreQuery.java b/src/main/java/org/commonjava/indy/service/repository/data/cassandra/CassandraStoreQuery.java index 362992b0..62c0f6df 100644 --- a/src/main/java/org/commonjava/indy/service/repository/data/cassandra/CassandraStoreQuery.java +++ b/src/main/java/org/commonjava/indy/service/repository/data/cassandra/CassandraStoreQuery.java @@ -23,17 +23,24 @@ import com.datastax.driver.core.exceptions.NoHostAvailableException; import com.datastax.driver.mapping.Mapper; import com.datastax.driver.mapping.MappingManager; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; import org.commonjava.indy.service.repository.model.StoreKey; import org.commonjava.indy.service.repository.model.StoreType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import jakarta.annotation.PostConstruct; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; +import java.util.ArrayList; import java.util.HashSet; +import java.util.List; +import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; +import static org.commonjava.indy.service.repository.data.cassandra.CassandraStoreUtil.CONSTITUENTS; import static org.commonjava.indy.service.repository.data.cassandra.CassandraStoreUtil.TABLE_AFFECTED_STORE; import static org.commonjava.indy.service.repository.data.cassandra.CassandraStoreUtil.TABLE_STORE; @@ -50,6 +57,9 @@ public class CassandraStoreQuery @Inject CassandraConfiguration config; + @Inject + ObjectMapper objectMapper; + private Mapper storeMapper; private Session session; @@ -143,6 +153,70 @@ public DtxArtifactStore getArtifactStore( String packageType, StoreType type, St return toDtxArtifactStore( result.one() ); } + public boolean addConstituentToGroup( StoreKey key, StoreKey member ) + { + DtxArtifactStore dtxArtifactStore = getArtifactStore( key.getPackageType(), key.getType(), key.getName() ); + if ( dtxArtifactStore == null ) + { + logger.warn( "No DtxArtifactStore was found to match the StoreKey {}.", key ); + return false; + } + + Map extras = dtxArtifactStore.getExtras(); + String members = extras.get( CONSTITUENTS ); + if ( members == null || members.isEmpty() ) + { + return saveExtraValue( member, extras, dtxArtifactStore ); + } + + List memberStrList = readListValue( members ); + if ( memberStrList == null ) + { + return false; + } + List memberList = memberStrList.stream().map( StoreKey::fromString ).collect( Collectors.toList() ); + + if ( memberList.contains( member ) ) + { + logger.info( "StoreKey {} was already existed in Group {} members, skip.", member, key ); + return true; + } + else + { + memberList.add( member ); + return saveExtraValue( memberList, extras, dtxArtifactStore ); + } + } + + private List readListValue( String value ) + { + try + { + return objectMapper.readValue( value, List.class ); + } + catch ( JsonProcessingException e ) + { + logger.error( "Failed to read member list value, value: {}.", value, e ); + return null; + } + } + + private boolean saveExtraValue( Object value, Map extras, DtxArtifactStore dtxArtifactStore ) + { + try + { + extras.put( CONSTITUENTS, objectMapper.writeValueAsString( value ) ); + dtxArtifactStore.setExtras( extras ); + storeMapper.save( dtxArtifactStore ); + return true; + } + catch ( JsonProcessingException e ) + { + logger.error( "Failed to write value into extra, value: {}", value, e ); + return false; + } + } + public Set getArtifactStoresByPkgAndType( String packageType, StoreType type ) { diff --git a/src/main/java/org/commonjava/indy/service/repository/data/mem/MemoryStoreDataManager.java b/src/main/java/org/commonjava/indy/service/repository/data/mem/MemoryStoreDataManager.java index 7c9e8bf8..c1a5b115 100644 --- a/src/main/java/org/commonjava/indy/service/repository/data/mem/MemoryStoreDataManager.java +++ b/src/main/java/org/commonjava/indy/service/repository/data/mem/MemoryStoreDataManager.java @@ -15,19 +15,21 @@ */ package org.commonjava.indy.service.repository.data.mem; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; import org.commonjava.indy.service.repository.audit.ChangeSummary; -import org.commonjava.indy.service.repository.data.AbstractStoreDataManager; -import org.commonjava.indy.service.repository.data.annotations.MemStoreDataManager; import org.commonjava.indy.service.repository.change.event.NoOpStoreEventDispatcher; import org.commonjava.indy.service.repository.change.event.StoreEventDispatcher; +import org.commonjava.indy.service.repository.data.AbstractStoreDataManager; +import org.commonjava.indy.service.repository.data.annotations.MemStoreDataManager; +import org.commonjava.indy.service.repository.exception.IndyDataException; import org.commonjava.indy.service.repository.model.ArtifactStore; +import org.commonjava.indy.service.repository.model.Group; import org.commonjava.indy.service.repository.model.StoreKey; import org.commonjava.indy.service.repository.model.StoreType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; import java.util.HashMap; import java.util.HashSet; import java.util.Map; @@ -171,4 +173,18 @@ protected ArtifactStore putArtifactStoreInternal( StoreKey storeKey, ArtifactSto return stores.put( storeKey, store ); } + @Override + public boolean addConstituentToGroup( final StoreKey key, final StoreKey member ) + throws IndyDataException + { + Group group = (Group) stores.get( key ); + if ( group != null ) + { + group.addConstituent( member ); + stores.put( key, group ); + return true; + } + return false; + } + } diff --git a/src/main/java/org/commonjava/indy/service/repository/jaxrs/RepositoryAdminResources.java b/src/main/java/org/commonjava/indy/service/repository/jaxrs/RepositoryAdminResources.java index 6bf13b66..798f0f54 100644 --- a/src/main/java/org/commonjava/indy/service/repository/jaxrs/RepositoryAdminResources.java +++ b/src/main/java/org/commonjava/indy/service/repository/jaxrs/RepositoryAdminResources.java @@ -16,6 +16,22 @@ package org.commonjava.indy.service.repository.jaxrs; import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.HEAD; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.Response.Status; +import jakarta.ws.rs.core.UriInfo; import org.apache.commons.io.IOUtils; import org.commonjava.atlas.maven.ident.util.JoinString; import org.commonjava.indy.service.repository.controller.AdminController; @@ -40,22 +56,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; -import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.DELETE; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.HEAD; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.PUT; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.PathParam; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.QueryParam; -import jakarta.ws.rs.core.Context; -import jakarta.ws.rs.core.Response; -import jakarta.ws.rs.core.Response.Status; -import jakarta.ws.rs.core.UriInfo; import java.io.IOException; import java.net.URI; import java.nio.charset.Charset; @@ -608,4 +608,57 @@ public Response returnDisabledStores( @PathParam( "packageType" ) String package return responseHelper.formatOkResponseWithJsonEntity( adminController.getDisabledRemoteRepositories() ); } + @Operation( description = "Add specific member into Group" ) + @Parameters( value = { + @Parameter( name = "packageType", in = PATH, description = "The package type of the Group repository.", + example = "maven, npm, generic-http", required = true ), + @Parameter( name = "type", in = PATH, description = "The type of the repository. Must be group.", + schema = @Schema( enumeration = { "group" } ), required = true ) } ) + @RequestBody( description = "The member store key definition JSON", name = "body", required = true, + content = @Content( schema = @Schema( implementation = StoreKey.class ) ) ) + @APIResponse( responseCode = "200", description = "The Group member was added" ) + @Path( "/{name}/addConstituent" ) + @PUT + @Consumes( APPLICATION_JSON ) + public Response addConstituentToGroup( final @PathParam( "packageType" ) String packageType, + final @Parameter( in = PATH, required = true ) + @PathParam( "name" ) String name, final @Context HttpRequest request ) + { + String json = null; + Response response; + StoreKey member; + try + { + json = IOUtils.toString( request.getInputStream(), Charset.defaultCharset() ); + logger.debug( "{}", json ); + member = objectMapper.readValue( json, StoreKey.class ); + } + catch ( final IOException e ) + { + logger.error( "Failed to parse member StoreKey {} from request body.", json, e ); + return responseHelper.formatResponse( e ); + } + + StoreKey key = new StoreKey( packageType, StoreType.group, name ); + try + { + logger.info( "Adding member {} into Group {}", member, key ); + if ( adminController.addConstituentToGroup( key, member ) ) + { + response = ok().build(); + } + else + { + logger.warn( "Member {} was NOT added into Group {}!", member, key ); + response = notModified().build(); + } + } + catch ( final IndyWorkflowException e ) + { + logger.error( e.getMessage() ); + response = responseHelper.formatResponse( e ); + } + + return response; + } } \ No newline at end of file