]> source.dussan.org Git - archiva.git/commitdiff
Refactoring exceptions and adding REST V2 service
authorMartin Stockhammer <martin_s@apache.org>
Tue, 19 Jan 2021 08:36:23 +0000 (09:36 +0100)
committerMartin Stockhammer <martin_s@apache.org>
Tue, 19 Jan 2021 08:36:23 +0000 (09:36 +0100)
13 files changed:
archiva-modules/archiva-base/archiva-repository-admin/archiva-repository-admin-api/src/main/java/org/apache/archiva/admin/model/EntityExistsException.java [new file with mode: 0644]
archiva-modules/archiva-base/archiva-repository-admin/archiva-repository-admin-api/src/main/java/org/apache/archiva/admin/model/EntityNotFoundException.java [new file with mode: 0644]
archiva-modules/archiva-base/archiva-repository-admin/archiva-repository-admin-api/src/main/java/org/apache/archiva/admin/model/RepositoryAdminException.java
archiva-modules/archiva-base/archiva-repository-admin/archiva-repository-admin-api/src/main/java/org/apache/archiva/admin/model/beans/RepositoryGroup.java
archiva-modules/archiva-base/archiva-repository-admin/archiva-repository-admin-api/src/main/java/org/apache/archiva/admin/model/group/RepositoryGroupAdmin.java
archiva-modules/archiva-base/archiva-repository-admin/archiva-repository-admin-api/src/main/resources/org/apache/archiva/admin/model/error/AdminErrors.properties [new file with mode: 0644]
archiva-modules/archiva-base/archiva-repository-admin/archiva-repository-admin-default/src/main/java/org/apache/archiva/admin/repository/group/DefaultRepositoryGroupAdmin.java
archiva-modules/archiva-web/archiva-rest/archiva-rest-api/src/main/java/org/apache/archiva/rest/api/model/v2/MergeConfiguration.java [new file with mode: 0644]
archiva-modules/archiva-web/archiva-rest/archiva-rest-api/src/main/java/org/apache/archiva/rest/api/model/v2/RepositoryGroup.java [new file with mode: 0644]
archiva-modules/archiva-web/archiva-rest/archiva-rest-api/src/main/java/org/apache/archiva/rest/api/services/v2/RepositoryGroupService.java [new file with mode: 0644]
archiva-modules/archiva-web/archiva-rest/archiva-rest-api/src/main/java/org/apache/archiva/rest/api/services/v2/package-info.java [new file with mode: 0644]
archiva-modules/archiva-web/archiva-rest/archiva-rest-services/src/main/java/org/apache/archiva/rest/services/v2/DefaultRepositoryGroupService.java [new file with mode: 0644]
archiva-modules/archiva-web/archiva-rest/archiva-rest-services/src/main/java/org/apache/archiva/rest/services/v2/ErrorKeys.java

diff --git a/archiva-modules/archiva-base/archiva-repository-admin/archiva-repository-admin-api/src/main/java/org/apache/archiva/admin/model/EntityExistsException.java b/archiva-modules/archiva-base/archiva-repository-admin/archiva-repository-admin-api/src/main/java/org/apache/archiva/admin/model/EntityExistsException.java
new file mode 100644 (file)
index 0000000..9868427
--- /dev/null
@@ -0,0 +1,78 @@
+package org.apache.archiva.admin.model;/*
+ * 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.
+ */
+
+/*
+ * 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.
+ */
+
+/**
+ * This exception is thrown, if a entity that should be created, exists already.
+ * @author Martin Stockhammer <martin_s@apache.org>
+ * @since 3.0
+ */
+public class EntityExistsException extends RepositoryAdminException
+{
+    private static final String KEY = "entity.exists";
+
+    public static EntityExistsException of(String... parameters) {
+        String message = getMessage( KEY, parameters );
+        return new EntityExistsException( message, parameters );
+    }
+
+    public EntityExistsException( String s, String... parameters )
+    {
+        super( s );
+        setKey( KEY );
+        setParameters( parameters );
+    }
+
+    public EntityExistsException( String s, String fieldName, String... parameters )
+    {
+        super( s, fieldName );
+        setKey( KEY );
+        setParameters( parameters );
+    }
+
+    public EntityExistsException( String message, Throwable cause, String... parameters )
+    {
+        super( message, cause );
+        setKey( KEY );
+        setParameters( parameters );
+    }
+
+    public EntityExistsException( String message, Throwable cause, String fieldName, String... parameters )
+    {
+        super( message, cause, fieldName );
+        setKey( KEY );
+        setParameters( parameters );
+    }
+}
diff --git a/archiva-modules/archiva-base/archiva-repository-admin/archiva-repository-admin-api/src/main/java/org/apache/archiva/admin/model/EntityNotFoundException.java b/archiva-modules/archiva-base/archiva-repository-admin/archiva-repository-admin-api/src/main/java/org/apache/archiva/admin/model/EntityNotFoundException.java
new file mode 100644 (file)
index 0000000..90bf82d
--- /dev/null
@@ -0,0 +1,79 @@
+package org.apache.archiva.admin.model;/*
+ * 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.
+ */
+
+/*
+ * 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.
+ */
+
+/**
+ * This exception is thrown, if a requested entity does not exist.
+ *
+ * @author Martin Stockhammer <martin_s@apache.org>
+ * @since 3.0
+ */
+public class EntityNotFoundException extends RepositoryAdminException
+{
+    public static final String KEY = "entity.not_found";
+
+    public static EntityNotFoundException of(String... parameters) {
+        String message = getMessage( KEY, parameters );
+        return new EntityNotFoundException( message, parameters );
+    }
+
+    public EntityNotFoundException( String s, String... parameters )
+    {
+        super( s );
+        setKey( KEY );
+        setParameters( parameters );
+    }
+
+    public EntityNotFoundException( String s, String fieldName, String... parameters )
+    {
+        super( s, fieldName );
+        setKey( KEY );
+        setParameters( parameters );
+    }
+
+    public EntityNotFoundException( String message, Throwable cause, String... parameters )
+    {
+        super( message, cause );
+        setKey( KEY );
+        setParameters( parameters );
+    }
+
+    public EntityNotFoundException( String message, Throwable cause, String fieldName, String... parameters )
+    {
+        super( message, cause, fieldName );
+        setKey( KEY );
+        setParameters( parameters );
+    }
+}
index 80f77701502d4d4dd1238905b3ed6c50e5b54c35..c684c0e09ed0f635bb8299052fee23b9796ad2e4 100644 (file)
@@ -19,6 +19,12 @@ package org.apache.archiva.admin.model;
  */
 
 
+import org.apache.commons.lang3.StringUtils;
+
+import java.text.MessageFormat;
+import java.util.Locale;
+import java.util.ResourceBundle;
+
 /**
  * @author Olivier Lamy
  * @since 1.4-M1
@@ -27,6 +33,8 @@ public class RepositoryAdminException
     extends Exception
 {
 
+    private static final ResourceBundle bundle = ResourceBundle.getBundle( "org.apache.archiva.admin.model.error.AdminErrors", Locale.ROOT );
+
     /**
      * can return the field name of bean with issue
      * can be <code>null</code>
@@ -34,6 +42,58 @@ public class RepositoryAdminException
      */
     private String fieldName;
 
+    /**
+     * A unique identifier of this error
+     * @since 3.0
+     */
+    private String key;
+    private boolean keyExists = false;
+
+    /**
+     * Message parameters
+     */
+    String[] parameters = new String[0];
+
+
+    public static RepositoryAdminException ofKey(String key, String... params) {
+        String message = getMessage( key, params );
+        RepositoryAdminException ex = new RepositoryAdminException( message );
+        ex.setKey( key );
+        ex.setParameters( params );
+        return ex;
+    }
+
+    protected static String getMessage( String key, String[] params )
+    {
+        return MessageFormat.format( bundle.getString( key ), params );
+    }
+
+    public static RepositoryAdminException ofKey(String key, Throwable cause, String... params) {
+        String message = getMessage( key, params );
+        RepositoryAdminException ex = new RepositoryAdminException( message, cause );
+        ex.setKey( key );
+        ex.setParameters( params );
+        return ex;
+    }
+
+
+    public static RepositoryAdminException ofKeyAndField(String key, String fieldName, String... params) {
+        String message = getMessage( key, params );
+        RepositoryAdminException ex = new RepositoryAdminException( message, fieldName );
+        ex.setKey( key );
+        ex.setParameters( params );
+        return ex;
+    }
+
+    public static RepositoryAdminException ofKeyAndField(String key, Throwable cause, String fieldName, String... params) {
+        String message = getMessage( key, params );
+        RepositoryAdminException ex = new RepositoryAdminException( message, cause, fieldName );
+        ex.setKey( key );
+        ex.setFieldName( fieldName );
+        ex.setParameters( params );
+        return ex;
+    }
+
     public RepositoryAdminException( String s )
     {
         super( s );
@@ -65,4 +125,32 @@ public class RepositoryAdminException
     {
         this.fieldName = fieldName;
     }
+
+    public String getKey( )
+    {
+        return key;
+    }
+
+    public void setKey( String key )
+    {
+        this.keyExists=!StringUtils.isEmpty( key );
+        this.key = key;
+    }
+
+    public boolean keyExists() {
+        return this.keyExists;
+    }
+
+    public String[] getParameters( )
+    {
+        return parameters;
+    }
+
+    public void setParameters( String[] parameters )
+    {
+        if (parameters==null) {
+            this.parameters = new String[0];
+        }
+        this.parameters = parameters;
+    }
 }
index 74bafcd7d1b06db3e9cd2b732054c625ef281c0a..d4c0041b290f5856c5e7395bbd7338f713ab8d47 100644 (file)
@@ -59,6 +59,8 @@ public class RepositoryGroup
      */
     private String cronExpression;
 
+    private String location;
+
     public RepositoryGroup()
     {
         // no op
@@ -184,6 +186,16 @@ public class RepositoryGroup
         return this;
     }
 
+    public String getLocation( )
+    {
+        return location;
+    }
+
+    public void setLocation( String location )
+    {
+        this.location = location;
+    }
+
     @Override
     public boolean equals( Object other )
     {
index e98e8321a66d06088eb3f3d0dadb774473d546c9..e5411fea23547c0e97ca18ad6e6a87255050a4d2 100644 (file)
@@ -19,6 +19,7 @@ package org.apache.archiva.admin.model.group;
  */
 
 import org.apache.archiva.admin.model.AuditInformation;
+import org.apache.archiva.admin.model.EntityNotFoundException;
 import org.apache.archiva.admin.model.RepositoryAdminException;
 import org.apache.archiva.admin.model.beans.RepositoryGroup;
 import org.apache.archiva.repository.storage.StorageAsset;
@@ -35,8 +36,17 @@ public interface RepositoryGroupAdmin
     List<RepositoryGroup> getRepositoriesGroups()
         throws RepositoryAdminException;
 
+    /**
+     * Returns the repository group. If it is not found a {@link org.apache.archiva.admin.model.EntityNotFoundException}
+     * will be thrown.
+     *
+     * @param repositoryGroupId the identifier of the repository group
+     * @return the repository group object
+     * @throws RepositoryAdminException
+     * @throws EntityNotFoundException
+     */
     RepositoryGroup getRepositoryGroup( String repositoryGroupId )
-        throws RepositoryAdminException;
+        throws RepositoryAdminException, EntityNotFoundException;
 
     Boolean addRepositoryGroup( RepositoryGroup repositoryGroup, AuditInformation auditInformation )
         throws RepositoryAdminException;
diff --git a/archiva-modules/archiva-base/archiva-repository-admin/archiva-repository-admin-api/src/main/resources/org/apache/archiva/admin/model/error/AdminErrors.properties b/archiva-modules/archiva-base/archiva-repository-admin/archiva-repository-admin-api/src/main/resources/org/apache/archiva/admin/model/error/AdminErrors.properties
new file mode 100644 (file)
index 0000000..7a9ddf8
--- /dev/null
@@ -0,0 +1,27 @@
+#
+# 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.
+#
+
+entity.exists=The entity {0} exists already
+entity.not_found=The entity {0} was not found 
+repository_group.id.empty=The repository group id was empty
+repository_group.id.max_length=The id "{0}" of the repository group exceeds {1} characters
+repository_group.id.invalid_chars=The repository group id "{0}" contains invalid characters. Only the following are allowed: {1}.
+repository_group.merged_index_ttl.min=Merged Index TTL must be greater than {0}.
+repository_group.repository.not_found=The member repository with id "{0}" does not exist. Cannot be used in a repository group.
+repository_group.registry.add_error=The registry could not add the repository "{0}": {1}
+repository_group.registry.update_error=The registry could not update the repository "{0}": {1}
index 1986c425e4ec7a440c0b4ed12c6956ed725049fe..3b810c7c9971b0f9a6dcb2b9f19b94c7331f8d9c 100644 (file)
@@ -19,6 +19,8 @@ package org.apache.archiva.admin.repository.group;
  */
 
 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.beans.ManagedRepository;
 import org.apache.archiva.admin.model.beans.RepositoryGroup;
@@ -125,8 +127,14 @@ public class DefaultRepositoryGroupAdmin
     }
 
     @Override
-    public RepositoryGroup getRepositoryGroup( String repositoryGroupId ) {
-        return convertRepositoryGroupObject( repositoryRegistry.getRepositoryGroup( repositoryGroupId ) );
+    public RepositoryGroup getRepositoryGroup( String repositoryGroupId ) throws EntityNotFoundException
+    {
+        org.apache.archiva.repository.RepositoryGroup group = repositoryRegistry.getRepositoryGroup( repositoryGroupId );
+        if (group==null) {
+            throw new EntityNotFoundException( "Repository group does not exist" );
+        } else {
+            return convertRepositoryGroupObject( group );
+        }
     }
 
     @Override
@@ -146,7 +154,8 @@ public class DefaultRepositoryGroupAdmin
         try {
             repositoryRegistry.putRepositoryGroup(repositoryGroupConfiguration);
         } catch (RepositoryException e) {
-            e.printStackTrace();
+            log.error( "Could not add the repository group to the registry: {}", e.getMessage( ), e );
+            throw RepositoryAdminException.ofKey( "repository_group.registry.add_error", e, repositoryGroup.getId(), e.getMessage() );
         }
 
         triggerAuditEvent( repositoryGroup.getId(), null, AuditEvent.ADD_REPO_GROUP, auditInformation );
@@ -200,7 +209,8 @@ public class DefaultRepositoryGroupAdmin
         try {
             repositoryRegistry.putRepositoryGroup(repositoryGroupConfiguration);
         } catch (RepositoryException e) {
-            e.printStackTrace();
+            log.error( "Could not update the repository group in the registry: {}", e.getMessage( ), e );
+            throw RepositoryAdminException.ofKey( "repository_group.registry.update_error", e, repositoryGroup.getId(), e.getMessage() );
         }
 
         org.apache.archiva.repository.RepositoryGroup rg = repositoryRegistry.getRepositoryGroup( repositoryGroup.getId( ) );
@@ -349,26 +359,24 @@ public class DefaultRepositoryGroupAdmin
         String repoGroupId = repositoryGroup.getId();
         if ( StringUtils.isBlank( repoGroupId ) )
         {
-            throw new RepositoryAdminException( "repositoryGroup id cannot be empty" );
+            throw RepositoryAdminException.ofKey("repository_group.id.empty" );
         }
 
         if ( repoGroupId.length() > 100 )
         {
-            throw new RepositoryAdminException(
-                "Identifier [" + repoGroupId + "] is over the maximum limit of 100 characters" );
+            throw RepositoryAdminException.ofKey("repository_group.id.max_length",repoGroupId, Integer.toString( 100 ));
 
         }
 
         Matcher matcher = REPO_GROUP_ID_PATTERN.matcher( repoGroupId );
         if ( !matcher.matches() )
         {
-            throw new RepositoryAdminException(
-                "Invalid character(s) found in identifier. Only the following characters are allowed: alphanumeric, '.', '-' and '_'" );
+            throw RepositoryAdminException.ofKey("repository_group.id.invalid_chars","alphanumeric, '.', '-','_'" );
         }
 
         if ( repositoryGroup.getMergedIndexTtl() <= 0 )
         {
-            throw new RepositoryAdminException( "Merged Index TTL must be greater than 0." );
+            throw RepositoryAdminException.ofKey("repository_group.merged_index_ttl.min","0" );
         }
 
         Configuration configuration = getArchivaConfiguration().getConfiguration();
@@ -377,18 +385,18 @@ public class DefaultRepositoryGroupAdmin
         {
             if ( !updateMode )
             {
-                throw new RepositoryAdminException( "Unable to add new repository group with id [" + repoGroupId
+                throw new EntityExistsException( "Unable to add new repository group with id [" + repoGroupId
                                                         + "], that id already exists as a repository group." );
             }
         }
         else if ( configuration.getManagedRepositoriesAsMap().containsKey( repoGroupId ) )
         {
-            throw new RepositoryAdminException( "Unable to add new repository group with id [" + repoGroupId
+            throw new EntityExistsException( "Unable to add new repository group with id [" + repoGroupId
                                                     + "], that id already exists as a managed repository." );
         }
         else if ( configuration.getRemoteRepositoriesAsMap().containsKey( repoGroupId ) )
         {
-            throw new RepositoryAdminException( "Unable to add new repository group with id [" + repoGroupId
+            throw new EntityExistsException( "Unable to add new repository group with id [" + repoGroupId
                                                     + "], that id already exists as a remote repository." );
         }
 
@@ -402,8 +410,7 @@ public class DefaultRepositoryGroupAdmin
         {
             if ( getManagedRepositoryAdmin().getManagedRepository( id ) == null )
             {
-                throw new RepositoryAdminException(
-                    "managedRepository with id " + id + " not exists so cannot be used in a repositoryGroup" );
+                throw RepositoryAdminException.ofKey("repository_group.repository.not_found",id );
             }
         }
     }
@@ -427,6 +434,7 @@ public class DefaultRepositoryGroupAdmin
         }
         rg.setCronExpression( group.getSchedulingDefinition() );
         rg.setMergedIndexTtl( group.getMergedIndexTTL() );
+        rg.setLocation( group.getLocation().toString() );
         return rg;
     }
 }
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 (file)
index 0000000..c5e60da
--- /dev/null
@@ -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 (file)
index 0000000..d550f27
--- /dev/null
@@ -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 (file)
index 0000000..7796d1c
--- /dev/null
@@ -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 (file)
index 0000000..0cd02b0
--- /dev/null
@@ -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 (file)
index 0000000..1855ea8
--- /dev/null
@@ -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;
+    }
+}
index 83fec93619cd5aa6ff0608c1e617d2741f23897b..9f65161c7ac8a8c9555f4fb98b08d1fa14d1239c 100644 (file)
@@ -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";
+
 }