From e7ea9e686e0c03155476cdc5fbc390ca26e99bf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Fri, 29 Jun 2018 09:34:31 +0200 Subject: [PATCH 01/38] Clean up exceptions - Remove declared IOException - Fix repetition of exception messages --- .../src/main/java/sonia/scm/HandlerBase.java | 6 +-- scm-core/src/main/java/sonia/scm/Manager.java | 4 +- .../main/java/sonia/scm/ManagerDecorator.java | 9 ++-- .../group/GroupAlreadyExistsException.java | 6 +-- .../scm/group/GroupNotFoundException.java | 36 +------------- .../AbstractSimpleRepositoryHandler.java | 32 ++++++------ .../scm/security/SyncingRealmHelper.java | 9 +--- .../scm/user/UserAlreadyExistsException.java | 6 +-- .../sonia/scm/user/UserNotFoundException.java | 36 +------------- .../resources/RepositoryImportResource.java | 49 +++++++------------ .../rest/resources/RepositoryResource.java | 4 +- .../sonia/scm/group/DefaultGroupManager.java | 23 +++++---- .../repository/DefaultRepositoryManager.java | 19 +++---- .../LastModifiedUpdateListener.java | 6 +-- .../sonia/scm/user/DefaultUserManager.java | 16 +++--- 15 files changed, 86 insertions(+), 175 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/HandlerBase.java b/scm-core/src/main/java/sonia/scm/HandlerBase.java index 0baea8d929..8e6b12ba79 100644 --- a/scm-core/src/main/java/sonia/scm/HandlerBase.java +++ b/scm-core/src/main/java/sonia/scm/HandlerBase.java @@ -59,7 +59,7 @@ public interface HandlerBase * @throws E * @throws IOException */ - public void create(T object) throws E, IOException; + public void create(T object) throws E; /** * Removes a persistent object. @@ -70,7 +70,7 @@ public interface HandlerBase * @throws E * @throws IOException */ - public void delete(T object) throws E, IOException; + public void delete(T object) throws E; /** * Modifies a persistent object. @@ -81,5 +81,5 @@ public interface HandlerBase * @throws E * @throws IOException */ - public void modify(T object) throws E, IOException; + public void modify(T object) throws E; } diff --git a/scm-core/src/main/java/sonia/scm/Manager.java b/scm-core/src/main/java/sonia/scm/Manager.java index 79155b56a2..c0d074520a 100644 --- a/scm-core/src/main/java/sonia/scm/Manager.java +++ b/scm-core/src/main/java/sonia/scm/Manager.java @@ -33,7 +33,6 @@ package sonia.scm; -import java.io.IOException; import java.util.Collection; import java.util.Comparator; @@ -56,9 +55,8 @@ public interface Manager * @param object to refresh * * @throws E - * @throws IOException */ - void refresh(T object) throws E, IOException; + void refresh(T object) throws E; //~--- get methods ---------------------------------------------------------- diff --git a/scm-core/src/main/java/sonia/scm/ManagerDecorator.java b/scm-core/src/main/java/sonia/scm/ManagerDecorator.java index af0215202c..99707c4c4d 100644 --- a/scm-core/src/main/java/sonia/scm/ManagerDecorator.java +++ b/scm-core/src/main/java/sonia/scm/ManagerDecorator.java @@ -35,7 +35,6 @@ package sonia.scm; //~--- JDK imports ------------------------------------------------------------ import java.io.IOException; - import java.util.Collection; import java.util.Comparator; @@ -78,7 +77,7 @@ public class ManagerDecorator * {@inheritDoc} */ @Override - public void create(T object) throws E, IOException + public void create(T object) throws E { decorated.create(object); } @@ -87,7 +86,7 @@ public class ManagerDecorator * {@inheritDoc} */ @Override - public void delete(T object) throws E, IOException + public void delete(T object) throws E { decorated.delete(object); } @@ -105,7 +104,7 @@ public class ManagerDecorator * {@inheritDoc} */ @Override - public void modify(T object) throws E, IOException + public void modify(T object) throws E { decorated.modify(object); } @@ -114,7 +113,7 @@ public class ManagerDecorator * {@inheritDoc} */ @Override - public void refresh(T object) throws E, IOException + public void refresh(T object) throws E { decorated.refresh(object); } diff --git a/scm-core/src/main/java/sonia/scm/group/GroupAlreadyExistsException.java b/scm-core/src/main/java/sonia/scm/group/GroupAlreadyExistsException.java index 8389900098..e63ee1d78b 100644 --- a/scm-core/src/main/java/sonia/scm/group/GroupAlreadyExistsException.java +++ b/scm-core/src/main/java/sonia/scm/group/GroupAlreadyExistsException.java @@ -48,9 +48,9 @@ public class GroupAlreadyExistsException extends GroupException /** * Constructs a new instance. * - * @param message exception message + * @param name The name (aka id) of the group */ - public GroupAlreadyExistsException(String message) { - super(message); + public GroupAlreadyExistsException(String name) { + super(name + " group already exists"); } } diff --git a/scm-core/src/main/java/sonia/scm/group/GroupNotFoundException.java b/scm-core/src/main/java/sonia/scm/group/GroupNotFoundException.java index f4b9934128..d4c06a1559 100644 --- a/scm-core/src/main/java/sonia/scm/group/GroupNotFoundException.java +++ b/scm-core/src/main/java/sonia/scm/group/GroupNotFoundException.java @@ -52,39 +52,7 @@ public class GroupNotFoundException extends GroupException * Constructs a new GroupNotFoundException. * */ - public GroupNotFoundException() {} - - /** - * Constructs a new GroupNotFoundException. - * - * - * @param message message for the exception - */ - public GroupNotFoundException(String message) - { - super(message); - } - - /** - * Constructs a new GroupNotFoundException. - * - * - * @param throwable root cause - */ - public GroupNotFoundException(Throwable throwable) - { - super(throwable); - } - - /** - * Constructs a new GroupNotFoundException. - * - * - * @param message message for the exception - * @param throwable root cause - */ - public GroupNotFoundException(String message, Throwable throwable) - { - super(message, throwable); + public GroupNotFoundException() { + super("group does not exists"); } } diff --git a/scm-core/src/main/java/sonia/scm/repository/AbstractSimpleRepositoryHandler.java b/scm-core/src/main/java/sonia/scm/repository/AbstractSimpleRepositoryHandler.java index 670cab93e1..957142bae5 100644 --- a/scm-core/src/main/java/sonia/scm/repository/AbstractSimpleRepositoryHandler.java +++ b/scm-core/src/main/java/sonia/scm/repository/AbstractSimpleRepositoryHandler.java @@ -38,23 +38,20 @@ package sonia.scm.repository; import com.google.common.base.Charsets; import com.google.common.base.Throwables; import com.google.common.io.Resources; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.ConfigurationException; import sonia.scm.io.CommandResult; import sonia.scm.io.ExtendedCommand; import sonia.scm.io.FileSystem; +import sonia.scm.store.ConfigurationStoreFactory; import sonia.scm.util.IOUtil; -//~--- JDK imports ------------------------------------------------------------ - import java.io.File; import java.io.IOException; - import java.net.URL; -import sonia.scm.store.ConfigurationStoreFactory; + +//~--- JDK imports ------------------------------------------------------------ /** * @@ -109,7 +106,7 @@ public abstract class AbstractSimpleRepositoryHandler Date: Tue, 3 Jul 2018 07:56:01 +0200 Subject: [PATCH 02/38] Start bugfix for illegal sortBy parameter detection From 0aa23268186e82139970020115ab326875a1bbac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Mon, 2 Jul 2018 20:11:53 +0200 Subject: [PATCH 03/38] Verify sortBy parameter before application --- .../resources/AbstractManagerResource.java | 85 ++++++++----------- .../scm/api/rest/resources/GroupResource.java | 11 +-- .../rest/resources/RepositoryResource.java | 2 +- .../scm/api/rest/resources/UserResource.java | 12 +-- .../v2/resources/GroupCollectionResource.java | 16 +++- .../scm/api/v2/resources/GroupResource.java | 2 +- .../v2/resources/ResourceManagerAdapter.java | 4 +- .../v2/resources/UserCollectionResource.java | 16 +++- .../scm/api/v2/resources/UserResource.java | 2 +- 9 files changed, 73 insertions(+), 77 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/AbstractManagerResource.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/AbstractManagerResource.java index 7599e569eb..10d65a16be 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/AbstractManagerResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/AbstractManagerResource.java @@ -55,6 +55,11 @@ import javax.ws.rs.core.Request; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.UriInfo; +import java.beans.BeanInfo; +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.util.Arrays; import java.util.Collection; import java.util.Comparator; import java.util.Date; @@ -76,17 +81,15 @@ public abstract class AbstractManagerResource manager; + private final Class type; - /** - * Constructs ... - * - * - * @param manager - */ - public AbstractManagerResource(Manager manager) - { + protected int cacheMaxAge = 0; + protected boolean disableCache = false; + + public AbstractManagerResource(Manager manager, Class type) { this.manager = manager; + this.type = type; } //~--- methods -------------------------------------------------------------- @@ -526,45 +529,25 @@ public abstract class AbstractManagerResource createComparator(String sortby, boolean desc) + private Comparator createComparator(String sortBy, boolean desc) { + checkSortByField(sortBy); Comparator comparator; if (desc) { - comparator = new BeanReverseComparator(sortby); + comparator = new BeanReverseComparator(sortBy); } else { - comparator = new BeanComparator(sortby); + comparator = new BeanComparator(sortBy); } return comparator; } - /** - * Method description - * - * - * - * @param sortby - * @param desc - * @param start - * @param limit - * - * @return - */ - private Collection fetchItems(String sortby, boolean desc, int start, + private Collection fetchItems(String sortBy, boolean desc, int start, int limit) { AssertUtil.assertPositive(start); @@ -573,18 +556,18 @@ public abstract class AbstractManagerResource 0) { - if (Util.isEmpty(sortby)) + if (Util.isEmpty(sortBy)) { // replace with something useful - sortby = "id"; + sortBy = "id"; } - items = manager.getAll(createComparator(sortby, desc), start, limit); + items = manager.getAll(createComparator(sortBy, desc), start, limit); } - else if (Util.isNotEmpty(sortby)) + else if (Util.isNotEmpty(sortBy)) { - items = manager.getAll(createComparator(sortby, desc)); + items = manager.getAll(createComparator(sortBy, desc)); } else { @@ -594,6 +577,18 @@ public abstract class AbstractManagerResource p.getName().equals(sortBy))) { + throw new IllegalArgumentException("sortBy"); + } + } catch (IntrospectionException e) { + throw new RuntimeException("error introspecting model type " + type.getName(), e); + } + } + protected PageResult fetchPage(String sortby, boolean desc, int pageNumber, int pageSize) { AssertUtil.assertPositive(pageNumber); @@ -676,16 +671,4 @@ public abstract class AbstractManagerResource manager; } diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/GroupResource.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/GroupResource.java index 364a1b200e..b9701f7e38 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/GroupResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/GroupResource.java @@ -41,18 +41,12 @@ import com.webcohesion.enunciate.metadata.rs.ResponseCode; import com.webcohesion.enunciate.metadata.rs.ResponseHeader; import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.TypeHint; - import org.apache.shiro.SecurityUtils; - import sonia.scm.group.Group; import sonia.scm.group.GroupException; import sonia.scm.group.GroupManager; import sonia.scm.security.Role; -//~--- JDK imports ------------------------------------------------------------ - -import java.util.Collection; - import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.DefaultValue; @@ -69,6 +63,9 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Request; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; +import java.util.Collection; + +//~--- JDK imports ------------------------------------------------------------ /** * RESTful Web Service Resource to manage groups and their members. @@ -97,7 +94,7 @@ public class GroupResource @Inject public GroupResource(GroupManager groupManager) { - super(groupManager); + super(groupManager, Group.class); } //~--- methods -------------------------------------------------------------- diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryResource.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryResource.java index 0637f3fc8f..1b48f66503 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryResource.java @@ -131,7 +131,7 @@ public class RepositoryResource extends AbstractManagerResource @Inject public UserResource(UserManager userManager, PasswordService passwordService) { - super(userManager); + super(userManager, User.class); this.passwordService = passwordService; } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupCollectionResource.java index 2d777c7487..4a9db5662c 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupCollectionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupCollectionResource.java @@ -1,13 +1,23 @@ package sonia.scm.api.v2.resources; -import com.webcohesion.enunciate.metadata.rs.*; +import com.webcohesion.enunciate.metadata.rs.ResponseCode; +import com.webcohesion.enunciate.metadata.rs.ResponseHeader; +import com.webcohesion.enunciate.metadata.rs.ResponseHeaders; +import com.webcohesion.enunciate.metadata.rs.StatusCodes; +import com.webcohesion.enunciate.metadata.rs.TypeHint; import sonia.scm.group.Group; import sonia.scm.group.GroupException; import sonia.scm.group.GroupManager; import sonia.scm.web.VndMediaType; import javax.inject.Inject; -import javax.ws.rs.*; +import javax.ws.rs.Consumes; +import javax.ws.rs.DefaultValue; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; import javax.ws.rs.core.Response; import java.io.IOException; @@ -26,7 +36,7 @@ public class GroupCollectionResource { this.dtoToGroupMapper = dtoToGroupMapper; this.groupCollectionToDtoMapper = groupCollectionToDtoMapper; this.resourceLinks = resourceLinks; - this.adapter = new ResourceManagerAdapter<>(manager); + this.adapter = new ResourceManagerAdapter<>(manager, Group.class); } /** diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupResource.java index 3c1f2aeb64..62bdaad38f 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupResource.java @@ -29,7 +29,7 @@ public class GroupResource { GroupDtoToGroupMapper groupDtoToGroupMapper) { this.groupToGroupDtoMapper = groupToGroupDtoMapper; this.dtoToGroupMapper = groupDtoToGroupMapper; - this.adapter = new ResourceManagerAdapter<>(manager); + this.adapter = new ResourceManagerAdapter<>(manager, Group.class); } /** diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceManagerAdapter.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceManagerAdapter.java index c2aab1a058..bc35942900 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceManagerAdapter.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceManagerAdapter.java @@ -30,8 +30,8 @@ class ResourceManagerAdapter extends AbstractManagerResource { - ResourceManagerAdapter(Manager manager) { - super(manager); + ResourceManagerAdapter(Manager manager, Class type) { + super(manager, type); } /** diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserCollectionResource.java index 4b88ea48bf..008fc08143 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserCollectionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserCollectionResource.java @@ -1,13 +1,23 @@ package sonia.scm.api.v2.resources; -import com.webcohesion.enunciate.metadata.rs.*; +import com.webcohesion.enunciate.metadata.rs.ResponseCode; +import com.webcohesion.enunciate.metadata.rs.ResponseHeader; +import com.webcohesion.enunciate.metadata.rs.ResponseHeaders; +import com.webcohesion.enunciate.metadata.rs.StatusCodes; +import com.webcohesion.enunciate.metadata.rs.TypeHint; import sonia.scm.user.User; import sonia.scm.user.UserException; import sonia.scm.user.UserManager; import sonia.scm.web.VndMediaType; import javax.inject.Inject; -import javax.ws.rs.*; +import javax.ws.rs.Consumes; +import javax.ws.rs.DefaultValue; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; import javax.ws.rs.core.Response; import java.io.IOException; @@ -25,7 +35,7 @@ public class UserCollectionResource { UserCollectionToDtoMapper userCollectionToDtoMapper, ResourceLinks resourceLinks) { this.dtoToUserMapper = dtoToUserMapper; this.userCollectionToDtoMapper = userCollectionToDtoMapper; - this.adapter = new ResourceManagerAdapter<>(manager); + this.adapter = new ResourceManagerAdapter<>(manager, User.class); this.resourceLinks = resourceLinks; } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserResource.java index bf747bab4d..7805ba3440 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserResource.java @@ -29,7 +29,7 @@ public class UserResource { public UserResource(UserDtoToUserMapper dtoToUserMapper, UserToUserDtoMapper userToDtoMapper, UserManager manager) { this.dtoToUserMapper = dtoToUserMapper; this.userToDtoMapper = userToDtoMapper; - this.adapter = new ResourceManagerAdapter<>(manager); + this.adapter = new ResourceManagerAdapter<>(manager, User.class); } /** From 0768b638edeaf7497458df1a1be034125eeed9b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 3 Jul 2018 12:39:01 +0200 Subject: [PATCH 04/38] Bootstrap v2 version to get repositories --- .../java/sonia/scm/repository/Repository.java | 12 ++-- .../RepositoryManagerDecorator.java | 19 ++----- .../main/java/sonia/scm/web/VndMediaType.java | 1 + .../rest/resources/RepositoryResource.java | 12 +--- .../scm/api/v2/resources/BaseMapper.java | 2 +- .../v2/resources/HealthCheckFailureDto.java | 11 ++++ .../scm/api/v2/resources/MapperModule.java | 2 + .../scm/api/v2/resources/PermissionDto.java | 11 ++++ .../scm/api/v2/resources/RepositoryDto.java | 29 ++++++++++ .../api/v2/resources/RepositoryResource.java | 44 +++++++++++++++ .../v2/resources/RepositoryRootResource.java | 25 +++++++++ .../RepositoryToRepositoryDtoMapper.java | 20 +++++++ .../RepositoryToRepositoryDtoMapperTest.java | 56 +++++++++++++++++++ 13 files changed, 213 insertions(+), 31 deletions(-) create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/HealthCheckFailureDto.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDto.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRootResource.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java create mode 100644 scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java diff --git a/scm-core/src/main/java/sonia/scm/repository/Repository.java b/scm-core/src/main/java/sonia/scm/repository/Repository.java index 63bef4ea6c..0b4d1c4dcd 100644 --- a/scm-core/src/main/java/sonia/scm/repository/Repository.java +++ b/scm-core/src/main/java/sonia/scm/repository/Repository.java @@ -37,22 +37,20 @@ import com.github.sdorra.ssp.PermissionObject; import com.github.sdorra.ssp.StaticPermissions; import com.google.common.base.Objects; import com.google.common.collect.Lists; - import sonia.scm.BasicPropertiesAware; import sonia.scm.ModelObject; import sonia.scm.util.HttpUtil; import sonia.scm.util.Util; import sonia.scm.util.ValidationUtil; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlElementWrapper; import javax.xml.bind.annotation.XmlRootElement; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; /** * Source code repository. @@ -207,6 +205,10 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per return name; } + public String getNamespace() { + return namespace; + } + /** * Returns the access permissions of the {@link Repository}. * diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryManagerDecorator.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryManagerDecorator.java index 632b54b741..d99a6483ab 100644 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryManagerDecorator.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryManagerDecorator.java @@ -38,13 +38,11 @@ package sonia.scm.repository; import sonia.scm.ManagerDecorator; import sonia.scm.Type; -//~--- JDK imports ------------------------------------------------------------ - +import javax.servlet.http.HttpServletRequest; import java.io.IOException; - import java.util.Collection; -import javax.servlet.http.HttpServletRequest; +//~--- JDK imports ------------------------------------------------------------ /** * Decorator for {@link RepositoryManager}. @@ -92,19 +90,10 @@ public class RepositoryManagerDecorator //~--- get methods ---------------------------------------------------------- - /** - * {@inheritDoc} - * - * - * @param type - * @param name - * - * @return - */ @Override - public Repository get(String type, String name) + public Repository get(String namespace, String name) { - return decorated.get(type, name); + return decorated.get(namespace, name); } /** diff --git a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java index 3ec121f9a4..f306114ecb 100644 --- a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java +++ b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java @@ -14,6 +14,7 @@ public class VndMediaType { public static final String USER = PREFIX + "user" + SUFFIX; public static final String GROUP = PREFIX + "group" + SUFFIX; + public static final String REPOSITORY = PREFIX + "repository" + SUFFIX; public static final String USER_COLLECTION = PREFIX + "userCollection" + SUFFIX; public static final String GROUP_COLLECTION = PREFIX + "groupCollection" + SUFFIX; diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryResource.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryResource.java index 0637f3fc8f..740f841f18 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryResource.java @@ -46,7 +46,6 @@ import org.apache.shiro.SecurityUtils; import org.apache.shiro.authz.AuthorizationException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sonia.scm.config.ScmConfiguration; import sonia.scm.repository.BlameResult; import sonia.scm.repository.Branches; import sonia.scm.repository.BrowserResult; @@ -120,19 +119,15 @@ public class RepositoryResource extends AbstractManagerResource { @Mapping(target = "attributes", ignore = true) // We do not map HAL attributes - public abstract D map(T user); + public abstract D map(T modelObject); Instant mapTime(Long epochMilli) { return epochMilli == null? null: Instant.ofEpochMilli(epochMilli); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/HealthCheckFailureDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/HealthCheckFailureDto.java new file mode 100644 index 0000000000..21d98c257c --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/HealthCheckFailureDto.java @@ -0,0 +1,11 @@ +package sonia.scm.api.v2.resources; + +import lombok.Getter; +import lombok.Setter; + +@Getter @Setter +public class HealthCheckFailureDto { + private String description; + private String summary; + private String url; +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java index fec2a677b5..0a0468de3c 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java @@ -15,6 +15,8 @@ public class MapperModule extends AbstractModule { bind(GroupToGroupDtoMapper.class).to(Mappers.getMapper(GroupToGroupDtoMapper.class).getClass()); bind(GroupCollectionToDtoMapper.class); + bind(RepositoryToRepositoryDtoMapper.class).to(Mappers.getMapper(RepositoryToRepositoryDtoMapper.class).getClass()); + bind(UriInfoStore.class).in(ServletScopes.REQUEST); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDto.java new file mode 100644 index 0000000000..293a16dd17 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDto.java @@ -0,0 +1,11 @@ +package sonia.scm.api.v2.resources; + +import lombok.Getter; +import lombok.Setter; + +@Getter @Setter +public class PermissionDto { + private String type; + private String name; + private boolean groupPermission; +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java new file mode 100644 index 0000000000..6afac172df --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java @@ -0,0 +1,29 @@ +package sonia.scm.api.v2.resources; + +import com.fasterxml.jackson.annotation.JsonInclude; +import de.otto.edison.hal.HalRepresentation; +import lombok.Getter; +import lombok.Setter; + +import javax.xml.bind.annotation.XmlElement; +import java.time.Instant; +import java.util.List; + +@Getter @Setter +public class RepositoryDto extends HalRepresentation { + + private String id; + private String contact; + private Instant creationDate; + private String description; + private List healthCheckFailures; + @JsonInclude(JsonInclude.Include.NON_NULL) + private Instant lastModified; + private String namespace; + private String name; + private List permissions; + @XmlElement(name = "public") + private boolean publicReadable = false; + private boolean archived = false; + private String type; +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java new file mode 100644 index 0000000000..c171dc5d83 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java @@ -0,0 +1,44 @@ +package sonia.scm.api.v2.resources; + +import com.webcohesion.enunciate.metadata.rs.ResponseCode; +import com.webcohesion.enunciate.metadata.rs.StatusCodes; +import com.webcohesion.enunciate.metadata.rs.TypeHint; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryException; +import sonia.scm.repository.RepositoryManager; +import sonia.scm.web.VndMediaType; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Response; + +public class RepositoryResource { + + private final RepositoryToRepositoryDtoMapper repositoryToDtoMapper; + + private final ResourceManagerAdapter adapter; + + @Inject + public RepositoryResource(RepositoryToRepositoryDtoMapper repositoryToDtoMapper, RepositoryManager manager) { + this.repositoryToDtoMapper = repositoryToDtoMapper; + this.adapter = new ResourceManagerAdapter<>(manager); + } + + @GET + @Path("") + @Produces(VndMediaType.REPOSITORY) + @TypeHint(RepositoryDto.class) + @StatusCodes({ + @ResponseCode(code = 200, condition = "success"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user has no privileges to read the repository"), + @ResponseCode(code = 404, condition = "not found, no repository with the specified name available in the namespace"), + @ResponseCode(code = 500, condition = "internal server error") + }) + public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name) { + return adapter.get("31QwjAKOK2", repositoryToDtoMapper::map); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRootResource.java new file mode 100644 index 0000000000..e7861a884b --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRootResource.java @@ -0,0 +1,25 @@ +package sonia.scm.api.v2.resources; + +import javax.inject.Inject; +import javax.inject.Provider; +import javax.ws.rs.Path; + +/** + * RESTful Web Service Resource to manage repositories. + */ +@Path(RepositoryRootResource.REPOSITORIES_PATH_V2) +public class RepositoryRootResource { + static final String REPOSITORIES_PATH_V2 = "v2/repositories/"; + + private final Provider repositoryResource; + + @Inject + public RepositoryRootResource(Provider repositoryResource) { + this.repositoryResource = repositoryResource; + } + + @Path("{namespace}/{name}") + public RepositoryResource getRepositoryResource() { + return repositoryResource.get(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java new file mode 100644 index 0000000000..0574fa9dd6 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java @@ -0,0 +1,20 @@ +package sonia.scm.api.v2.resources; + +import com.google.inject.Inject; +import org.mapstruct.Mapper; +import sonia.scm.repository.HealthCheckFailure; +import sonia.scm.repository.Permission; +import sonia.scm.repository.Repository; + +// Mapstruct does not support parameterized (i.e. non-default) constructors. Thus, we need to use field injection. +@SuppressWarnings("squid:S3306") +@Mapper +public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper { + + @Inject + private ResourceLinks resourceLinks; + + abstract HealthCheckFailureDto toDto(HealthCheckFailure failure); + + abstract PermissionDto toDto(Permission permission); +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java new file mode 100644 index 0000000000..39eb8418a6 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java @@ -0,0 +1,56 @@ +package sonia.scm.api.v2.resources; + +import org.junit.Test; +import sonia.scm.repository.HealthCheckFailure; +import sonia.scm.repository.Permission; +import sonia.scm.repository.PermissionType; +import sonia.scm.repository.Repository; + +import static java.util.Arrays.asList; +import static org.junit.Assert.assertEquals; + +public class RepositoryToRepositoryDtoMapperTest { + + private RepositoryToRepositoryDtoMapperImpl mapper = new RepositoryToRepositoryDtoMapperImpl(); + + @Test + public void shouldMapSimpleProperties() { + RepositoryDto dto = mapper.map(createDummyRepository()); + assertEquals("namespace", dto.getNamespace()); + assertEquals("name", dto.getName()); + assertEquals("description", dto.getDescription()); + assertEquals("git", dto.getType()); + assertEquals("none@example.com", dto.getContact()); + assertEquals("1", dto.getId()); + } + + @Test + public void shouldMapHealthCheck() { + RepositoryDto dto = mapper.map(createDummyRepository()); + assertEquals(1, dto.getHealthCheckFailures().size()); + assertEquals("summary", dto.getHealthCheckFailures().get(0).getSummary()); + } + + @Test + public void shouldMapPermissions() { + RepositoryDto dto = mapper.map(createDummyRepository()); + assertEquals(1, dto.getPermissions().size()); + assertEquals("permission", dto.getPermissions().get(0).getName()); + assertEquals("READ", dto.getPermissions().get(0).getType()); + } + + private Repository createDummyRepository() { + Repository repository = new Repository(); + repository.setNamespace("namespace"); + repository.setName("name"); + repository.setDescription("description"); + repository.setType("git"); + repository.setContact("none@example.com"); + repository.setId("1"); + repository.setCreationDate(System.currentTimeMillis()); + repository.setHealthCheckFailures(asList(new HealthCheckFailure("1", "summary", "url", "failure"))); + repository.setPermissions(asList(new Permission("permission", PermissionType.READ))); + + return repository; + } +} From 1bcc35d48bff712395b4e5a4f95411b53815ce03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 3 Jul 2018 13:11:18 +0200 Subject: [PATCH 05/38] Support reading object by other identifiers than id Therefore split adapter class for single entity and collection handling. --- .../scm/repository/RepositoryManager.java | 14 +++- ... => CollectionResourceManagerAdapter.java} | 33 +------- .../v2/resources/GroupCollectionResource.java | 18 ++++- .../scm/api/v2/resources/GroupResource.java | 4 +- .../resources/IdResourceManagerAdapter.java | 51 ++++++++++++ .../api/v2/resources/RepositoryResource.java | 8 +- .../SingleResourceManagerAdapter.java | 77 +++++++++++++++++++ .../v2/resources/UserCollectionResource.java | 18 ++++- .../scm/api/v2/resources/UserResource.java | 4 +- .../resources/RepositoryRootResourceTest.java | 77 +++++++++++++++++++ 10 files changed, 254 insertions(+), 50 deletions(-) rename scm-webapp/src/main/java/sonia/scm/api/v2/resources/{ResourceManagerAdapter.java => CollectionResourceManagerAdapter.java} (65%) create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/IdResourceManagerAdapter.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java create mode 100644 scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryManager.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryManager.java index 7cbac2b52e..ca1d5ab743 100644 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryManager.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryManager.java @@ -38,13 +38,11 @@ package sonia.scm.repository; import sonia.scm.Type; import sonia.scm.TypeManager; -//~--- JDK imports ------------------------------------------------------------ - +import javax.servlet.http.HttpServletRequest; import java.io.IOException; - import java.util.Collection; -import javax.servlet.http.HttpServletRequest; +//~--- JDK imports ------------------------------------------------------------ /** * The central class for managing {@link Repository} objects. @@ -149,4 +147,12 @@ public interface RepositoryManager */ @Override public RepositoryHandler getHandler(String type); + + default Repository getByNamespace(String namespace, String name) { + return getAll() + .stream() + .filter(r -> r.getName().equals(name) && r.getNamespace().equals(namespace)) + .findFirst() + .orElse(null); + } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceManagerAdapter.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/CollectionResourceManagerAdapter.java similarity index 65% rename from scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceManagerAdapter.java rename to scm-webapp/src/main/java/sonia/scm/api/v2/resources/CollectionResourceManagerAdapter.java index c2aab1a058..925198756a 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceManagerAdapter.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/CollectionResourceManagerAdapter.java @@ -26,43 +26,14 @@ import static javax.ws.rs.core.Response.Status.BAD_REQUEST; * @param The exception type for the model object, eg. {@link sonia.scm.user.UserException}. */ @SuppressWarnings("squid:S00119") // "MODEL_OBJECT" is much more meaningful than "M", right? -class ResourceManagerAdapter extends AbstractManagerResource { - ResourceManagerAdapter(Manager manager) { + CollectionResourceManagerAdapter(Manager manager) { super(manager); } - /** - * Reads the model object for the given id, transforms it to a dto and returns a corresponding http response. - * This handles all corner cases, eg. no matching object for the id or missing privileges. - */ - Response get(String id, Function mapToDto) { - MODEL_OBJECT modelObject = manager.get(id); - if (modelObject == null) { - return Response.status(Response.Status.NOT_FOUND).build(); - } - DTO dto = mapToDto.apply(modelObject); - return Response.ok(dto).build(); - } - - /** - * Update the model object for the given id according to the given function and returns a corresponding http response. - * This handles all corner cases, eg. no matching object for the id or missing privileges. - */ - public Response update(String id, Function applyChanges) { - MODEL_OBJECT existingModelObject = manager.get(id); - if (existingModelObject == null) { - return Response.status(Response.Status.NOT_FOUND).build(); - } - MODEL_OBJECT changedModelObject = applyChanges.apply(existingModelObject); - if (!id.equals(changedModelObject.getId())) { - return Response.status(BAD_REQUEST).entity("illegal change of id").build(); - } - return update(id, changedModelObject); - } - /** * Reads all model objects in a paged way, maps them using the given function and returns a corresponding http response. * This handles all corner cases, eg. missing privileges. diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupCollectionResource.java index 2d777c7487..195efc56ae 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupCollectionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupCollectionResource.java @@ -1,13 +1,23 @@ package sonia.scm.api.v2.resources; -import com.webcohesion.enunciate.metadata.rs.*; +import com.webcohesion.enunciate.metadata.rs.ResponseCode; +import com.webcohesion.enunciate.metadata.rs.ResponseHeader; +import com.webcohesion.enunciate.metadata.rs.ResponseHeaders; +import com.webcohesion.enunciate.metadata.rs.StatusCodes; +import com.webcohesion.enunciate.metadata.rs.TypeHint; import sonia.scm.group.Group; import sonia.scm.group.GroupException; import sonia.scm.group.GroupManager; import sonia.scm.web.VndMediaType; import javax.inject.Inject; -import javax.ws.rs.*; +import javax.ws.rs.Consumes; +import javax.ws.rs.DefaultValue; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; import javax.ws.rs.core.Response; import java.io.IOException; @@ -19,14 +29,14 @@ public class GroupCollectionResource { private final GroupCollectionToDtoMapper groupCollectionToDtoMapper; private final ResourceLinks resourceLinks; - private final ResourceManagerAdapter adapter; + private final IdResourceManagerAdapter adapter; @Inject public GroupCollectionResource(GroupManager manager, GroupDtoToGroupMapper dtoToGroupMapper, GroupCollectionToDtoMapper groupCollectionToDtoMapper, ResourceLinks resourceLinks) { this.dtoToGroupMapper = dtoToGroupMapper; this.groupCollectionToDtoMapper = groupCollectionToDtoMapper; this.resourceLinks = resourceLinks; - this.adapter = new ResourceManagerAdapter<>(manager); + this.adapter = new IdResourceManagerAdapter<>(manager); } /** diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupResource.java index 3c1f2aeb64..7a490b6d7a 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupResource.java @@ -22,14 +22,14 @@ public class GroupResource { private final GroupToGroupDtoMapper groupToGroupDtoMapper; private final GroupDtoToGroupMapper dtoToGroupMapper; - private final ResourceManagerAdapter adapter; + private final IdResourceManagerAdapter adapter; @Inject public GroupResource(GroupManager manager, GroupToGroupDtoMapper groupToGroupDtoMapper, GroupDtoToGroupMapper groupDtoToGroupMapper) { this.groupToGroupDtoMapper = groupToGroupDtoMapper; this.dtoToGroupMapper = groupDtoToGroupMapper; - this.adapter = new ResourceManagerAdapter<>(manager); + this.adapter = new IdResourceManagerAdapter<>(manager); } /** diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IdResourceManagerAdapter.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IdResourceManagerAdapter.java new file mode 100644 index 0000000000..c7bcc3b6b1 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IdResourceManagerAdapter.java @@ -0,0 +1,51 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.HalRepresentation; +import sonia.scm.Manager; +import sonia.scm.ModelObject; +import sonia.scm.PageResult; + +import javax.ws.rs.core.Response; +import java.io.IOException; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * Facade for {@link SingleResourceManagerAdapter} and {@link CollectionResourceManagerAdapter}. + */ +@SuppressWarnings("squid:S00119") // "MODEL_OBJECT" is much more meaningful than "M", right? +class IdResourceManagerAdapter { + + private final Manager manager; + + private final SingleResourceManagerAdapter singleAdapter; + private final CollectionResourceManagerAdapter collectionAdapter; + + IdResourceManagerAdapter(Manager manager) { + this.manager = manager; + singleAdapter = new SingleResourceManagerAdapter<>(manager); + collectionAdapter = new CollectionResourceManagerAdapter<>(manager); + } + + Response get(String id, Function mapToDto) { + return singleAdapter.get(() -> manager.get(id), mapToDto); + } + + public Response update(String id, Function applyChanges) { + return singleAdapter.update(() -> manager.get(id), applyChanges); + } + + public Response getAll(int page, int pageSize, String sortBy, boolean desc, Function, CollectionDto> mapToDto) { + return collectionAdapter.getAll(page, pageSize, sortBy, desc, mapToDto); + } + + public Response create(DTO dto, Supplier modelObjectSupplier, Function uriCreator) throws IOException, EXCEPTION { + return collectionAdapter.create(dto, modelObjectSupplier, uriCreator); + } + + public Response delete(String id) { + return singleAdapter.delete(id); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java index c171dc5d83..c3cb349529 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java @@ -19,12 +19,14 @@ public class RepositoryResource { private final RepositoryToRepositoryDtoMapper repositoryToDtoMapper; - private final ResourceManagerAdapter adapter; + private final RepositoryManager manager; + private final SingleResourceManagerAdapter adapter; @Inject public RepositoryResource(RepositoryToRepositoryDtoMapper repositoryToDtoMapper, RepositoryManager manager) { + this.manager = manager; this.repositoryToDtoMapper = repositoryToDtoMapper; - this.adapter = new ResourceManagerAdapter<>(manager); + this.adapter = new SingleResourceManagerAdapter<>(manager); } @GET @@ -39,6 +41,6 @@ public class RepositoryResource { @ResponseCode(code = 500, condition = "internal server error") }) public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name) { - return adapter.get("31QwjAKOK2", repositoryToDtoMapper::map); + return adapter.get(() -> manager.getByNamespace(namespace, name), repositoryToDtoMapper::map); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java new file mode 100644 index 0000000000..a56111d893 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java @@ -0,0 +1,77 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.HalRepresentation; +import sonia.scm.Manager; +import sonia.scm.ModelObject; +import sonia.scm.api.rest.resources.AbstractManagerResource; + +import javax.ws.rs.core.GenericEntity; +import javax.ws.rs.core.Response; +import java.util.Collection; +import java.util.function.Function; +import java.util.function.Supplier; + +import static javax.ws.rs.core.Response.Status.BAD_REQUEST; + +/** + * Adapter from resource http endpoints to managers. + * + * Provides common CRUD operations and DTO to Model Object mapping to keep Resources more DRY. + * + * @param The type of the model object, eg. {@link sonia.scm.user.User}. + * @param The corresponding transport object, eg. {@link UserDto}. + * @param The exception type for the model object, eg. {@link sonia.scm.user.UserException}. + */ +@SuppressWarnings("squid:S00119") // "MODEL_OBJECT" is much more meaningful than "M", right? +class SingleResourceManagerAdapter extends AbstractManagerResource { + + SingleResourceManagerAdapter(Manager manager) { + super(manager); + } + + /** + * Reads the model object for the given id, transforms it to a dto and returns a corresponding http response. + * This handles all corner cases, eg. no matching object for the id or missing privileges. + */ + Response get(Supplier reader, Function mapToDto) { + MODEL_OBJECT modelObject = reader.get(); + if (modelObject == null) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + DTO dto = mapToDto.apply(modelObject); + return Response.ok(dto).build(); + } + + /** + * Update the model object for the given id according to the given function and returns a corresponding http response. + * This handles all corner cases, eg. no matching object for the id or missing privileges. + */ + public Response update(Supplier reader, Function applyChanges) { + MODEL_OBJECT existingModelObject = reader.get(); + if (existingModelObject == null) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + MODEL_OBJECT changedModelObject = applyChanges.apply(existingModelObject); + if (!getId(existingModelObject).equals(getId(changedModelObject))) { + return Response.status(BAD_REQUEST).entity("illegal change of id").build(); + } + return update(getId(existingModelObject), changedModelObject); + } + + @Override + protected GenericEntity> createGenericEntity(Collection modelObjects) { + throw new UnsupportedOperationException(); + } + + @Override + protected String getId(MODEL_OBJECT item) { + return item.getId(); + } + + @Override + protected String getPathPart() { + throw new UnsupportedOperationException(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserCollectionResource.java index 4b88ea48bf..8daddd67c1 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserCollectionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserCollectionResource.java @@ -1,13 +1,23 @@ package sonia.scm.api.v2.resources; -import com.webcohesion.enunciate.metadata.rs.*; +import com.webcohesion.enunciate.metadata.rs.ResponseCode; +import com.webcohesion.enunciate.metadata.rs.ResponseHeader; +import com.webcohesion.enunciate.metadata.rs.ResponseHeaders; +import com.webcohesion.enunciate.metadata.rs.StatusCodes; +import com.webcohesion.enunciate.metadata.rs.TypeHint; import sonia.scm.user.User; import sonia.scm.user.UserException; import sonia.scm.user.UserManager; import sonia.scm.web.VndMediaType; import javax.inject.Inject; -import javax.ws.rs.*; +import javax.ws.rs.Consumes; +import javax.ws.rs.DefaultValue; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; import javax.ws.rs.core.Response; import java.io.IOException; @@ -18,14 +28,14 @@ public class UserCollectionResource { private final UserCollectionToDtoMapper userCollectionToDtoMapper; private final ResourceLinks resourceLinks; - private final ResourceManagerAdapter adapter; + private final IdResourceManagerAdapter adapter; @Inject public UserCollectionResource(UserManager manager, UserDtoToUserMapper dtoToUserMapper, UserCollectionToDtoMapper userCollectionToDtoMapper, ResourceLinks resourceLinks) { this.dtoToUserMapper = dtoToUserMapper; this.userCollectionToDtoMapper = userCollectionToDtoMapper; - this.adapter = new ResourceManagerAdapter<>(manager); + this.adapter = new IdResourceManagerAdapter<>(manager); this.resourceLinks = resourceLinks; } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserResource.java index bf747bab4d..2cfcdfe772 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserResource.java @@ -23,13 +23,13 @@ public class UserResource { private final UserDtoToUserMapper dtoToUserMapper; private final UserToUserDtoMapper userToDtoMapper; - private final ResourceManagerAdapter adapter; + private final IdResourceManagerAdapter adapter; @Inject public UserResource(UserDtoToUserMapper dtoToUserMapper, UserToUserDtoMapper userToDtoMapper, UserManager manager) { this.dtoToUserMapper = dtoToUserMapper; this.userToDtoMapper = userToDtoMapper; - this.adapter = new ResourceManagerAdapter<>(manager); + this.adapter = new IdResourceManagerAdapter<>(manager); } /** diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java new file mode 100644 index 0000000000..74de87eb67 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java @@ -0,0 +1,77 @@ +package sonia.scm.api.v2.resources; + +import org.jboss.resteasy.core.Dispatcher; +import org.jboss.resteasy.mock.MockDispatcherFactory; +import org.jboss.resteasy.mock.MockHttpRequest; +import org.jboss.resteasy.mock.MockHttpResponse; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Answers; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryManager; + +import java.net.URI; +import java.net.URISyntaxException; + +import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; +import static javax.servlet.http.HttpServletResponse.SC_OK; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.initMocks; + +public class RepositoryRootResourceTest { + + private final Dispatcher dispatcher = MockDispatcherFactory.createDispatcher(); + + @Mock + private RepositoryManager repositoryManager; + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private ResourceLinks resourceLinks; + @InjectMocks + private RepositoryToRepositoryDtoMapperImpl repositoryToDtoMapper; + + @Before + public void prepareEnvironment() { + initMocks(this); + ResourceLinksMock.initMock(resourceLinks, URI.create("/")); + RepositoryResource repositoryResource = new RepositoryResource(repositoryToDtoMapper, repositoryManager); + RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider.of(repositoryResource)); + dispatcher.getRegistry().addSingletonResource(repositoryRootResource); + } + + @Test + public void shouldFailForNotExistingRepository() throws URISyntaxException { + mockRepository("space", "repo"); + + MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/other"); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(SC_NOT_FOUND, response.getStatus()); + } + + @Test + public void shouldFindExistingRepository() throws URISyntaxException { + mockRepository("space", "repo"); + + MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo"); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(SC_OK, response.getStatus()); + assertTrue(response.getContentAsString().contains("\"name\":\"repo\"")); + } + + private Repository mockRepository(String namespace, String name) { + Repository repository = new Repository(); + repository.setNamespace(namespace); + repository.setName(name); + when(repositoryManager.getByNamespace(namespace, name)).thenReturn(repository); + return repository; + } +} From 150838be85d17022614248e872edeedd258129a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 3 Jul 2018 14:29:48 +0200 Subject: [PATCH 06/38] Generate repository links in dto --- .../scm/api/v2/resources/RepositoryDto.java | 7 ++ .../RepositoryToRepositoryDtoMapper.java | 19 +++++ .../scm/api/v2/resources/ResourceLinks.java | 24 ++++++ .../resources/RepositoryRootResourceTest.java | 12 +++ .../RepositoryToRepositoryDtoMapperTest.java | 80 ++++++++++++++++--- .../api/v2/resources/ResourceLinksMock.java | 4 + 6 files changed, 137 insertions(+), 9 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java index 6afac172df..14083837bf 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java @@ -2,6 +2,7 @@ package sonia.scm.api.v2.resources; import com.fasterxml.jackson.annotation.JsonInclude; import de.otto.edison.hal.HalRepresentation; +import de.otto.edison.hal.Links; import lombok.Getter; import lombok.Setter; @@ -26,4 +27,10 @@ public class RepositoryDto extends HalRepresentation { private boolean publicReadable = false; private boolean archived = false; private String type; + + @Override + @SuppressWarnings("squid:S1185") // We want to have this method available in this package + protected HalRepresentation add(Links links) { + return super.add(links); + } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java index 0574fa9dd6..e2c246675d 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java @@ -1,10 +1,17 @@ package sonia.scm.api.v2.resources; import com.google.inject.Inject; +import de.otto.edison.hal.Links; +import org.mapstruct.AfterMapping; import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; import sonia.scm.repository.HealthCheckFailure; import sonia.scm.repository.Permission; import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryPermissions; + +import static de.otto.edison.hal.Link.link; +import static de.otto.edison.hal.Links.linkingTo; // Mapstruct does not support parameterized (i.e. non-default) constructors. Thus, we need to use field injection. @SuppressWarnings("squid:S3306") @@ -17,4 +24,16 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper baseUri + GROUPS_PATH_V2); when(resourceLinks.groupCollection().create()).thenAnswer(invocation -> baseUri + GROUPS_PATH_V2); + + when(resourceLinks.repository().self(anyString(), anyString())).thenAnswer(invocation -> baseUri + GROUPS_PATH_V2 + invocation.getArguments()[0] + "/" + invocation.getArguments()[1]); + when(resourceLinks.repository().update(anyString(), anyString())).thenAnswer(invocation -> baseUri + GROUPS_PATH_V2 + invocation.getArguments()[0] + "/" + invocation.getArguments()[1]); + when(resourceLinks.repository().delete(anyString(), anyString())).thenAnswer(invocation -> baseUri + GROUPS_PATH_V2 + invocation.getArguments()[0] + "/" + invocation.getArguments()[1]); } } From fb811ef725f438195d4b78a0a41ae8c18d5f3f31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 3 Jul 2018 16:17:51 +0200 Subject: [PATCH 07/38] Correct resource links and add tags link --- .../api/v2/resources/RepositoryResource.java | 24 ++++++++++++++++++- .../RepositoryToRepositoryDtoMapper.java | 1 + .../scm/api/v2/resources/ResourceLinks.java | 18 +++++++++++++- .../v2/resources/TagCollectionResource.java | 18 ++++++++++++++ .../scm/api/v2/resources/TagRootResource.java | 20 ++++++++++++++++ .../resources/RepositoryRootResourceTest.java | 2 +- .../RepositoryToRepositoryDtoMapperTest.java | 8 +++++++ .../api/v2/resources/ResourceLinksMock.java | 2 ++ .../api/v2/resources/ResourceLinksTest.java | 24 +++++++++++++++++++ 9 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/TagCollectionResource.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/TagRootResource.java diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java index c3cb349529..3340c34df5 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java @@ -9,7 +9,10 @@ import sonia.scm.repository.RepositoryManager; import sonia.scm.web.VndMediaType; import javax.inject.Inject; +import javax.inject.Provider; +import javax.ws.rs.DELETE; import javax.ws.rs.GET; +import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; @@ -21,12 +24,14 @@ public class RepositoryResource { private final RepositoryManager manager; private final SingleResourceManagerAdapter adapter; + private final Provider tagRootResource; @Inject - public RepositoryResource(RepositoryToRepositoryDtoMapper repositoryToDtoMapper, RepositoryManager manager) { + public RepositoryResource(RepositoryToRepositoryDtoMapper repositoryToDtoMapper, RepositoryManager manager, Provider tagRootResource) { this.manager = manager; this.repositoryToDtoMapper = repositoryToDtoMapper; this.adapter = new SingleResourceManagerAdapter<>(manager); + this.tagRootResource = tagRootResource; } @GET @@ -43,4 +48,21 @@ public class RepositoryResource { public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name) { return adapter.get(() -> manager.getByNamespace(namespace, name), repositoryToDtoMapper::map); } + + @DELETE + @Path("") + public Response delete(@PathParam("namespace") String namespace, @PathParam("name") String name) { + throw new UnsupportedOperationException(); + } + + @PUT + @Path("") + public Response update(@PathParam("namespace") String namespace, @PathParam("name") String name) { + throw new UnsupportedOperationException(); + } + + @Path("tags/") + public TagRootResource tags() { + return tagRootResource.get(); + } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java index e2c246675d..278afd3916 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java @@ -34,6 +34,7 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper tagCollectionResource; + + @Inject + public TagRootResource(Provider tagCollectionResource) { + this.tagCollectionResource = tagCollectionResource; + } + + @Path("") + public TagCollectionResource getTagCollectionResource() { + return tagCollectionResource.get(); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java index 36ed2415dc..decc7ccd3d 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java @@ -48,7 +48,7 @@ public class RepositoryRootResourceTest { public void prepareEnvironment() { initMocks(this); ResourceLinksMock.initMock(resourceLinks, URI.create("/")); - RepositoryResource repositoryResource = new RepositoryResource(repositoryToDtoMapper, repositoryManager); + RepositoryResource repositoryResource = new RepositoryResource(repositoryToDtoMapper, repositoryManager, null); RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider.of(repositoryResource)); dispatcher.getRegistry().addSingletonResource(repositoryRootResource); } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java index 809fd5b3e5..ce879b7229 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java @@ -101,6 +101,14 @@ public class RepositoryToRepositoryDtoMapperTest { assertEquals("READ", dto.getPermissions().get(0).getType()); } + @Test + public void shouldCreateTagsLink() { + RepositoryDto dto = mapper.map(createTestRepository()); + assertEquals( + "http://example.com/base/v2/groups/testspace/test/tags/", + dto.getLinks().getLinkBy("tags").get().getHref()); + } + private Repository createTestRepository() { Repository repository = new Repository(); repository.setNamespace("testspace"); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java index 465f6e0769..18da197b17 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java @@ -26,5 +26,7 @@ public class ResourceLinksMock { when(resourceLinks.repository().self(anyString(), anyString())).thenAnswer(invocation -> baseUri + GROUPS_PATH_V2 + invocation.getArguments()[0] + "/" + invocation.getArguments()[1]); when(resourceLinks.repository().update(anyString(), anyString())).thenAnswer(invocation -> baseUri + GROUPS_PATH_V2 + invocation.getArguments()[0] + "/" + invocation.getArguments()[1]); when(resourceLinks.repository().delete(anyString(), anyString())).thenAnswer(invocation -> baseUri + GROUPS_PATH_V2 + invocation.getArguments()[0] + "/" + invocation.getArguments()[1]); + + when(resourceLinks.tagCollection().self(anyString(), anyString())).thenAnswer(invocation -> baseUri + GROUPS_PATH_V2 + invocation.getArguments()[0] + "/" + invocation.getArguments()[1] + "/tags/"); } } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksTest.java index 103eebd411..272084cd97 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksTest.java @@ -84,6 +84,30 @@ public class ResourceLinksTest { assertEquals(BASE_URL + GroupRootResource.GROUPS_PATH_V2, url); } + @Test + public void shouldCreateCorrectRepositorySelfUrl() { + String url = resourceLinks.repository().self("space", "repo"); + assertEquals(BASE_URL + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo", url); + } + + @Test + public void shouldCreateCorrectRepositoryDeleteUrl() { + String url = resourceLinks.repository().delete("space", "repo"); + assertEquals(BASE_URL + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo", url); + } + + @Test + public void shouldCreateCorrectRepositoryUpdateUrl() { + String url = resourceLinks.repository().update("space", "repo"); + assertEquals(BASE_URL + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo", url); + } + + @Test + public void shouldCreateCorrectTagCollectionUrl() { + String url = resourceLinks.tagCollection().self("space", "repo"); + assertEquals(BASE_URL + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/tags/", url); + } + @Before public void initUriInfo() { initMocks(this); From 4a49068b4afb9138372ca5c56dadef32832505f1 Mon Sep 17 00:00:00 2001 From: Johannes Schnatterer Date: Tue, 3 Jul 2018 16:18:44 +0200 Subject: [PATCH 08/38] Review - Adds reason for RuntimeException & extends REST docs --- .../api/rest/resources/AbstractManagerResource.java | 11 +++++++---- .../scm/api/v2/resources/GroupCollectionResource.java | 8 +++++--- .../scm/api/v2/resources/UserCollectionResource.java | 6 ++++-- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/AbstractManagerResource.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/AbstractManagerResource.java index 10d65a16be..b8a3dfc45d 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/AbstractManagerResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/AbstractManagerResource.java @@ -577,6 +577,9 @@ public abstract class AbstractManagerResource fetchPage(String sortby, boolean desc, int pageNumber, + protected PageResult fetchPage(String sortBy, boolean desc, int pageNumber, int pageSize) { AssertUtil.assertPositive(pageNumber); AssertUtil.assertPositive(pageSize); - if (Util.isEmpty(sortby)) { + if (Util.isEmpty(sortBy)) { // replace with something useful - sortby = "id"; + sortBy = "id"; } - return manager.getPage(createComparator(sortby, desc), pageNumber, pageSize); + return manager.getPage(createComparator(sortBy, desc), pageNumber, pageSize); } //~--- get methods ---------------------------------------------------------- diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupCollectionResource.java index 4a9db5662c..912d890fe8 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupCollectionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupCollectionResource.java @@ -41,11 +41,12 @@ public class GroupCollectionResource { /** * Returns all groups for a given page number with a given page size (default page size is {@value DEFAULT_PAGE_SIZE}). - * + * * Note: This method requires "group" privilege. - * @param page the number of the requested page + * + * @param page the number of the requested page * @param pageSize the page size (default page size is {@value DEFAULT_PAGE_SIZE}) - * @param sortBy sort parameter + * @param sortBy sort parameter (if empty - undefined sorting) * @param desc sort direction desc or aesc */ @GET @@ -54,6 +55,7 @@ public class GroupCollectionResource { @TypeHint(GroupDto[].class) @StatusCodes({ @ResponseCode(code = 200, condition = "success"), + @ResponseCode(code = 400, condition = "\"sortBy\" field unknown"), @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"group\" privilege"), @ResponseCode(code = 500, condition = "internal server error") diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserCollectionResource.java index 008fc08143..c269cd9f90 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserCollectionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserCollectionResource.java @@ -43,9 +43,10 @@ public class UserCollectionResource { * Returns all users for a given page number with a given page size (default page size is {@value DEFAULT_PAGE_SIZE}). * * Note: This method requires "user" privilege. - * @param page the number of the requested page + * + * @param page the number of the requested page * @param pageSize the page size (default page size is {@value DEFAULT_PAGE_SIZE}) - * @param sortBy sort parameter + * @param sortBy sort parameter (if empty - undefined sorting) * @param desc sort direction desc or asc */ @GET @@ -54,6 +55,7 @@ public class UserCollectionResource { @TypeHint(UserDto[].class) @StatusCodes({ @ResponseCode(code = 200, condition = "success"), + @ResponseCode(code = 400, condition = "\"sortBy\" field unknown"), @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"user\" privilege"), @ResponseCode(code = 500, condition = "internal server error") From a1d567799a8008d4a57eb171f0ca5fda772d37a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 3 Jul 2018 16:51:46 +0200 Subject: [PATCH 09/38] Test sortBy parameter check --- .../AbstractManagerResourceTest.java | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 scm-webapp/src/test/java/sonia/scm/api/rest/resources/AbstractManagerResourceTest.java diff --git a/scm-webapp/src/test/java/sonia/scm/api/rest/resources/AbstractManagerResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/rest/resources/AbstractManagerResourceTest.java new file mode 100644 index 0000000000..581cd6df03 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/rest/resources/AbstractManagerResourceTest.java @@ -0,0 +1,114 @@ +package sonia.scm.api.rest.resources; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import sonia.scm.Manager; +import sonia.scm.ModelObject; + +import javax.ws.rs.core.GenericEntity; +import javax.ws.rs.core.Request; +import java.util.Collection; +import java.util.Comparator; + +import static java.util.Collections.emptyList; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentCaptor.forClass; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class AbstractManagerResourceTest { + + private final Manager manager = mock(Manager.class); + private final Request request = mock(Request.class); + private final ArgumentCaptor comparatorCaptor = forClass(Comparator.class); + + private final AbstractManagerResource abstractManagerResource = new SimpleManagerResource(); + + @Test + public void shouldAcceptDefaultSortByParameter() { + abstractManagerResource.getAll(request, 0, 1, null, true); + + Comparator comparator = comparatorCaptor.getValue(); + assertTrue(comparator.compare(new Simple("1", null), new Simple("2", null)) > 0); + } + + @Test + public void shouldAcceptValidSortByParameter() { + abstractManagerResource.getAll(request, 0, 1, "data", true); + + Comparator comparator = comparatorCaptor.getValue(); + assertTrue(comparator.compare(new Simple("", "1"), new Simple("", "2")) > 0); + } + + @Test(expected = IllegalArgumentException.class) + public void shouldFailForIllegalSortByParameter() { + abstractManagerResource.getAll(request, 0, 1, "x", true); + } + + @Before + public void captureComparator() { + when(manager.getAll(comparatorCaptor.capture(), eq(0), eq(1))).thenReturn(emptyList()); + } + + private class SimpleManagerResource extends AbstractManagerResource { + + { + disableCache = true; + } + + public SimpleManagerResource() { + super(AbstractManagerResourceTest.this.manager, Simple.class); + } + + @Override + protected GenericEntity> createGenericEntity(Collection items) { + return null; + } + + @Override + protected String getId(Simple item) { + return null; + } + + @Override + protected String getPathPart() { + return null; + } + } + + public static class Simple implements ModelObject { + + private String id; + private String data; + + public Simple(String id, String data) { + this.id = id; + this.data = data; + } + + public String getData() { + return data; + } + + @Override + public String getId() { + return id; + } + + @Override + public Long getLastModified() { + return null; + } + + @Override + public String getType() { + return null; + } + @Override + public boolean isValid() { + return false; + } + } +} From 7c662ed42a9f5dcbef3da0e494ca2a1b0119e7b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 4 Jul 2018 08:37:57 +0200 Subject: [PATCH 10/38] Generate link to branches for repository --- .../resources/BranchCollectionResource.java | 18 +++++++++++++++ .../api/v2/resources/BranchRootResource.java | 20 +++++++++++++++++ .../api/v2/resources/RepositoryResource.java | 13 ++++++++++- .../RepositoryToRepositoryDtoMapper.java | 1 + .../scm/api/v2/resources/ResourceLinks.java | 22 ++++++++++++++++--- .../resources/RepositoryRootResourceTest.java | 2 +- .../RepositoryToRepositoryDtoMapperTest.java | 8 +++++++ .../api/v2/resources/ResourceLinksMock.java | 2 ++ .../api/v2/resources/ResourceLinksTest.java | 6 +++++ .../DefaultRepositoryManagerTest.java | 5 +++++ 10 files changed, 92 insertions(+), 5 deletions(-) create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchCollectionResource.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRootResource.java diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchCollectionResource.java new file mode 100644 index 0000000000..d64016b475 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchCollectionResource.java @@ -0,0 +1,18 @@ +package sonia.scm.api.v2.resources; + +import javax.ws.rs.DefaultValue; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Response; + +public class BranchCollectionResource { + @GET + @Path("") + public Response getAll(@DefaultValue("0") @QueryParam("page") int page, + @DefaultValue("10") @QueryParam("pageSize") int pageSize, + @QueryParam("sortBy") String sortBy, + @DefaultValue("false") @QueryParam("desc") boolean desc) { + throw new UnsupportedOperationException(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRootResource.java new file mode 100644 index 0000000000..ee50fdcd1e --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRootResource.java @@ -0,0 +1,20 @@ +package sonia.scm.api.v2.resources; + +import javax.inject.Inject; +import javax.inject.Provider; +import javax.ws.rs.Path; + +public class BranchRootResource { + + private final Provider branchCollectionResource; + + @Inject + public BranchRootResource(Provider branchCollectionResource) { + this.branchCollectionResource = branchCollectionResource; + } + + @Path("") + public BranchCollectionResource getBranchCollectionResource() { + return branchCollectionResource.get(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java index 3340c34df5..e2bcd60e88 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java @@ -25,13 +25,19 @@ public class RepositoryResource { private final RepositoryManager manager; private final SingleResourceManagerAdapter adapter; private final Provider tagRootResource; + private final Provider branchRootResource; @Inject - public RepositoryResource(RepositoryToRepositoryDtoMapper repositoryToDtoMapper, RepositoryManager manager, Provider tagRootResource) { + public RepositoryResource( + RepositoryToRepositoryDtoMapper repositoryToDtoMapper, + RepositoryManager manager, + Provider tagRootResource, + Provider branchRootResource) { this.manager = manager; this.repositoryToDtoMapper = repositoryToDtoMapper; this.adapter = new SingleResourceManagerAdapter<>(manager); this.tagRootResource = tagRootResource; + this.branchRootResource = branchRootResource; } @GET @@ -65,4 +71,9 @@ public class RepositoryResource { public TagRootResource tags() { return tagRootResource.get(); } + + @Path("branches/") + public BranchRootResource branches() { + return branchRootResource.get(); + } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java index 278afd3916..6121ac05b2 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java @@ -35,6 +35,7 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper baseUri + GROUPS_PATH_V2 + invocation.getArguments()[0] + "/" + invocation.getArguments()[1]); when(resourceLinks.tagCollection().self(anyString(), anyString())).thenAnswer(invocation -> baseUri + GROUPS_PATH_V2 + invocation.getArguments()[0] + "/" + invocation.getArguments()[1] + "/tags/"); + + when(resourceLinks.branchCollection().self(anyString(), anyString())).thenAnswer(invocation -> baseUri + GROUPS_PATH_V2 + invocation.getArguments()[0] + "/" + invocation.getArguments()[1] + "/branches/"); } } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksTest.java index 272084cd97..de8e962948 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksTest.java @@ -108,6 +108,12 @@ public class ResourceLinksTest { assertEquals(BASE_URL + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/tags/", url); } + @Test + public void shouldCreateCorrectBranchCollectionUrl() { + String url = resourceLinks.branchCollection().self("space", "repo"); + assertEquals(BASE_URL + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/branches/", url); + } + @Before public void initUriInfo() { initMocks(this); diff --git a/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java index a66bf222cb..0371b4974b 100644 --- a/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java @@ -37,6 +37,7 @@ import com.github.sdorra.shiro.ShiroRule; import com.github.sdorra.shiro.SubjectAware; import com.google.common.collect.ImmutableSet; import org.apache.shiro.authz.UnauthorizedException; +import org.apache.shiro.util.ThreadContext; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; @@ -92,6 +93,10 @@ import static org.mockito.Mockito.when; ) public class DefaultRepositoryManagerTest extends ManagerTestBase { + { + ThreadContext.unbindSecurityManager(); + } + @Rule public ShiroRule shiro = new ShiroRule(); From 6043b093da0630353319395e1bdfc6f3d8adea9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 4 Jul 2018 08:55:36 +0200 Subject: [PATCH 11/38] Simplify mocking of resource links --- .../scm/api/v2/resources/ResourceLinks.java | 14 ++++---- .../v2/resources/GroupRootResourceTest.java | 3 +- .../resources/GroupToGroupDtoMapperTest.java | 3 +- .../resources/RepositoryRootResourceTest.java | 3 +- .../RepositoryToRepositoryDtoMapperTest.java | 13 ++++---- .../api/v2/resources/ResourceLinksMock.java | 33 +++++++------------ .../UserCollectionToDtoMapperTest.java | 3 +- .../v2/resources/UserRootResourceTest.java | 3 +- .../v2/resources/UserToUserDtoMapperTest.java | 3 +- 9 files changed, 30 insertions(+), 48 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java index 98b94fdd82..b905783d1d 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java @@ -20,7 +20,7 @@ class ResourceLinks { static class GroupLinks { private final LinkBuilder groupLinkBuilder; - private GroupLinks(UriInfo uriInfo) { + GroupLinks(UriInfo uriInfo) { groupLinkBuilder = new LinkBuilder(uriInfo, GroupRootResource.class, GroupResource.class); } @@ -44,7 +44,7 @@ class ResourceLinks { static class GroupCollectionLinks { private final LinkBuilder collectionLinkBuilder; - private GroupCollectionLinks(UriInfo uriInfo) { + GroupCollectionLinks(UriInfo uriInfo) { collectionLinkBuilder = new LinkBuilder(uriInfo, GroupRootResource.class, GroupCollectionResource.class); } @@ -64,7 +64,7 @@ class ResourceLinks { static class UserLinks { private final LinkBuilder userLinkBuilder; - private UserLinks(UriInfo uriInfo) { + UserLinks(UriInfo uriInfo) { userLinkBuilder = new LinkBuilder(uriInfo, UserRootResource.class, UserResource.class); } @@ -88,7 +88,7 @@ class ResourceLinks { static class UserCollectionLinks { private final LinkBuilder collectionLinkBuilder; - private UserCollectionLinks(UriInfo uriInfo) { + UserCollectionLinks(UriInfo uriInfo) { collectionLinkBuilder = new LinkBuilder(uriInfo, UserRootResource.class, UserCollectionResource.class); } @@ -108,7 +108,7 @@ class ResourceLinks { static class RepositoryLinks { private final LinkBuilder repositoryLinkBuilder; - private RepositoryLinks(UriInfo uriInfo) { + RepositoryLinks(UriInfo uriInfo) { repositoryLinkBuilder = new LinkBuilder(uriInfo, RepositoryRootResource.class, RepositoryResource.class); } @@ -132,7 +132,7 @@ class ResourceLinks { static class TagCollectionLinks { private final LinkBuilder tagLinkBuilder; - private TagCollectionLinks(UriInfo uriInfo) { + TagCollectionLinks(UriInfo uriInfo) { tagLinkBuilder = new LinkBuilder(uriInfo, RepositoryRootResource.class, RepositoryResource.class, TagRootResource.class, TagCollectionResource.class); } @@ -148,7 +148,7 @@ class ResourceLinks { static class BranchCollectionLinks { private final LinkBuilder branchLinkBuilder; - private BranchCollectionLinks(UriInfo uriInfo) { + BranchCollectionLinks(UriInfo uriInfo) { branchLinkBuilder = new LinkBuilder(uriInfo, RepositoryRootResource.class, RepositoryResource.class, BranchRootResource.class, BranchCollectionResource.class); } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupRootResourceTest.java index 7f88ceb50f..cec210de1b 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupRootResourceTest.java @@ -10,7 +10,6 @@ import org.jboss.resteasy.mock.MockHttpResponse; import org.junit.Before; import org.junit.Rule; import org.junit.Test; -import org.mockito.Answers; import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; @@ -49,7 +48,7 @@ public class GroupRootResourceTest { private Dispatcher dispatcher = MockDispatcherFactory.createDispatcher(); - @Mock(answer = Answers.RETURNS_DEEP_STUBS) + @Mock private ResourceLinks resourceLinks; @Mock diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupToGroupDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupToGroupDtoMapperTest.java index 1db089cd0b..221733486e 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupToGroupDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupToGroupDtoMapperTest.java @@ -7,7 +7,6 @@ import org.apache.shiro.util.ThreadState; import org.junit.After; import org.junit.Before; import org.junit.Test; -import org.mockito.Answers; import org.mockito.InjectMocks; import org.mockito.Mock; import sonia.scm.group.Group; @@ -24,7 +23,7 @@ import static org.mockito.MockitoAnnotations.initMocks; public class GroupToGroupDtoMapperTest { - @Mock(answer = Answers.RETURNS_DEEP_STUBS) + @Mock private ResourceLinks resourceLinks; @InjectMocks diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java index 659ada8e89..023bb71dc2 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java @@ -9,7 +9,6 @@ import org.jboss.resteasy.mock.MockHttpResponse; import org.junit.Before; import org.junit.Rule; import org.junit.Test; -import org.mockito.Answers; import org.mockito.InjectMocks; import org.mockito.Mock; import sonia.scm.repository.Repository; @@ -39,7 +38,7 @@ public class RepositoryRootResourceTest { @Mock private RepositoryManager repositoryManager; - @Mock(answer = Answers.RETURNS_DEEP_STUBS) + @Mock private ResourceLinks resourceLinks; @InjectMocks private RepositoryToRepositoryDtoMapperImpl repositoryToDtoMapper; diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java index b4396a78bf..67f903e6de 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java @@ -6,7 +6,6 @@ import org.apache.shiro.util.ThreadContext; import org.apache.shiro.util.ThreadState; import org.junit.Before; import org.junit.Test; -import org.mockito.Answers; import org.mockito.InjectMocks; import org.mockito.Mock; import sonia.scm.repository.HealthCheckFailure; @@ -26,7 +25,7 @@ import static org.mockito.MockitoAnnotations.initMocks; public class RepositoryToRepositoryDtoMapperTest { - @Mock(answer = Answers.RETURNS_DEEP_STUBS) + @Mock private ResourceLinks resourceLinks; @InjectMocks @@ -62,7 +61,7 @@ public class RepositoryToRepositoryDtoMapperTest { public void shouldCreateLinksForUnprivilegedUser() { RepositoryDto dto = mapper.map(createTestRepository()); assertEquals( - "http://example.com/base/v2/groups/testspace/test", + "http://example.com/base/v2/repositories/testspace/test", dto.getLinks().getLinkBy("self").get().getHref()); assertFalse(dto.getLinks().getLinkBy("update").isPresent()); assertFalse(dto.getLinks().getLinkBy("delete").isPresent()); @@ -73,7 +72,7 @@ public class RepositoryToRepositoryDtoMapperTest { when(subject.isPermitted("repository:delete:1")).thenReturn(true); RepositoryDto dto = mapper.map(createTestRepository()); assertEquals( - "http://example.com/base/v2/groups/testspace/test", + "http://example.com/base/v2/repositories/testspace/test", dto.getLinks().getLinkBy("delete").get().getHref()); } @@ -82,7 +81,7 @@ public class RepositoryToRepositoryDtoMapperTest { when(subject.isPermitted("repository:modify:1")).thenReturn(true); RepositoryDto dto = mapper.map(createTestRepository()); assertEquals( - "http://example.com/base/v2/groups/testspace/test", + "http://example.com/base/v2/repositories/testspace/test", dto.getLinks().getLinkBy("update").get().getHref()); } @@ -105,7 +104,7 @@ public class RepositoryToRepositoryDtoMapperTest { public void shouldCreateTagsLink() { RepositoryDto dto = mapper.map(createTestRepository()); assertEquals( - "http://example.com/base/v2/groups/testspace/test/tags/", + "http://example.com/base/v2/repositories/testspace/test/tags/", dto.getLinks().getLinkBy("tags").get().getHref()); } @@ -113,7 +112,7 @@ public class RepositoryToRepositoryDtoMapperTest { public void shouldCreateBranchesLink() { RepositoryDto dto = mapper.map(createTestRepository()); assertEquals( - "http://example.com/base/v2/groups/testspace/test/branches/", + "http://example.com/base/v2/repositories/testspace/test/branches/", dto.getLinks().getLinkBy("branches").get().getHref()); } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java index e5ee289149..8913a6d9c4 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java @@ -1,34 +1,23 @@ package sonia.scm.api.v2.resources; +import javax.ws.rs.core.UriInfo; import java.net.URI; -import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import static sonia.scm.api.v2.resources.GroupRootResource.GROUPS_PATH_V2; -import static sonia.scm.api.v2.resources.UserRootResource.USERS_PATH_V2; public class ResourceLinksMock { public static void initMock(ResourceLinks resourceLinks, URI baseUri) { - when(resourceLinks.user().self(anyString())).thenAnswer(invocation -> baseUri + USERS_PATH_V2 + invocation.getArguments()[0]); - when(resourceLinks.user().update(anyString())).thenAnswer(invocation -> baseUri + USERS_PATH_V2 + invocation.getArguments()[0]); - when(resourceLinks.user().delete(anyString())).thenAnswer(invocation -> baseUri + USERS_PATH_V2 + invocation.getArguments()[0]); - when(resourceLinks.userCollection().self()).thenAnswer(invocation -> baseUri + USERS_PATH_V2); - when(resourceLinks.userCollection().create()).thenAnswer(invocation -> baseUri + USERS_PATH_V2); + UriInfo uriInfo = mock(UriInfo.class); + when(uriInfo.getBaseUri()).thenReturn(baseUri); - when(resourceLinks.group().self(anyString())).thenAnswer(invocation -> baseUri + GROUPS_PATH_V2 + invocation.getArguments()[0]); - when(resourceLinks.group().update(anyString())).thenAnswer(invocation -> baseUri + GROUPS_PATH_V2 + invocation.getArguments()[0]); - when(resourceLinks.group().delete(anyString())).thenAnswer(invocation -> baseUri + GROUPS_PATH_V2 + invocation.getArguments()[0]); - - when(resourceLinks.groupCollection().self()).thenAnswer(invocation -> baseUri + GROUPS_PATH_V2); - when(resourceLinks.groupCollection().create()).thenAnswer(invocation -> baseUri + GROUPS_PATH_V2); - - when(resourceLinks.repository().self(anyString(), anyString())).thenAnswer(invocation -> baseUri + GROUPS_PATH_V2 + invocation.getArguments()[0] + "/" + invocation.getArguments()[1]); - when(resourceLinks.repository().update(anyString(), anyString())).thenAnswer(invocation -> baseUri + GROUPS_PATH_V2 + invocation.getArguments()[0] + "/" + invocation.getArguments()[1]); - when(resourceLinks.repository().delete(anyString(), anyString())).thenAnswer(invocation -> baseUri + GROUPS_PATH_V2 + invocation.getArguments()[0] + "/" + invocation.getArguments()[1]); - - when(resourceLinks.tagCollection().self(anyString(), anyString())).thenAnswer(invocation -> baseUri + GROUPS_PATH_V2 + invocation.getArguments()[0] + "/" + invocation.getArguments()[1] + "/tags/"); - - when(resourceLinks.branchCollection().self(anyString(), anyString())).thenAnswer(invocation -> baseUri + GROUPS_PATH_V2 + invocation.getArguments()[0] + "/" + invocation.getArguments()[1] + "/branches/"); + when(resourceLinks.user()).thenReturn(new ResourceLinks.UserLinks(uriInfo)); + when(resourceLinks.userCollection()).thenReturn(new ResourceLinks.UserCollectionLinks(uriInfo)); + when(resourceLinks.group()).thenReturn(new ResourceLinks.GroupLinks(uriInfo)); + when(resourceLinks.groupCollection()).thenReturn(new ResourceLinks.GroupCollectionLinks(uriInfo)); + when(resourceLinks.repository()).thenReturn(new ResourceLinks.RepositoryLinks(uriInfo)); + when(resourceLinks.tagCollection()).thenReturn(new ResourceLinks.TagCollectionLinks(uriInfo)); + when(resourceLinks.branchCollection()).thenReturn(new ResourceLinks.BranchCollectionLinks(uriInfo)); } } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserCollectionToDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserCollectionToDtoMapperTest.java index b9ed8558d4..9b8a9c5d42 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserCollectionToDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserCollectionToDtoMapperTest.java @@ -7,7 +7,6 @@ import org.apache.shiro.util.ThreadContext; import org.junit.After; import org.junit.Before; import org.junit.Test; -import org.mockito.Answers; import org.mockito.InjectMocks; import org.mockito.Mock; import sonia.scm.PageResult; @@ -28,7 +27,7 @@ import static sonia.scm.PageResult.createPage; public class UserCollectionToDtoMapperTest { - @Mock(answer = Answers.RETURNS_DEEP_STUBS) + @Mock private ResourceLinks resourceLinks; @Mock private UserToUserDtoMapper userToDtoMapper; diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java index 077cbef9b2..5ac4096583 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java @@ -11,7 +11,6 @@ import org.jboss.resteasy.mock.MockHttpResponse; import org.junit.Before; import org.junit.Rule; import org.junit.Test; -import org.mockito.Answers; import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; @@ -51,7 +50,7 @@ public class UserRootResourceTest { private Dispatcher dispatcher = MockDispatcherFactory.createDispatcher(); - @Mock(answer = Answers.RETURNS_DEEP_STUBS) + @Mock private ResourceLinks resourceLinks; @Mock diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserToUserDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserToUserDtoMapperTest.java index ae3168cc07..ebdd7e3c30 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserToUserDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserToUserDtoMapperTest.java @@ -7,7 +7,6 @@ import org.apache.shiro.util.ThreadState; import org.junit.After; import org.junit.Before; import org.junit.Test; -import org.mockito.Answers; import org.mockito.InjectMocks; import org.mockito.Mock; import sonia.scm.api.rest.resources.UserResource; @@ -25,7 +24,7 @@ import static org.mockito.MockitoAnnotations.initMocks; public class UserToUserDtoMapperTest { - @Mock(answer = Answers.RETURNS_DEEP_STUBS) + @Mock private ResourceLinks resourceLinks; @InjectMocks From 397cf012a18c1b2b68108d0581d19aad4276681e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 4 Jul 2018 10:57:34 +0200 Subject: [PATCH 12/38] Simplify mocking of resource links even further --- .../api/v2/resources/GroupRootResourceTest.java | 5 +---- .../v2/resources/GroupToGroupDtoMapperTest.java | 9 ++------- .../v2/resources/RepositoryRootResourceTest.java | 7 ++++--- .../RepositoryToRepositoryDtoMapperTest.java | 16 +++++++++------- .../scm/api/v2/resources/ResourceLinksMock.java | 4 +++- .../resources/UserCollectionToDtoMapperTest.java | 6 ++---- .../api/v2/resources/UserRootResourceTest.java | 5 +---- .../v2/resources/UserToUserDtoMapperTest.java | 10 +++------- 8 files changed, 25 insertions(+), 37 deletions(-) diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupRootResourceTest.java index cec210de1b..f071015b80 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupRootResourceTest.java @@ -48,8 +48,7 @@ public class GroupRootResourceTest { private Dispatcher dispatcher = MockDispatcherFactory.createDispatcher(); - @Mock - private ResourceLinks resourceLinks; + private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(URI.create("/")); @Mock private GroupManager groupManager; @@ -70,8 +69,6 @@ public class GroupRootResourceTest { when(groupManager.getPage(any(), eq(0), eq(10))).thenReturn(new PageResult<>(singletonList(group), 1)); when(groupManager.get("admin")).thenReturn(group); - ResourceLinksMock.initMock(resourceLinks, URI.create("/")); - GroupCollectionToDtoMapper groupCollectionToDtoMapper = new GroupCollectionToDtoMapper(groupToDtoMapper, resourceLinks); GroupCollectionResource groupCollectionResource = new GroupCollectionResource(groupManager, dtoToGroupMapper, groupCollectionToDtoMapper, resourceLinks); GroupResource groupResource = new GroupResource(groupManager, groupToDtoMapper, dtoToGroupMapper); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupToGroupDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupToGroupDtoMapperTest.java index 221733486e..831a4b7c2a 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupToGroupDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupToGroupDtoMapperTest.java @@ -8,7 +8,6 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; import org.mockito.InjectMocks; -import org.mockito.Mock; import sonia.scm.group.Group; import java.net.URI; @@ -23,8 +22,8 @@ import static org.mockito.MockitoAnnotations.initMocks; public class GroupToGroupDtoMapperTest { - @Mock - private ResourceLinks resourceLinks; + private final URI baseUri = URI.create("http://example.com/base/"); + private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); @InjectMocks private GroupToGroupDtoMapperImpl mapper; @@ -37,12 +36,8 @@ public class GroupToGroupDtoMapperTest { @Before public void init() throws URISyntaxException { initMocks(this); - URI baseUri = new URI("http://example.com/base/"); expectedBaseUri = baseUri.resolve(GroupRootResource.GROUPS_PATH_V2 + "/"); subjectThreadState.bind(); - - ResourceLinksMock.initMock(resourceLinks, baseUri); - ThreadContext.bind(subject); } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java index 023bb71dc2..fc03348c98 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java @@ -38,15 +38,16 @@ public class RepositoryRootResourceTest { @Mock private RepositoryManager repositoryManager; - @Mock - private ResourceLinks resourceLinks; + + private final URI baseUri = URI.create("/"); + private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); + @InjectMocks private RepositoryToRepositoryDtoMapperImpl repositoryToDtoMapper; @Before public void prepareEnvironment() { initMocks(this); - ResourceLinksMock.initMock(resourceLinks, URI.create("/")); RepositoryResource repositoryResource = new RepositoryResource(repositoryToDtoMapper, repositoryManager, null, null); RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider.of(repositoryResource)); dispatcher.getRegistry().addSingletonResource(repositoryRootResource); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java index 67f903e6de..c286efe4b7 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java @@ -4,17 +4,16 @@ import org.apache.shiro.subject.Subject; import org.apache.shiro.subject.support.SubjectThreadState; import org.apache.shiro.util.ThreadContext; import org.apache.shiro.util.ThreadState; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.mockito.InjectMocks; -import org.mockito.Mock; import sonia.scm.repository.HealthCheckFailure; import sonia.scm.repository.Permission; import sonia.scm.repository.PermissionType; import sonia.scm.repository.Repository; import java.net.URI; -import java.net.URISyntaxException; import static java.util.Arrays.asList; import static org.junit.Assert.assertEquals; @@ -25,8 +24,8 @@ import static org.mockito.MockitoAnnotations.initMocks; public class RepositoryToRepositoryDtoMapperTest { - @Mock - private ResourceLinks resourceLinks; + private final URI baseUri = URI.create("http://example.com/base/"); + private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); @InjectMocks private RepositoryToRepositoryDtoMapperImpl mapper; @@ -37,15 +36,18 @@ public class RepositoryToRepositoryDtoMapperTest { private URI expectedBaseUri; @Before - public void init() throws URISyntaxException { + public void init() { initMocks(this); - URI baseUri = new URI("http://example.com/base/"); expectedBaseUri = baseUri.resolve(RepositoryRootResource.REPOSITORIES_PATH_V2 + "/"); subjectThreadState.bind(); - ResourceLinksMock.initMock(resourceLinks, baseUri); ThreadContext.bind(subject); } + @After + public void cleanup() { + ThreadContext.unbindSubject(); + } + @Test public void shouldMapSimpleProperties() { RepositoryDto dto = mapper.map(createTestRepository()); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java index 8913a6d9c4..6e3cd921bf 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java @@ -7,7 +7,8 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class ResourceLinksMock { - public static void initMock(ResourceLinks resourceLinks, URI baseUri) { + public static ResourceLinks createMock(URI baseUri) { + ResourceLinks resourceLinks = mock(ResourceLinks.class); UriInfo uriInfo = mock(UriInfo.class); when(uriInfo.getBaseUri()).thenReturn(baseUri); @@ -19,5 +20,6 @@ public class ResourceLinksMock { when(resourceLinks.repository()).thenReturn(new ResourceLinks.RepositoryLinks(uriInfo)); when(resourceLinks.tagCollection()).thenReturn(new ResourceLinks.TagCollectionLinks(uriInfo)); when(resourceLinks.branchCollection()).thenReturn(new ResourceLinks.BranchCollectionLinks(uriInfo)); + return resourceLinks; } } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserCollectionToDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserCollectionToDtoMapperTest.java index 9b8a9c5d42..4d2bf7c62e 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserCollectionToDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserCollectionToDtoMapperTest.java @@ -27,8 +27,8 @@ import static sonia.scm.PageResult.createPage; public class UserCollectionToDtoMapperTest { - @Mock - private ResourceLinks resourceLinks; + private final URI baseUri = URI.create("http://example.com/base/"); + private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); @Mock private UserToUserDtoMapper userToDtoMapper; @Mock @@ -44,9 +44,7 @@ public class UserCollectionToDtoMapperTest { @Before public void init() throws URISyntaxException { initMocks(this); - URI baseUri = new URI("http://example.com/base/"); expectedBaseUri = baseUri.resolve(UserRootResource.USERS_PATH_V2 + "/"); - ResourceLinksMock.initMock(resourceLinks, baseUri); subjectThreadState.bind(); ThreadContext.bind(subject); } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java index 5ac4096583..2cf2ee0566 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java @@ -50,8 +50,7 @@ public class UserRootResourceTest { private Dispatcher dispatcher = MockDispatcherFactory.createDispatcher(); - @Mock - private ResourceLinks resourceLinks; + private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(URI.create("/")); @Mock private PasswordService passwordService; @@ -72,8 +71,6 @@ public class UserRootResourceTest { doNothing().when(userManager).modify(userCaptor.capture()); doNothing().when(userManager).delete(userCaptor.capture()); - ResourceLinksMock.initMock(resourceLinks, URI.create("/")); - UserCollectionToDtoMapper userCollectionToDtoMapper = new UserCollectionToDtoMapper(userToDtoMapper, resourceLinks); UserCollectionResource userCollectionResource = new UserCollectionResource(userManager, dtoToUserMapper, userCollectionToDtoMapper, resourceLinks); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserToUserDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserToUserDtoMapperTest.java index ebdd7e3c30..0ac980df45 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserToUserDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserToUserDtoMapperTest.java @@ -8,12 +8,10 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; import org.mockito.InjectMocks; -import org.mockito.Mock; import sonia.scm.api.rest.resources.UserResource; import sonia.scm.user.User; import java.net.URI; -import java.net.URISyntaxException; import java.time.Instant; import static org.junit.Assert.assertEquals; @@ -24,8 +22,8 @@ import static org.mockito.MockitoAnnotations.initMocks; public class UserToUserDtoMapperTest { - @Mock - private ResourceLinks resourceLinks; + private final URI baseUri = URI.create("http://example.com/base/"); + private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); @InjectMocks private UserToUserDtoMapperImpl mapper; @@ -36,12 +34,10 @@ public class UserToUserDtoMapperTest { private URI expectedBaseUri; @Before - public void init() throws URISyntaxException { + public void init() { initMocks(this); - URI baseUri = new URI("http://example.com/base/"); expectedBaseUri = baseUri.resolve(UserRootResource.USERS_PATH_V2 + "/"); subjectThreadState.bind(); - ResourceLinksMock.initMock(resourceLinks, baseUri); ThreadContext.bind(subject); } From d26f4bb2651af9730aa5b384a6bac6c739254b1f Mon Sep 17 00:00:00 2001 From: Johannes Schnatterer Date: Wed, 4 Jul 2018 11:31:09 +0200 Subject: [PATCH 13/38] Review - gets rid of warnings Especially regarding generics params. --- .../AbstractManagerResourceTest.java | 36 +++++++++++-------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/scm-webapp/src/test/java/sonia/scm/api/rest/resources/AbstractManagerResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/rest/resources/AbstractManagerResourceTest.java index 581cd6df03..e8421bc417 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/rest/resources/AbstractManagerResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/rest/resources/AbstractManagerResourceTest.java @@ -2,7 +2,11 @@ package sonia.scm.api.rest.resources; import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; import sonia.scm.Manager; import sonia.scm.ModelObject; @@ -13,24 +17,32 @@ import java.util.Comparator; import static java.util.Collections.emptyList; import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentCaptor.forClass; import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +@RunWith(MockitoJUnitRunner.class) public class AbstractManagerResourceTest { - private final Manager manager = mock(Manager.class); - private final Request request = mock(Request.class); - private final ArgumentCaptor comparatorCaptor = forClass(Comparator.class); + @Mock + private Manager manager; + @Mock + private Request request; + @Captor + private ArgumentCaptor> comparatorCaptor; - private final AbstractManagerResource abstractManagerResource = new SimpleManagerResource(); + private AbstractManagerResource abstractManagerResource; + + @Before + public void captureComparator() { + when(manager.getAll(comparatorCaptor.capture(), eq(0), eq(1))).thenReturn(emptyList()); + abstractManagerResource = new SimpleManagerResource(); + } @Test public void shouldAcceptDefaultSortByParameter() { abstractManagerResource.getAll(request, 0, 1, null, true); - Comparator comparator = comparatorCaptor.getValue(); + Comparator comparator = comparatorCaptor.getValue(); assertTrue(comparator.compare(new Simple("1", null), new Simple("2", null)) > 0); } @@ -38,7 +50,7 @@ public class AbstractManagerResourceTest { public void shouldAcceptValidSortByParameter() { abstractManagerResource.getAll(request, 0, 1, "data", true); - Comparator comparator = comparatorCaptor.getValue(); + Comparator comparator = comparatorCaptor.getValue(); assertTrue(comparator.compare(new Simple("", "1"), new Simple("", "2")) > 0); } @@ -47,10 +59,6 @@ public class AbstractManagerResourceTest { abstractManagerResource.getAll(request, 0, 1, "x", true); } - @Before - public void captureComparator() { - when(manager.getAll(comparatorCaptor.capture(), eq(0), eq(1))).thenReturn(emptyList()); - } private class SimpleManagerResource extends AbstractManagerResource { @@ -58,7 +66,7 @@ public class AbstractManagerResourceTest { disableCache = true; } - public SimpleManagerResource() { + private SimpleManagerResource() { super(AbstractManagerResourceTest.this.manager, Simple.class); } @@ -83,7 +91,7 @@ public class AbstractManagerResourceTest { private String id; private String data; - public Simple(String id, String data) { + Simple(String id, String data) { this.id = id; this.data = data; } From cc30b7d2a1164b71313466f3cbdc1bf7ee0bb162 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 4 Jul 2018 11:53:48 +0200 Subject: [PATCH 14/38] Remove id from repository dto --- .../src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java | 1 - .../api/v2/resources/RepositoryToRepositoryDtoMapperTest.java | 1 - 2 files changed, 2 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java index 14083837bf..cde2feac27 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java @@ -13,7 +13,6 @@ import java.util.List; @Getter @Setter public class RepositoryDto extends HalRepresentation { - private String id; private String contact; private Instant creationDate; private String description; diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java index c286efe4b7..54bfb9d2c6 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java @@ -56,7 +56,6 @@ public class RepositoryToRepositoryDtoMapperTest { assertEquals("description", dto.getDescription()); assertEquals("git", dto.getType()); assertEquals("none@example.com", dto.getContact()); - assertEquals("1", dto.getId()); } @Test From 01a3b932895984510469de2fbea39603917721fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 4 Jul 2018 11:58:37 +0200 Subject: [PATCH 15/38] Generate link to changesets for repository --- .../ChangesetCollectionResource.java | 18 +++++++++++++++++ .../v2/resources/ChangesetRootResource.java | 20 +++++++++++++++++++ .../api/v2/resources/RepositoryResource.java | 10 +++++++++- .../RepositoryToRepositoryDtoMapper.java | 1 + .../scm/api/v2/resources/ResourceLinks.java | 16 +++++++++++++++ .../resources/RepositoryRootResourceTest.java | 2 +- .../RepositoryToRepositoryDtoMapperTest.java | 8 ++++++++ .../api/v2/resources/ResourceLinksMock.java | 1 + .../api/v2/resources/ResourceLinksTest.java | 6 ++++++ 9 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetCollectionResource.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetRootResource.java diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetCollectionResource.java new file mode 100644 index 0000000000..d42494a270 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetCollectionResource.java @@ -0,0 +1,18 @@ +package sonia.scm.api.v2.resources; + +import javax.ws.rs.DefaultValue; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Response; + +public class ChangesetCollectionResource { + @GET + @Path("") + public Response getAll(@DefaultValue("0") @QueryParam("page") int page, + @DefaultValue("10") @QueryParam("pageSize") int pageSize, + @QueryParam("sortBy") String sortBy, + @DefaultValue("false") @QueryParam("desc") boolean desc) { + throw new UnsupportedOperationException(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetRootResource.java new file mode 100644 index 0000000000..1681a27cd4 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetRootResource.java @@ -0,0 +1,20 @@ +package sonia.scm.api.v2.resources; + +import javax.inject.Inject; +import javax.inject.Provider; +import javax.ws.rs.Path; + +public class ChangesetRootResource { + + private final Provider changesetCollectionResource; + + @Inject + public ChangesetRootResource(Provider changesetCollectionResource) { + this.changesetCollectionResource = changesetCollectionResource; + } + + @Path("") + public ChangesetCollectionResource getChangesetCollectionResource() { + return changesetCollectionResource.get(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java index e2bcd60e88..fe7cfc7ecf 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java @@ -26,18 +26,21 @@ public class RepositoryResource { private final SingleResourceManagerAdapter adapter; private final Provider tagRootResource; private final Provider branchRootResource; + private final Provider changesetRootResource; @Inject public RepositoryResource( RepositoryToRepositoryDtoMapper repositoryToDtoMapper, RepositoryManager manager, Provider tagRootResource, - Provider branchRootResource) { + Provider branchRootResource, + Provider changesetRootResource) { this.manager = manager; this.repositoryToDtoMapper = repositoryToDtoMapper; this.adapter = new SingleResourceManagerAdapter<>(manager); this.tagRootResource = tagRootResource; this.branchRootResource = branchRootResource; + this.changesetRootResource = changesetRootResource; } @GET @@ -76,4 +79,9 @@ public class RepositoryResource { public BranchRootResource branches() { return branchRootResource.get(); } + + @Path("changesets/") + public ChangesetRootResource changesets() { + return changesetRootResource.get(); + } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java index 6121ac05b2..375c69c933 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java @@ -36,6 +36,7 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper Date: Wed, 4 Jul 2018 12:06:58 +0200 Subject: [PATCH 16/38] Generate link to sources for repository --- .../api/v2/resources/RepositoryResource.java | 10 +++++++++- .../RepositoryToRepositoryDtoMapper.java | 1 + .../scm/api/v2/resources/ResourceLinks.java | 16 +++++++++++++++ .../resources/SourceCollectionResource.java | 18 +++++++++++++++++ .../api/v2/resources/SourceRootResource.java | 20 +++++++++++++++++++ .../resources/RepositoryRootResourceTest.java | 2 +- .../RepositoryToRepositoryDtoMapperTest.java | 8 ++++++++ .../api/v2/resources/ResourceLinksMock.java | 1 + .../api/v2/resources/ResourceLinksTest.java | 6 ++++++ 9 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/SourceCollectionResource.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/SourceRootResource.java diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java index fe7cfc7ecf..666cd5672a 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java @@ -27,6 +27,7 @@ public class RepositoryResource { private final Provider tagRootResource; private final Provider branchRootResource; private final Provider changesetRootResource; + private final Provider sourceRootResource; @Inject public RepositoryResource( @@ -34,13 +35,15 @@ public class RepositoryResource { RepositoryManager manager, Provider tagRootResource, Provider branchRootResource, - Provider changesetRootResource) { + Provider changesetRootResource, + Provider sourceRootResource) { this.manager = manager; this.repositoryToDtoMapper = repositoryToDtoMapper; this.adapter = new SingleResourceManagerAdapter<>(manager); this.tagRootResource = tagRootResource; this.branchRootResource = branchRootResource; this.changesetRootResource = changesetRootResource; + this.sourceRootResource = sourceRootResource; } @GET @@ -84,4 +87,9 @@ public class RepositoryResource { public ChangesetRootResource changesets() { return changesetRootResource.get(); } + + @Path("sources/") + public SourceRootResource sources() { + return sourceRootResource.get(); + } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java index 375c69c933..b8c70a880d 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java @@ -37,6 +37,7 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper sourceCollectionResource; + + @Inject + public SourceRootResource(Provider sourceCollectionResource) { + this.sourceCollectionResource = sourceCollectionResource; + } + + @Path("") + public SourceCollectionResource getSourceCollectionResource() { + return sourceCollectionResource.get(); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java index 11d9de4563..cc93f2ecd2 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java @@ -48,7 +48,7 @@ public class RepositoryRootResourceTest { @Before public void prepareEnvironment() { initMocks(this); - RepositoryResource repositoryResource = new RepositoryResource(repositoryToDtoMapper, repositoryManager, null, null, null); + RepositoryResource repositoryResource = new RepositoryResource(repositoryToDtoMapper, repositoryManager, null, null, null, null); RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider.of(repositoryResource)); dispatcher.getRegistry().addSingletonResource(repositoryRootResource); } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java index bcf8948463..7d31504137 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java @@ -125,6 +125,14 @@ public class RepositoryToRepositoryDtoMapperTest { dto.getLinks().getLinkBy("changesets").get().getHref()); } + @Test + public void shouldCreateSourcesLink() { + RepositoryDto dto = mapper.map(createTestRepository()); + assertEquals( + "http://example.com/base/v2/repositories/testspace/test/sources/", + dto.getLinks().getLinkBy("sources").get().getHref()); + } + private Repository createTestRepository() { Repository repository = new Repository(); repository.setNamespace("testspace"); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java index dc4cd2e0be..efd8916811 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java @@ -21,6 +21,7 @@ public class ResourceLinksMock { when(resourceLinks.tagCollection()).thenReturn(new ResourceLinks.TagCollectionLinks(uriInfo)); when(resourceLinks.branchCollection()).thenReturn(new ResourceLinks.BranchCollectionLinks(uriInfo)); when(resourceLinks.changesetCollection()).thenReturn(new ResourceLinks.ChangesetCollectionLinks(uriInfo)); + when(resourceLinks.sourceCollection()).thenReturn(new ResourceLinks.SourceCollectionLinks(uriInfo)); return resourceLinks; } } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksTest.java index 62f2a187d7..d9f20448f0 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksTest.java @@ -120,6 +120,12 @@ public class ResourceLinksTest { assertEquals(BASE_URL + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/changesets/", url); } + @Test + public void shouldCreateCorrectSourceCollectionUrl() { + String url = resourceLinks.sourceCollection().self("space", "repo"); + assertEquals(BASE_URL + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/sources/", url); + } + @Before public void initUriInfo() { initMocks(this); From a5349b339db4fec6653740aa2678c62826e390d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 4 Jul 2018 15:11:17 +0200 Subject: [PATCH 17/38] Move repository permissions to separate endpoint --- .../PermissionCollectionResource.java | 18 ++++++++ .../v2/resources/PermissionRootResource.java | 20 +++++++++ .../scm/api/v2/resources/RepositoryDto.java | 4 -- .../api/v2/resources/RepositoryResource.java | 9 +++- .../RepositoryToRepositoryDtoMapper.java | 4 +- .../scm/api/v2/resources/ResourceLinks.java | 16 +++++++ .../resources/RepositoryRootResourceTest.java | 2 +- .../RepositoryToRepositoryDtoMapperTest.java | 44 +++++++++---------- .../api/v2/resources/ResourceLinksMock.java | 1 + .../api/v2/resources/ResourceLinksTest.java | 6 +++ 10 files changed, 92 insertions(+), 32 deletions(-) create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionCollectionResource.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionCollectionResource.java new file mode 100644 index 0000000000..6c4b52c16d --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionCollectionResource.java @@ -0,0 +1,18 @@ +package sonia.scm.api.v2.resources; + +import javax.ws.rs.DefaultValue; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Response; + +public class PermissionCollectionResource { + @GET + @Path("") + public Response getAll(@DefaultValue("0") @QueryParam("page") int page, + @DefaultValue("10") @QueryParam("pageSize") int pageSize, + @QueryParam("sortBy") String sortBy, + @DefaultValue("false") @QueryParam("desc") boolean desc) { + throw new UnsupportedOperationException(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java new file mode 100644 index 0000000000..cd1e970e43 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java @@ -0,0 +1,20 @@ +package sonia.scm.api.v2.resources; + +import javax.inject.Inject; +import javax.inject.Provider; +import javax.ws.rs.Path; + +public class PermissionRootResource { + + private final Provider permissionCollectionResource; + + @Inject + public PermissionRootResource(Provider permissionCollectionResource) { + this.permissionCollectionResource = permissionCollectionResource; + } + + @Path("") + public PermissionCollectionResource getPermissionCollectionResource() { + return permissionCollectionResource.get(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java index cde2feac27..fbef4ca16a 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java @@ -6,7 +6,6 @@ import de.otto.edison.hal.Links; import lombok.Getter; import lombok.Setter; -import javax.xml.bind.annotation.XmlElement; import java.time.Instant; import java.util.List; @@ -21,9 +20,6 @@ public class RepositoryDto extends HalRepresentation { private Instant lastModified; private String namespace; private String name; - private List permissions; - @XmlElement(name = "public") - private boolean publicReadable = false; private boolean archived = false; private String type; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java index 666cd5672a..0884664891 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java @@ -28,6 +28,7 @@ public class RepositoryResource { private final Provider branchRootResource; private final Provider changesetRootResource; private final Provider sourceRootResource; + private final Provider permissionRootResource; @Inject public RepositoryResource( @@ -36,7 +37,7 @@ public class RepositoryResource { Provider tagRootResource, Provider branchRootResource, Provider changesetRootResource, - Provider sourceRootResource) { + Provider sourceRootResource, Provider permissionRootResource) { this.manager = manager; this.repositoryToDtoMapper = repositoryToDtoMapper; this.adapter = new SingleResourceManagerAdapter<>(manager); @@ -44,6 +45,7 @@ public class RepositoryResource { this.branchRootResource = branchRootResource; this.changesetRootResource = changesetRootResource; this.sourceRootResource = sourceRootResource; + this.permissionRootResource = permissionRootResource; } @GET @@ -92,4 +94,9 @@ public class RepositoryResource { public SourceRootResource sources() { return sourceRootResource.get(); } + + @Path("permissions/") + public PermissionRootResource permissions() { + return permissionRootResource.get(); + } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java index b8c70a880d..2f13723d39 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java @@ -6,7 +6,6 @@ import org.mapstruct.AfterMapping; import org.mapstruct.Mapper; import org.mapstruct.MappingTarget; import sonia.scm.repository.HealthCheckFailure; -import sonia.scm.repository.Permission; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryPermissions; @@ -23,8 +22,6 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper Date: Wed, 4 Jul 2018 16:45:52 +0200 Subject: [PATCH 18/38] Create resource collection endpoint for get --- .../main/java/sonia/scm/web/VndMediaType.java | 1 + .../RepositoryCollectionResource.java | 54 +++++++++++++++++++ .../RepositoryCollectionToDtoMapper.java | 34 ++++++++++++ .../v2/resources/RepositoryRootResource.java | 9 +++- .../scm/api/v2/resources/ResourceLinks.java | 20 +++++++ .../resources/RepositoryRootResourceTest.java | 26 ++++++++- .../api/v2/resources/ResourceLinksMock.java | 1 + 7 files changed, 143 insertions(+), 2 deletions(-) create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionToDtoMapper.java diff --git a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java index f306114ecb..fb80450bc8 100644 --- a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java +++ b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java @@ -17,6 +17,7 @@ public class VndMediaType { public static final String REPOSITORY = PREFIX + "repository" + SUFFIX; public static final String USER_COLLECTION = PREFIX + "userCollection" + SUFFIX; public static final String GROUP_COLLECTION = PREFIX + "groupCollection" + SUFFIX; + public static final String REPOSITORY_COLLECTION = PREFIX + "repositoryCollection" + SUFFIX; private VndMediaType() { } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java new file mode 100644 index 0000000000..e9c1f9e34e --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java @@ -0,0 +1,54 @@ +package sonia.scm.api.v2.resources; + +import com.webcohesion.enunciate.metadata.rs.ResponseCode; +import com.webcohesion.enunciate.metadata.rs.StatusCodes; +import com.webcohesion.enunciate.metadata.rs.TypeHint; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryException; +import sonia.scm.repository.RepositoryManager; +import sonia.scm.web.VndMediaType; + +import javax.inject.Inject; +import javax.ws.rs.DefaultValue; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Response; + +public class RepositoryCollectionResource { + + private final CollectionResourceManagerAdapter adapter; + private final RepositoryCollectionToDtoMapper repositoryCollectionToDtoMapper; + + @Inject + public RepositoryCollectionResource(RepositoryManager manager, RepositoryCollectionToDtoMapper repositoryCollectionToDtoMapper) { + this.adapter = new CollectionResourceManagerAdapter<>(manager); + this.repositoryCollectionToDtoMapper = repositoryCollectionToDtoMapper; + } + + @GET + @Path("") + @Produces(VndMediaType.REPOSITORY_COLLECTION) + @TypeHint(UserDto[].class) + @StatusCodes({ + @ResponseCode(code = 200, condition = "success"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"repository\" privilege"), + @ResponseCode(code = 500, condition = "internal server error") + }) + public Response getAll(@DefaultValue("0") @QueryParam("page") int page, + @DefaultValue("10") @QueryParam("pageSize") int pageSize, + @QueryParam("sortBy") String sortBy, + @DefaultValue("false") @QueryParam("desc") boolean desc) { + return adapter.getAll(page, pageSize, sortBy, desc, + pageResult -> repositoryCollectionToDtoMapper.map(page, pageSize, pageResult)); + } + + @POST + @Path("") + public Response create() { + throw new UnsupportedOperationException(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionToDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionToDtoMapper.java new file mode 100644 index 0000000000..a1cf0218e4 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionToDtoMapper.java @@ -0,0 +1,34 @@ +package sonia.scm.api.v2.resources; + +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryPermissions; + +import javax.inject.Inject; + +// Mapstruct does not support parameterized (i.e. non-default) constructors. Thus, we need to use field injection. +@SuppressWarnings("squid:S3306") +public class RepositoryCollectionToDtoMapper extends BasicCollectionToDtoMapper { + + private final ResourceLinks resourceLinks; + + @Inject + public RepositoryCollectionToDtoMapper(RepositoryToRepositoryDtoMapper repositoryToDtoMapper, ResourceLinks resourceLinks) { + super("repositories", repositoryToDtoMapper); + this.resourceLinks = resourceLinks; + } + + @Override + String createCreateLink() { + return resourceLinks.repositoryCollection().create(); + } + + @Override + String createSelfLink() { + return resourceLinks.repositoryCollection().self(); + } + + @Override + boolean isCreatePermitted() { + return RepositoryPermissions.create().isPermitted(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRootResource.java index e7861a884b..a7a6365c37 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRootResource.java @@ -12,14 +12,21 @@ public class RepositoryRootResource { static final String REPOSITORIES_PATH_V2 = "v2/repositories/"; private final Provider repositoryResource; + private final Provider repositoryCollectionResource; @Inject - public RepositoryRootResource(Provider repositoryResource) { + public RepositoryRootResource(Provider repositoryResource, Provider repositoryCollectionResource) { this.repositoryResource = repositoryResource; + this.repositoryCollectionResource = repositoryCollectionResource; } @Path("{namespace}/{name}") public RepositoryResource getRepositoryResource() { return repositoryResource.get(); } + + @Path("") + public RepositoryCollectionResource getRepositoryCollectionResource() { + return repositoryCollectionResource.get(); + } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java index 11d1bc97ef..8f6f1eaae3 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java @@ -125,6 +125,26 @@ class ResourceLinks { } } + RepositoryCollectionLinks repositoryCollection() { + return new RepositoryCollectionLinks(uriInfoStore.get()); + } + + static class RepositoryCollectionLinks { + private final LinkBuilder collectionLinkBuilder; + + RepositoryCollectionLinks(UriInfo uriInfo) { + collectionLinkBuilder = new LinkBuilder(uriInfo, RepositoryRootResource.class, RepositoryCollectionResource.class); + } + + String self() { + return collectionLinkBuilder.method("getRepositoryCollectionResource").parameters().method("getAll").parameters().href(); + } + + String create() { + return collectionLinkBuilder.method("getRepositoryCollectionResource").parameters().method("create").parameters().href(); + } + } + public TagCollectionLinks tagCollection() { return new TagCollectionLinks(uriInfoStore.get()); } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java index fcd918152e..66e8aa0056 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java @@ -11,16 +11,20 @@ import org.junit.Rule; import org.junit.Test; import org.mockito.InjectMocks; import org.mockito.Mock; +import sonia.scm.PageResult; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryManager; import java.net.URI; import java.net.URISyntaxException; +import static java.util.Collections.singletonList; import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; import static javax.servlet.http.HttpServletResponse.SC_OK; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; @@ -49,7 +53,9 @@ public class RepositoryRootResourceTest { public void prepareEnvironment() { initMocks(this); RepositoryResource repositoryResource = new RepositoryResource(repositoryToDtoMapper, repositoryManager, null, null, null, null, null); - RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider.of(repositoryResource)); + RepositoryCollectionToDtoMapper repositoryCollectionToDtoMapper = new RepositoryCollectionToDtoMapper(repositoryToDtoMapper, resourceLinks); + RepositoryCollectionResource repositoryCollectionResource = new RepositoryCollectionResource(repositoryManager, repositoryCollectionToDtoMapper); + RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider.of(repositoryResource), MockProvider.of(repositoryCollectionResource)); dispatcher.getRegistry().addSingletonResource(repositoryRootResource); } @@ -78,6 +84,24 @@ public class RepositoryRootResourceTest { assertTrue(response.getContentAsString().contains("\"name\":\"repo\"")); } + @Test + public void shouldGetAll() throws URISyntaxException { + PageResult singletonPageResult = createSingletonPageResult(mockRepository("space", "repo")); + when(repositoryManager.getPage(any(), eq(0), eq(10))).thenReturn(singletonPageResult); + + MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(SC_OK, response.getStatus()); + assertTrue(response.getContentAsString().contains("\"name\":\"repo\"")); + } + + private PageResult createSingletonPageResult(Repository repository) { + return new PageResult<>(singletonList(repository), 0); + } + private Repository mockRepository(String namespace, String name) { Repository repository = new Repository(); repository.setNamespace(namespace); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java index 5dad23fe58..b35bc3820c 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java @@ -18,6 +18,7 @@ public class ResourceLinksMock { when(resourceLinks.group()).thenReturn(new ResourceLinks.GroupLinks(uriInfo)); when(resourceLinks.groupCollection()).thenReturn(new ResourceLinks.GroupCollectionLinks(uriInfo)); when(resourceLinks.repository()).thenReturn(new ResourceLinks.RepositoryLinks(uriInfo)); + when(resourceLinks.repositoryCollection()).thenReturn(new ResourceLinks.RepositoryCollectionLinks(uriInfo)); when(resourceLinks.tagCollection()).thenReturn(new ResourceLinks.TagCollectionLinks(uriInfo)); when(resourceLinks.branchCollection()).thenReturn(new ResourceLinks.BranchCollectionLinks(uriInfo)); when(resourceLinks.changesetCollection()).thenReturn(new ResourceLinks.ChangesetCollectionLinks(uriInfo)); From c2effbe9c5ff4b7d14e75d4f7710dcc84fea011b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 5 Jul 2018 10:42:09 +0200 Subject: [PATCH 19/38] Implement update and delete for repository --- .../scm/api/v2/resources/MapperModule.java | 1 + .../RepositoryDtoToRepositoryMapper.java | 18 +++++ .../api/v2/resources/RepositoryResource.java | 21 ++++-- .../SingleResourceManagerAdapter.java | 5 ++ .../resources/RepositoryRootResourceTest.java | 66 ++++++++++++++++++- .../scm/api/v2/repository-test-update.json | 8 +++ 6 files changed, 113 insertions(+), 6 deletions(-) create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDtoToRepositoryMapper.java create mode 100644 scm-webapp/src/test/resources/sonia/scm/api/v2/repository-test-update.json diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java index 0a0468de3c..8e22755fe0 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java @@ -16,6 +16,7 @@ public class MapperModule extends AbstractModule { bind(GroupCollectionToDtoMapper.class); bind(RepositoryToRepositoryDtoMapper.class).to(Mappers.getMapper(RepositoryToRepositoryDtoMapper.class).getClass()); + bind(RepositoryDtoToRepositoryMapper.class).to(Mappers.getMapper(RepositoryDtoToRepositoryMapper.class).getClass()); bind(UriInfoStore.class).in(ServletScopes.REQUEST); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDtoToRepositoryMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDtoToRepositoryMapper.java new file mode 100644 index 0000000000..9bab9d1d3b --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDtoToRepositoryMapper.java @@ -0,0 +1,18 @@ +package sonia.scm.api.v2.resources; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import sonia.scm.repository.Repository; + +@Mapper +public abstract class RepositoryDtoToRepositoryMapper { + + @Mapping(target = "creationDate", ignore = true) + @Mapping(target = "lastModified", ignore = true) + @Mapping(target = "id", ignore = true) + @Mapping(target = "publicReadable", ignore = true) + @Mapping(target = "healthCheckFailures", ignore = true) + @Mapping(target = "permissions", ignore = true) + public abstract Repository map(RepositoryDto repositoryDto); + +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java index 0884664891..d1ecadf4dd 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java @@ -21,6 +21,7 @@ import javax.ws.rs.core.Response; public class RepositoryResource { private final RepositoryToRepositoryDtoMapper repositoryToDtoMapper; + private final RepositoryDtoToRepositoryMapper dtoToRepositoryMapper; private final RepositoryManager manager; private final SingleResourceManagerAdapter adapter; @@ -33,11 +34,12 @@ public class RepositoryResource { @Inject public RepositoryResource( RepositoryToRepositoryDtoMapper repositoryToDtoMapper, - RepositoryManager manager, + RepositoryDtoToRepositoryMapper dtoToRepositoryMapper, RepositoryManager manager, Provider tagRootResource, Provider branchRootResource, Provider changesetRootResource, Provider sourceRootResource, Provider permissionRootResource) { + this.dtoToRepositoryMapper = dtoToRepositoryMapper; this.manager = manager; this.repositoryToDtoMapper = repositoryToDtoMapper; this.adapter = new SingleResourceManagerAdapter<>(manager); @@ -65,14 +67,25 @@ public class RepositoryResource { @DELETE @Path("") + @StatusCodes({ + @ResponseCode(code = 204, condition = "delete success or nothing to delete"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"repository\" privilege"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @TypeHint(TypeHint.NO_CONTENT.class) public Response delete(@PathParam("namespace") String namespace, @PathParam("name") String name) { - throw new UnsupportedOperationException(); + return adapter.delete(() -> manager.getByNamespace(namespace, name)); } @PUT @Path("") - public Response update(@PathParam("namespace") String namespace, @PathParam("name") String name) { - throw new UnsupportedOperationException(); + public Response update(@PathParam("namespace") String namespace, @PathParam("name") String name, RepositoryDto repositoryDto) { + return adapter.update(() -> manager.getByNamespace(namespace, name), existing -> { + Repository repository = dtoToRepositoryMapper.map(repositoryDto); + repository.setId(existing.getId()); + return repository; + }); } @Path("tags/") diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java index a56111d893..d5e7a97c51 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java @@ -60,6 +60,11 @@ class SingleResourceManagerAdapter reader) { + MODEL_OBJECT existingModelObject = reader.get(); + return delete(existingModelObject.getId()); + } + @Override protected GenericEntity> createGenericEntity(Collection modelObjects) { throw new UnsupportedOperationException(); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java index 66e8aa0056..a5367971b4 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java @@ -2,6 +2,7 @@ package sonia.scm.api.v2.resources; import com.github.sdorra.shiro.ShiroRule; import com.github.sdorra.shiro.SubjectAware; +import com.google.common.io.Resources; import org.jboss.resteasy.core.Dispatcher; import org.jboss.resteasy.mock.MockDispatcherFactory; import org.jboss.resteasy.mock.MockHttpRequest; @@ -14,17 +15,23 @@ import org.mockito.Mock; import sonia.scm.PageResult; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryManager; +import sonia.scm.web.VndMediaType; +import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; +import java.net.URL; import static java.util.Collections.singletonList; import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; +import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT; import static javax.servlet.http.HttpServletResponse.SC_OK; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyObject; import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; @@ -48,11 +55,13 @@ public class RepositoryRootResourceTest { @InjectMocks private RepositoryToRepositoryDtoMapperImpl repositoryToDtoMapper; + @InjectMocks + private RepositoryDtoToRepositoryMapperImpl dtoToRepositoryMapper; @Before public void prepareEnvironment() { initMocks(this); - RepositoryResource repositoryResource = new RepositoryResource(repositoryToDtoMapper, repositoryManager, null, null, null, null, null); + RepositoryResource repositoryResource = new RepositoryResource(repositoryToDtoMapper, dtoToRepositoryMapper, repositoryManager, null, null, null, null, null); RepositoryCollectionToDtoMapper repositoryCollectionToDtoMapper = new RepositoryCollectionToDtoMapper(repositoryToDtoMapper, resourceLinks); RepositoryCollectionResource repositoryCollectionResource = new RepositoryCollectionResource(repositoryManager, repositoryCollectionToDtoMapper); RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider.of(repositoryResource), MockProvider.of(repositoryCollectionResource)); @@ -98,6 +107,57 @@ public class RepositoryRootResourceTest { assertTrue(response.getContentAsString().contains("\"name\":\"repo\"")); } + @Test + public void shouldHandleUpdateForNotExistingRepository() throws URISyntaxException, IOException { + URL url = Resources.getResource("sonia/scm/api/v2/repository-test-update.json"); + byte[] repository = Resources.toByteArray(url); + + MockHttpRequest request = MockHttpRequest + .put("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo") + .contentType(VndMediaType.REPOSITORY) + .content(repository); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(SC_NOT_FOUND, response.getStatus()); + } + + @Test + public void shouldHandleUpdateForExistingRepository() throws Exception { + mockRepository("space", "repo"); + + URL url = Resources.getResource("sonia/scm/api/v2/repository-test-update.json"); + byte[] repository = Resources.toByteArray(url); + + MockHttpRequest request = MockHttpRequest + .put("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo") + .contentType(VndMediaType.REPOSITORY) + .content(repository); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(SC_NO_CONTENT, response.getStatus()); + verify(repositoryManager).modify(anyObject()); + } + + @Test + public void shouldHandleDeleteForExistingRepository() throws Exception { + mockRepository("space", "repo"); + + URL url = Resources.getResource("sonia/scm/api/v2/repository-test-update.json"); + byte[] repository = Resources.toByteArray(url); + + MockHttpRequest request = MockHttpRequest.delete("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo"); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(SC_NO_CONTENT, response.getStatus()); + verify(repositoryManager).delete(anyObject()); + } + private PageResult createSingletonPageResult(Repository repository) { return new PageResult<>(singletonList(repository), 0); } @@ -106,8 +166,10 @@ public class RepositoryRootResourceTest { Repository repository = new Repository(); repository.setNamespace(namespace); repository.setName(name); - repository.setId("id"); + String id = namespace + "-" + name; + repository.setId(id); when(repositoryManager.getByNamespace(namespace, name)).thenReturn(repository); + when(repositoryManager.get(id)).thenReturn(repository); return repository; } } diff --git a/scm-webapp/src/test/resources/sonia/scm/api/v2/repository-test-update.json b/scm-webapp/src/test/resources/sonia/scm/api/v2/repository-test-update.json new file mode 100644 index 0000000000..660fa256bf --- /dev/null +++ b/scm-webapp/src/test/resources/sonia/scm/api/v2/repository-test-update.json @@ -0,0 +1,8 @@ +{ + "contact": "none@example.com", + "description": "Test repository", + "namespace": "space", + "name": "repo", + "archived": false, + "type": "git" +} From 92bd8696bedf9892cad0270b28afc75a9a0b94b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 5 Jul 2018 10:45:16 +0200 Subject: [PATCH 20/38] Add docs --- .../scm/api/v2/resources/RepositoryResource.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java index d1ecadf4dd..f7fd3dab37 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java @@ -10,6 +10,7 @@ import sonia.scm.web.VndMediaType; import javax.inject.Inject; import javax.inject.Provider; +import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.PUT; @@ -80,6 +81,16 @@ public class RepositoryResource { @PUT @Path("") + @Consumes(VndMediaType.REPOSITORY) + @StatusCodes({ + @ResponseCode(code = 204, condition = "update success"), + @ResponseCode(code = 400, condition = "Invalid body, e.g. illegal change of namespace or name"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"repository\" privilege"), + @ResponseCode(code = 404, condition = "not found, no repository with the specified namespace and name available"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @TypeHint(TypeHint.NO_CONTENT.class) public Response update(@PathParam("namespace") String namespace, @PathParam("name") String name, RepositoryDto repositoryDto) { return adapter.update(() -> manager.getByNamespace(namespace, name), existing -> { Repository repository = dtoToRepositoryMapper.map(repositoryDto); From e993cebc0de20afffcaf1edc5ee8fda625125b36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 5 Jul 2018 10:52:19 +0200 Subject: [PATCH 21/38] Implement create repository --- .../RepositoryCollectionResource.java | 26 ++++++++++++++++--- .../resources/RepositoryRootResourceTest.java | 24 ++++++++++++++--- 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java index e9c1f9e34e..99a20b25d8 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java @@ -1,6 +1,8 @@ package sonia.scm.api.v2.resources; import com.webcohesion.enunciate.metadata.rs.ResponseCode; +import com.webcohesion.enunciate.metadata.rs.ResponseHeader; +import com.webcohesion.enunciate.metadata.rs.ResponseHeaders; import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.TypeHint; import sonia.scm.repository.Repository; @@ -9,6 +11,7 @@ import sonia.scm.repository.RepositoryManager; import sonia.scm.web.VndMediaType; import javax.inject.Inject; +import javax.ws.rs.Consumes; import javax.ws.rs.DefaultValue; import javax.ws.rs.GET; import javax.ws.rs.POST; @@ -16,16 +19,21 @@ import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Response; +import java.io.IOException; public class RepositoryCollectionResource { private final CollectionResourceManagerAdapter adapter; private final RepositoryCollectionToDtoMapper repositoryCollectionToDtoMapper; + private final RepositoryDtoToRepositoryMapper dtoToRepositoryMapper; + private final ResourceLinks resourceLinks; @Inject - public RepositoryCollectionResource(RepositoryManager manager, RepositoryCollectionToDtoMapper repositoryCollectionToDtoMapper) { + public RepositoryCollectionResource(RepositoryManager manager, RepositoryCollectionToDtoMapper repositoryCollectionToDtoMapper, RepositoryDtoToRepositoryMapper dtoToRepositoryMapper, ResourceLinks resourceLinks) { this.adapter = new CollectionResourceManagerAdapter<>(manager); this.repositoryCollectionToDtoMapper = repositoryCollectionToDtoMapper; + this.dtoToRepositoryMapper = dtoToRepositoryMapper; + this.resourceLinks = resourceLinks; } @GET @@ -48,7 +56,19 @@ public class RepositoryCollectionResource { @POST @Path("") - public Response create() { - throw new UnsupportedOperationException(); + @Consumes(VndMediaType.REPOSITORY) + @StatusCodes({ + @ResponseCode(code = 201, condition = "create success"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"repository\" privilege"), + @ResponseCode(code = 409, condition = "conflict, a repository with this name already exists"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @TypeHint(TypeHint.NO_CONTENT.class) + @ResponseHeaders(@ResponseHeader(name = "Location", description = "uri to the created repository")) + public Response create(RepositoryDto repositoryDto) throws IOException, RepositoryException { + return adapter.create(repositoryDto, + () -> dtoToRepositoryMapper.map(repositoryDto), + user -> resourceLinks.user().self(user.getName())); } } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java index a5367971b4..e150c06d23 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java @@ -14,9 +14,11 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import sonia.scm.PageResult; import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryException; import sonia.scm.repository.RepositoryManager; import sonia.scm.web.VndMediaType; +import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; @@ -63,7 +65,7 @@ public class RepositoryRootResourceTest { initMocks(this); RepositoryResource repositoryResource = new RepositoryResource(repositoryToDtoMapper, dtoToRepositoryMapper, repositoryManager, null, null, null, null, null); RepositoryCollectionToDtoMapper repositoryCollectionToDtoMapper = new RepositoryCollectionToDtoMapper(repositoryToDtoMapper, resourceLinks); - RepositoryCollectionResource repositoryCollectionResource = new RepositoryCollectionResource(repositoryManager, repositoryCollectionToDtoMapper); + RepositoryCollectionResource repositoryCollectionResource = new RepositoryCollectionResource(repositoryManager, repositoryCollectionToDtoMapper, dtoToRepositoryMapper, resourceLinks); RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider.of(repositoryResource), MockProvider.of(repositoryCollectionResource)); dispatcher.getRegistry().addSingletonResource(repositoryRootResource); } @@ -146,9 +148,6 @@ public class RepositoryRootResourceTest { public void shouldHandleDeleteForExistingRepository() throws Exception { mockRepository("space", "repo"); - URL url = Resources.getResource("sonia/scm/api/v2/repository-test-update.json"); - byte[] repository = Resources.toByteArray(url); - MockHttpRequest request = MockHttpRequest.delete("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo"); MockHttpResponse response = new MockHttpResponse(); @@ -158,6 +157,23 @@ public class RepositoryRootResourceTest { verify(repositoryManager).delete(anyObject()); } + @Test + public void shouldCreateNewRepository() throws URISyntaxException, IOException, RepositoryException { + URL url = Resources.getResource("sonia/scm/api/v2/repository-test-update.json"); + byte[] repositoryJson = Resources.toByteArray(url); + + MockHttpRequest request = MockHttpRequest + .post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2) + .contentType(VndMediaType.REPOSITORY) + .content(repositoryJson); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(HttpServletResponse.SC_CREATED, response.getStatus()); + verify(repositoryManager).create(any(Repository.class)); + } + private PageResult createSingletonPageResult(Repository repository) { return new PageResult<>(singletonList(repository), 0); } From 4bdcb0cae87f33aa306b8faf3309e1b10efc0696 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 5 Jul 2018 10:58:26 +0200 Subject: [PATCH 22/38] Correct uri for create response --- .../scm/api/v2/resources/RepositoryCollectionResource.java | 2 +- .../sonia/scm/api/v2/resources/RepositoryRootResourceTest.java | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java index 99a20b25d8..a2f303ae8d 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java @@ -69,6 +69,6 @@ public class RepositoryCollectionResource { public Response create(RepositoryDto repositoryDto) throws IOException, RepositoryException { return adapter.create(repositoryDto, () -> dtoToRepositoryMapper.map(repositoryDto), - user -> resourceLinks.user().self(user.getName())); + repository -> resourceLinks.repository().self(repository.getNamespace(), repository.getName())); } } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java index e150c06d23..c51abb9973 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java @@ -171,6 +171,7 @@ public class RepositoryRootResourceTest { dispatcher.invoke(request, response); assertEquals(HttpServletResponse.SC_CREATED, response.getStatus()); + assertEquals("/v2/repositories/space/repo", response.getOutputHeaders().get("Location").get(0).toString()); verify(repositoryManager).create(any(Repository.class)); } From bbce9b7ca2a4000ee2b9a556fd3f4db68f38ef98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 5 Jul 2018 12:19:31 +0200 Subject: [PATCH 23/38] Use correct namespace of created repository --- scm-core/src/main/java/sonia/scm/HandlerBase.java | 8 ++------ .../src/main/java/sonia/scm/ManagerDecorator.java | 5 ++--- .../repository/AbstractSimpleRepositoryHandler.java | 13 ++++++------- scm-core/src/test/java/sonia/scm/ManagerTest.java | 2 +- .../resources/CollectionResourceManagerAdapter.java | 4 ++-- .../java/sonia/scm/group/DefaultGroupManager.java | 10 ++++++++-- .../scm/repository/DefaultRepositoryManager.java | 7 ++++--- .../java/sonia/scm/user/DefaultUserManager.java | 3 ++- .../scm/api/v2/resources/GroupRootResourceTest.java | 2 +- .../v2/resources/RepositoryRootResourceTest.java | 10 ++++++++-- .../scm/api/v2/resources/UserRootResourceTest.java | 2 +- 11 files changed, 37 insertions(+), 29 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/HandlerBase.java b/scm-core/src/main/java/sonia/scm/HandlerBase.java index 0baea8d929..ba7c26ea09 100644 --- a/scm-core/src/main/java/sonia/scm/HandlerBase.java +++ b/scm-core/src/main/java/sonia/scm/HandlerBase.java @@ -53,13 +53,9 @@ public interface HandlerBase /** * Persists a new object. * - * - * @param object to store - * - * @throws E - * @throws IOException + * @return The persisted object. */ - public void create(T object) throws E, IOException; + public T create(T object) throws E, IOException; /** * Removes a persistent object. diff --git a/scm-core/src/main/java/sonia/scm/ManagerDecorator.java b/scm-core/src/main/java/sonia/scm/ManagerDecorator.java index af0215202c..4a8227574a 100644 --- a/scm-core/src/main/java/sonia/scm/ManagerDecorator.java +++ b/scm-core/src/main/java/sonia/scm/ManagerDecorator.java @@ -35,7 +35,6 @@ package sonia.scm; //~--- JDK imports ------------------------------------------------------------ import java.io.IOException; - import java.util.Collection; import java.util.Comparator; @@ -78,9 +77,9 @@ public class ManagerDecorator * {@inheritDoc} */ @Override - public void create(T object) throws E, IOException + public T create(T object) throws E, IOException { - decorated.create(object); + return decorated.create(object); } /** diff --git a/scm-core/src/main/java/sonia/scm/repository/AbstractSimpleRepositoryHandler.java b/scm-core/src/main/java/sonia/scm/repository/AbstractSimpleRepositoryHandler.java index 670cab93e1..3e71bbb368 100644 --- a/scm-core/src/main/java/sonia/scm/repository/AbstractSimpleRepositoryHandler.java +++ b/scm-core/src/main/java/sonia/scm/repository/AbstractSimpleRepositoryHandler.java @@ -38,23 +38,20 @@ package sonia.scm.repository; import com.google.common.base.Charsets; import com.google.common.base.Throwables; import com.google.common.io.Resources; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.ConfigurationException; import sonia.scm.io.CommandResult; import sonia.scm.io.ExtendedCommand; import sonia.scm.io.FileSystem; +import sonia.scm.store.ConfigurationStoreFactory; import sonia.scm.util.IOUtil; -//~--- JDK imports ------------------------------------------------------------ - import java.io.File; import java.io.IOException; - import java.net.URL; -import sonia.scm.store.ConfigurationStoreFactory; + +//~--- JDK imports ------------------------------------------------------------ /** * @@ -108,7 +105,7 @@ public abstract class AbstractSimpleRepositoryHandler invocation.getArguments()[0]); doNothing().when(groupManager).modify(groupCaptor.capture()); Group group = createDummyGroup(); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java index c51abb9973..c4ebf88c3d 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java @@ -158,7 +158,13 @@ public class RepositoryRootResourceTest { } @Test - public void shouldCreateNewRepository() throws URISyntaxException, IOException, RepositoryException { + public void shouldCreateNewRepositoryInCorrectNamespace() throws URISyntaxException, IOException, RepositoryException { + when(repositoryManager.create(any())).thenAnswer(invocation -> { + Repository repository = (Repository) invocation.getArguments()[0]; + repository.setNamespace("otherspace"); + return repository; + }); + URL url = Resources.getResource("sonia/scm/api/v2/repository-test-update.json"); byte[] repositoryJson = Resources.toByteArray(url); @@ -171,7 +177,7 @@ public class RepositoryRootResourceTest { dispatcher.invoke(request, response); assertEquals(HttpServletResponse.SC_CREATED, response.getStatus()); - assertEquals("/v2/repositories/space/repo", response.getOutputHeaders().get("Location").get(0).toString()); + assertEquals("/v2/repositories/otherspace/repo", response.getOutputHeaders().get("Location").get(0).toString()); verify(repositoryManager).create(any(Repository.class)); } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java index 2cf2ee0566..7fb1ae5cdc 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java @@ -67,7 +67,7 @@ public class UserRootResourceTest { public void prepareEnvironment() throws IOException, UserException { initMocks(this); User dummyUser = createDummyUser("Neo"); - doNothing().when(userManager).create(userCaptor.capture()); + when(userManager.create(userCaptor.capture())).thenAnswer(invocation -> invocation.getArguments()[0]); doNothing().when(userManager).modify(userCaptor.capture()); doNothing().when(userManager).delete(userCaptor.capture()); From cad793322c8ce441e8e757aa427df77f14c699f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 5 Jul 2018 12:40:19 +0200 Subject: [PATCH 24/38] Add test for MapperModule --- .../api/v2/resources/MapperModuleTest.java | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 scm-webapp/src/test/java/sonia/scm/api/v2/resources/MapperModuleTest.java diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MapperModuleTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MapperModuleTest.java new file mode 100644 index 0000000000..a792d40e76 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MapperModuleTest.java @@ -0,0 +1,33 @@ +package sonia.scm.api.v2.resources; + +import com.google.inject.binder.AnnotatedBindingBuilder; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class MapperModuleTest { + + @Test + public void shouldBindToClassesWithDefaultConstructorOnly() { + AnnotatedBindingBuilder binding = mock(AnnotatedBindingBuilder.class); + ArgumentCaptor captor = ArgumentCaptor.forClass(Class.class); + when(binding.to(captor.capture())).thenReturn(null); + new MapperModule() { + @Override + protected AnnotatedBindingBuilder bind(Class clazz) { + return binding; + } + }.configure(); + captor.getAllValues().forEach(this::verifyClassCanBeInstantiated); + } + + private T verifyClassCanBeInstantiated(Class c) { + try { + return c.getConstructor().newInstance(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} From 0bbc58b9786c10be36d6f6a5036aaa785fe3550f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 5 Jul 2018 13:07:43 +0200 Subject: [PATCH 25/38] Make setting of id explicit for modified repositories --- .../api/v2/resources/RepositoryCollectionResource.java | 2 +- .../v2/resources/RepositoryDtoToRepositoryMapper.java | 9 ++++++++- .../sonia/scm/api/v2/resources/RepositoryResource.java | 9 ++++----- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java index a2f303ae8d..11078090af 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java @@ -68,7 +68,7 @@ public class RepositoryCollectionResource { @ResponseHeaders(@ResponseHeader(name = "Location", description = "uri to the created repository")) public Response create(RepositoryDto repositoryDto) throws IOException, RepositoryException { return adapter.create(repositoryDto, - () -> dtoToRepositoryMapper.map(repositoryDto), + () -> dtoToRepositoryMapper.map(repositoryDto, null), repository -> resourceLinks.repository().self(repository.getNamespace(), repository.getName())); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDtoToRepositoryMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDtoToRepositoryMapper.java index 9bab9d1d3b..2c02bb8180 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDtoToRepositoryMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDtoToRepositoryMapper.java @@ -1,7 +1,10 @@ package sonia.scm.api.v2.resources; +import org.mapstruct.AfterMapping; +import org.mapstruct.Context; import org.mapstruct.Mapper; import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; import sonia.scm.repository.Repository; @Mapper @@ -13,6 +16,10 @@ public abstract class RepositoryDtoToRepositoryMapper { @Mapping(target = "publicReadable", ignore = true) @Mapping(target = "healthCheckFailures", ignore = true) @Mapping(target = "permissions", ignore = true) - public abstract Repository map(RepositoryDto repositoryDto); + public abstract Repository map(RepositoryDto repositoryDto, @Context String id); + @AfterMapping + void updateId(@MappingTarget Repository repository, @Context String id) { + repository.setId(id); + } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java index f7fd3dab37..e6d8b80fd3 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java @@ -92,11 +92,10 @@ public class RepositoryResource { }) @TypeHint(TypeHint.NO_CONTENT.class) public Response update(@PathParam("namespace") String namespace, @PathParam("name") String name, RepositoryDto repositoryDto) { - return adapter.update(() -> manager.getByNamespace(namespace, name), existing -> { - Repository repository = dtoToRepositoryMapper.map(repositoryDto); - repository.setId(existing.getId()); - return repository; - }); + return adapter.update( + () -> manager.getByNamespace(namespace, name), + existing -> dtoToRepositoryMapper.map(repositoryDto, existing.getId()) + ); } @Path("tags/") From b6c618b0b0443c9b97cdb02622a7cd97e9d90930 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 5 Jul 2018 13:19:49 +0200 Subject: [PATCH 26/38] Verify that key values are not changed on update --- .../resources/IdResourceManagerAdapter.java | 6 +++++- .../api/v2/resources/RepositoryResource.java | 3 ++- .../SingleResourceManagerAdapter.java | 5 +++-- .../resources/RepositoryRootResourceTest.java | 21 +++++++++++++++++++ 4 files changed, 31 insertions(+), 4 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IdResourceManagerAdapter.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IdResourceManagerAdapter.java index c7bcc3b6b1..db5133dbf3 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IdResourceManagerAdapter.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IdResourceManagerAdapter.java @@ -34,7 +34,11 @@ class IdResourceManagerAdapter applyChanges) { - return singleAdapter.update(() -> manager.get(id), applyChanges); + return singleAdapter.update( + () -> manager.get(id), + applyChanges, + changed -> changed.getId().equals(id) + ); } public Response getAll(int page, int pageSize, String sortBy, boolean desc, Function, CollectionDto> mapToDto) { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java index e6d8b80fd3..ee251051b1 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java @@ -94,7 +94,8 @@ public class RepositoryResource { public Response update(@PathParam("namespace") String namespace, @PathParam("name") String name, RepositoryDto repositoryDto) { return adapter.update( () -> manager.getByNamespace(namespace, name), - existing -> dtoToRepositoryMapper.map(repositoryDto, existing.getId()) + existing -> dtoToRepositoryMapper.map(repositoryDto, existing.getId()), + changed -> changed.getName().equals(name) && changed.getNamespace().equals(namespace) ); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java index d5e7a97c51..547fd3654b 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java @@ -9,6 +9,7 @@ import javax.ws.rs.core.GenericEntity; import javax.ws.rs.core.Response; import java.util.Collection; import java.util.function.Function; +import java.util.function.Predicate; import java.util.function.Supplier; import static javax.ws.rs.core.Response.Status.BAD_REQUEST; @@ -48,13 +49,13 @@ class SingleResourceManagerAdapter reader, Function applyChanges) { + public Response update(Supplier reader, Function applyChanges, Predicate hasSameKey) { MODEL_OBJECT existingModelObject = reader.get(); if (existingModelObject == null) { return Response.status(Response.Status.NOT_FOUND).build(); } MODEL_OBJECT changedModelObject = applyChanges.apply(existingModelObject); - if (!getId(existingModelObject).equals(getId(changedModelObject))) { + if (!hasSameKey.test(changedModelObject)) { return Response.status(BAD_REQUEST).entity("illegal change of id").build(); } return update(getId(existingModelObject), changedModelObject); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java index c4ebf88c3d..c0fb0c0234 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java @@ -25,6 +25,7 @@ import java.net.URISyntaxException; import java.net.URL; import static java.util.Collections.singletonList; +import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT; import static javax.servlet.http.HttpServletResponse.SC_OK; @@ -33,6 +34,7 @@ import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyObject; import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; @@ -144,6 +146,25 @@ public class RepositoryRootResourceTest { verify(repositoryManager).modify(anyObject()); } + @Test + public void shouldHandleUpdateForExistingRepositoryForChangedNamespace() throws Exception { + mockRepository("wrong", "repo"); + + URL url = Resources.getResource("sonia/scm/api/v2/repository-test-update.json"); + byte[] repository = Resources.toByteArray(url); + + MockHttpRequest request = MockHttpRequest + .put("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "wrong/repo") + .contentType(VndMediaType.REPOSITORY) + .content(repository); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(SC_BAD_REQUEST, response.getStatus()); + verify(repositoryManager, never()).modify(anyObject()); + } + @Test public void shouldHandleDeleteForExistingRepository() throws Exception { mockRepository("space", "repo"); From 88029d23fe26953976cf232df37e87d1744a87f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 5 Jul 2018 17:23:19 +0200 Subject: [PATCH 27/38] Small cleanup --- .../v2/resources/IdResourceManagerAdapter.java | 18 ++++++++++++++---- .../api/v2/resources/RepositoryResource.java | 18 ++++++++++++++---- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IdResourceManagerAdapter.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IdResourceManagerAdapter.java index 64c5a063f5..be634a5fac 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IdResourceManagerAdapter.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IdResourceManagerAdapter.java @@ -8,10 +8,12 @@ import sonia.scm.PageResult; import javax.ws.rs.core.Response; import java.io.IOException; import java.util.function.Function; +import java.util.function.Predicate; import java.util.function.Supplier; /** - * Facade for {@link SingleResourceManagerAdapter} and {@link CollectionResourceManagerAdapter}. + * Facade for {@link SingleResourceManagerAdapter} and {@link CollectionResourceManagerAdapter} + * for model objects handled by a single id. */ @SuppressWarnings("squid:S00119") // "MODEL_OBJECT" is much more meaningful than "M", right? class IdResourceManagerAdapter mapToDto) { - return singleAdapter.get(() -> manager.get(id), mapToDto); + return singleAdapter.get(loadBy(id), mapToDto); } public Response update(String id, Function applyChanges) { return singleAdapter.update( - () -> manager.get(id), + loadBy(id), applyChanges, - changed -> changed.getId().equals(id) + idStaysTheSame(id) ); } @@ -52,4 +54,12 @@ class IdResourceManagerAdapter loadBy(String id) { + return () -> manager.get(id); + } + + private Predicate idStaysTheSame(String id) { + return changed -> changed.getId().equals(id); + } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java index 7fd3600c65..6e6d837830 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java @@ -18,6 +18,8 @@ import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.core.Response; +import java.util.function.Predicate; +import java.util.function.Supplier; public class RepositoryResource { @@ -63,7 +65,7 @@ public class RepositoryResource { @ResponseCode(code = 500, condition = "internal server error") }) public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name) { - return adapter.get(() -> manager.getByNamespace(namespace, name), repositoryToDtoMapper::map); + return adapter.get(loadBy(namespace, name), repositoryToDtoMapper::map); } @DELETE @@ -76,7 +78,7 @@ public class RepositoryResource { }) @TypeHint(TypeHint.NO_CONTENT.class) public Response delete(@PathParam("namespace") String namespace, @PathParam("name") String name) { - return adapter.delete(() -> manager.getByNamespace(namespace, name)); + return adapter.delete(loadBy(namespace, name)); } @PUT @@ -93,9 +95,9 @@ public class RepositoryResource { @TypeHint(TypeHint.NO_CONTENT.class) public Response update(@PathParam("namespace") String namespace, @PathParam("name") String name, RepositoryDto repositoryDto) { return adapter.update( - () -> manager.getByNamespace(namespace, name), + loadBy(namespace, name), existing -> dtoToRepositoryMapper.map(repositoryDto, existing.getId()), - changed -> changed.getName().equals(name) && changed.getNamespace().equals(namespace) + nameAndNamespaceStaysTheSame(namespace, name) ); } @@ -123,4 +125,12 @@ public class RepositoryResource { public PermissionRootResource permissions() { return permissionRootResource.get(); } + + private Supplier loadBy(String namespace, String name) { + return () -> manager.getByNamespace(namespace, name); + } + + private Predicate nameAndNamespaceStaysTheSame(String namespace, String name) { + return changed -> changed.getName().equals(name) && changed.getNamespace().equals(namespace); + } } From e1963d45dd196300abb6cb4d0337ad9e02d94d34 Mon Sep 17 00:00:00 2001 From: Johannes Schnatterer Date: Wed, 11 Jul 2018 12:03:04 +0200 Subject: [PATCH 28/38] Some polishing during review --- .../repository/AbstractSimpleRepositoryHandler.java | 1 + .../resources/CollectionResourceManagerAdapter.java | 5 +++-- .../sonia/scm/api/v2/resources/PermissionDto.java | 11 ----------- .../v2/resources/RepositoryCollectionResource.java | 5 ++--- .../v2/resources/SingleResourceManagerAdapter.java | 4 +++- .../api/v2/resources/GroupToGroupDtoMapperTest.java | 1 + .../RepositoryToRepositoryDtoMapperTest.java | 7 ++++--- .../scm/api/v2/resources/UserRootResourceTest.java | 2 +- .../scm/api/v2/resources/UserToUserDtoMapperTest.java | 1 + .../scm/repository/DefaultRepositoryManagerTest.java | 4 ---- 10 files changed, 16 insertions(+), 25 deletions(-) delete mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDto.java diff --git a/scm-core/src/main/java/sonia/scm/repository/AbstractSimpleRepositoryHandler.java b/scm-core/src/main/java/sonia/scm/repository/AbstractSimpleRepositoryHandler.java index fb044a325c..b49d4355b9 100644 --- a/scm-core/src/main/java/sonia/scm/repository/AbstractSimpleRepositoryHandler.java +++ b/scm-core/src/main/java/sonia/scm/repository/AbstractSimpleRepositoryHandler.java @@ -143,6 +143,7 @@ public abstract class AbstractSimpleRepositoryHandler The type of the model object, eg. {@link sonia.scm.user.User}. * @param The corresponding transport object, eg. {@link UserDto}. * @param The exception type for the model object, eg. {@link sonia.scm.user.UserException}. + * + * @see SingleResourceManagerAdapter */ @SuppressWarnings("squid:S00119") // "MODEL_OBJECT" is much more meaningful than "M", right? class CollectionResourceManagerAdapter dtoToRepositoryMapper.map(repositoryDto, null), repository -> resourceLinks.repository().self(repository.getNamespace(), repository.getName())); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java index 96c9d05d2c..6cd12c9dd6 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java @@ -15,13 +15,15 @@ import java.util.function.Supplier; import static javax.ws.rs.core.Response.Status.BAD_REQUEST; /** - * Adapter from resource http endpoints to managers. + * Adapter from resource http endpoints to managers, for Single resources (e.g. {@code /user/name}). * * Provides common CRUD operations and DTO to Model Object mapping to keep Resources more DRY. * * @param The type of the model object, eg. {@link sonia.scm.user.User}. * @param The corresponding transport object, eg. {@link UserDto}. * @param The exception type for the model object, eg. {@link sonia.scm.user.UserException}. + * + * @see CollectionResourceManagerAdapter */ @SuppressWarnings("squid:S00119") // "MODEL_OBJECT" is much more meaningful than "M", right? class SingleResourceManagerAdapter userCaptor = ArgumentCaptor.forClass(User.class); @Before - public void prepareEnvironment() throws IOException, UserException { + public void prepareEnvironment() throws UserException { initMocks(this); User dummyUser = createDummyUser("Neo"); when(userManager.create(userCaptor.capture())).thenAnswer(invocation -> invocation.getArguments()[0]); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserToUserDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserToUserDtoMapperTest.java index 0ac980df45..330dd2a89e 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserToUserDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserToUserDtoMapperTest.java @@ -23,6 +23,7 @@ import static org.mockito.MockitoAnnotations.initMocks; public class UserToUserDtoMapperTest { private final URI baseUri = URI.create("http://example.com/base/"); + @SuppressWarnings("unused") // Is injected private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); @InjectMocks diff --git a/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java index 0371b4974b..d057a97a84 100644 --- a/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java @@ -93,10 +93,6 @@ import static org.mockito.Mockito.when; ) public class DefaultRepositoryManagerTest extends ManagerTestBase { - { - ThreadContext.unbindSecurityManager(); - } - @Rule public ShiroRule shiro = new ShiroRule(); From ccf2708520614702aad8b285117a6ef820f53150 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 11 Jul 2018 13:03:46 +0200 Subject: [PATCH 29/38] Add javadoc for repository endpoint --- .../RepositoryCollectionResource.java | 23 +++++++++++++++- .../api/v2/resources/RepositoryResource.java | 27 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java index 1a90e3cf8d..590cd1324b 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java @@ -23,6 +23,8 @@ import java.io.IOException; public class RepositoryCollectionResource { + private static final int DEFAULT_PAGE_SIZE = 10; + private final CollectionResourceManagerAdapter adapter; private final RepositoryCollectionToDtoMapper repositoryCollectionToDtoMapper; private final RepositoryDtoToRepositoryMapper dtoToRepositoryMapper; @@ -36,6 +38,16 @@ public class RepositoryCollectionResource { this.resourceLinks = resourceLinks; } + /** + * Returns all repositories for a given page number with a given page size (default page size is {@value DEFAULT_PAGE_SIZE}). + * + * Note: This method requires "repository" privilege. + * + * @param page the number of the requested page + * @param pageSize the page size (default page size is {@value DEFAULT_PAGE_SIZE}) + * @param sortBy sort parameter (if empty - undefined sorting) + * @param desc sort direction desc or asc + */ @GET @Path("") @Produces(VndMediaType.REPOSITORY_COLLECTION) @@ -47,13 +59,22 @@ public class RepositoryCollectionResource { @ResponseCode(code = 500, condition = "internal server error") }) public Response getAll(@DefaultValue("0") @QueryParam("page") int page, - @DefaultValue("10") @QueryParam("pageSize") int pageSize, + @DefaultValue("" + DEFAULT_PAGE_SIZE) @QueryParam("pageSize") int pageSize, @QueryParam("sortBy") String sortBy, @DefaultValue("false") @QueryParam("desc") boolean desc) { return adapter.getAll(page, pageSize, sortBy, desc, pageResult -> repositoryCollectionToDtoMapper.map(page, pageSize, pageResult)); } + /** + * Creates a new repository. + * + * Note: This method requires "repository" privilege. The namespace of the given repository will + * be ignored and set by the configured namespace strategy. + * + * @param repositoryDto The repository to be created. + * @return A response with the link to the new repository (if created successfully). + */ @POST @Path("") @Consumes(VndMediaType.REPOSITORY) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java index 6e6d837830..1ff366cc0a 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java @@ -53,6 +53,15 @@ public class RepositoryResource { this.permissionRootResource = permissionRootResource; } + /** + * Returns a repository. + * + * Note: This method requires "repository" privilege. + * + * @param namespace the namespace of the repository + * @param name the name of the repository + * + */ @GET @Path("") @Produces(VndMediaType.REPOSITORY) @@ -68,6 +77,15 @@ public class RepositoryResource { return adapter.get(loadBy(namespace, name), repositoryToDtoMapper::map); } + /** + * Deletes a repository. + * + * Note: This method requires "repository" privilege. + * + * @param namespace the namespace of the repository to delete + * @param name the name of the repository to delete + * + */ @DELETE @Path("") @StatusCodes({ @@ -81,6 +99,15 @@ public class RepositoryResource { return adapter.delete(loadBy(namespace, name)); } + /** + * Modifies the given repository. + * + * Note: This method requires "repository" privilege. + * + * @param namespace the namespace of the repository to be modified + * @param name the name of the repository to be modified + * @param repositoryDto repository object to modify + */ @PUT @Path("") @Consumes(VndMediaType.REPOSITORY) From 617b98c6f30a03be3adf0acf094e40b45c377f18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 11 Jul 2018 13:42:09 +0200 Subject: [PATCH 30/38] Use optionals for get by name and namespace --- .../scm/repository/RepositoryManager.java | 6 ++-- .../resources/IdResourceManagerAdapter.java | 5 +-- .../api/v2/resources/RepositoryResource.java | 3 +- .../SingleResourceManagerAdapter.java | 32 ++++++++++--------- .../resources/RepositoryRootResourceTest.java | 7 +++- 5 files changed, 31 insertions(+), 22 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryManager.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryManager.java index ca1d5ab743..0edd21f4c2 100644 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryManager.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryManager.java @@ -41,6 +41,7 @@ import sonia.scm.TypeManager; import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.util.Collection; +import java.util.Optional; //~--- JDK imports ------------------------------------------------------------ @@ -148,11 +149,10 @@ public interface RepositoryManager @Override public RepositoryHandler getHandler(String type); - default Repository getByNamespace(String namespace, String name) { + default Optional getByNamespace(String namespace, String name) { return getAll() .stream() .filter(r -> r.getName().equals(name) && r.getNamespace().equals(namespace)) - .findFirst() - .orElse(null); + .findFirst(); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IdResourceManagerAdapter.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IdResourceManagerAdapter.java index be634a5fac..ded4bff309 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IdResourceManagerAdapter.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IdResourceManagerAdapter.java @@ -7,6 +7,7 @@ import sonia.scm.PageResult; import javax.ws.rs.core.Response; import java.io.IOException; +import java.util.Optional; import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; @@ -55,8 +56,8 @@ class IdResourceManagerAdapter loadBy(String id) { - return () -> manager.get(id); + private Supplier> loadBy(String id) { + return () -> Optional.ofNullable(manager.get(id)); } private Predicate idStaysTheSame(String id) { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java index 1ff366cc0a..5972508c78 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java @@ -18,6 +18,7 @@ import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.core.Response; +import java.util.Optional; import java.util.function.Predicate; import java.util.function.Supplier; @@ -153,7 +154,7 @@ public class RepositoryResource { return permissionRootResource.get(); } - private Supplier loadBy(String namespace, String name) { + private Supplier> loadBy(String namespace, String name) { return () -> manager.getByNamespace(namespace, name); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java index 6cd12c9dd6..7f8b115dee 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java @@ -8,6 +8,7 @@ import sonia.scm.api.rest.resources.AbstractManagerResource; import javax.ws.rs.core.GenericEntity; import javax.ws.rs.core.Response; import java.util.Collection; +import java.util.Optional; import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; @@ -38,34 +39,35 @@ class SingleResourceManagerAdapter reader, Function mapToDto) { - MODEL_OBJECT modelObject = reader.get(); - if (modelObject == null) { - return Response.status(Response.Status.NOT_FOUND).build(); - } - DTO dto = mapToDto.apply(modelObject); - return Response.ok(dto).build(); + Response get(Supplier> reader, Function mapToDto) { + return reader.get() + .map(mapToDto) + .map(Response::ok) + .map(Response.ResponseBuilder::build) + .orElse(Response.status(Response.Status.NOT_FOUND).build()); } /** * Update the model object for the given id according to the given function and returns a corresponding http response. * This handles all corner cases, eg. no matching object for the id or missing privileges. */ - public Response update(Supplier reader, Function applyChanges, Predicate hasSameKey) { - MODEL_OBJECT existingModelObject = reader.get(); - if (existingModelObject == null) { + public Response update(Supplier> reader, Function applyChanges, Predicate hasSameKey) { + Optional existingModelObject = reader.get(); + if (!existingModelObject.isPresent()) { return Response.status(Response.Status.NOT_FOUND).build(); } - MODEL_OBJECT changedModelObject = applyChanges.apply(existingModelObject); + MODEL_OBJECT changedModelObject = applyChanges.apply(existingModelObject.get()); if (!hasSameKey.test(changedModelObject)) { return Response.status(BAD_REQUEST).entity("illegal change of id").build(); } - return update(getId(existingModelObject), changedModelObject); + return update(getId(existingModelObject.get()), changedModelObject); } - public Response delete(Supplier reader) { - MODEL_OBJECT existingModelObject = reader.get(); - return delete(existingModelObject.getId()); + public Response delete(Supplier> reader) { + return reader.get() + .map(MODEL_OBJECT::getId) + .map(this::delete) + .orElse(null); } @Override diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java index c0fb0c0234..a0652708ce 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java @@ -25,6 +25,8 @@ import java.net.URISyntaxException; import java.net.URL; import static java.util.Collections.singletonList; +import static java.util.Optional.empty; +import static java.util.Optional.of; import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT; @@ -33,6 +35,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyObject; +import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -74,6 +77,7 @@ public class RepositoryRootResourceTest { @Test public void shouldFailForNotExistingRepository() throws URISyntaxException { + when(repositoryManager.getByNamespace(anyString(), anyString())).thenReturn(empty()); mockRepository("space", "repo"); MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/other"); @@ -115,6 +119,7 @@ public class RepositoryRootResourceTest { public void shouldHandleUpdateForNotExistingRepository() throws URISyntaxException, IOException { URL url = Resources.getResource("sonia/scm/api/v2/repository-test-update.json"); byte[] repository = Resources.toByteArray(url); + when(repositoryManager.getByNamespace(anyString(), anyString())).thenReturn(empty()); MockHttpRequest request = MockHttpRequest .put("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo") @@ -212,7 +217,7 @@ public class RepositoryRootResourceTest { repository.setName(name); String id = namespace + "-" + name; repository.setId(id); - when(repositoryManager.getByNamespace(namespace, name)).thenReturn(repository); + when(repositoryManager.getByNamespace(namespace, name)).thenReturn(of(repository)); when(repositoryManager.get(id)).thenReturn(repository); return repository; } From 667cc48e088358509c1a1b2fab942837722433ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 11 Jul 2018 13:47:41 +0200 Subject: [PATCH 31/38] Map properties of repositories --- .../sonia/scm/api/v2/resources/RepositoryDto.java | 2 ++ .../v2/resources/RepositoryRootResourceTest.java | 13 +++++++++++++ .../RepositoryToRepositoryDtoMapperTest.java | 10 ++++++++++ 3 files changed, 25 insertions(+) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java index fbef4ca16a..bcc8e16ebb 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java @@ -8,6 +8,7 @@ import lombok.Setter; import java.time.Instant; import java.util.List; +import java.util.Map; @Getter @Setter public class RepositoryDto extends HalRepresentation { @@ -22,6 +23,7 @@ public class RepositoryDto extends HalRepresentation { private String name; private boolean archived = false; private String type; + protected Map properties; @Override @SuppressWarnings("squid:S1185") // We want to have this method available in this package diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java index a0652708ce..24a11fbf09 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java @@ -101,6 +101,19 @@ public class RepositoryRootResourceTest { assertTrue(response.getContentAsString().contains("\"name\":\"repo\"")); } + @Test + public void shouldMapProperties() throws URISyntaxException { + Repository repository = mockRepository("space", "repo"); + repository.setProperty("testKey", "testValue"); + + MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo"); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertTrue(response.getContentAsString().contains("\"testKey\":\"testValue\"")); + } + @Test public void shouldGetAll() throws URISyntaxException { PageResult singletonPageResult = createSingletonPageResult(mockRepository("space", "repo")); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java index fd41291d9d..ad6ff3cae7 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java @@ -57,6 +57,16 @@ public class RepositoryToRepositoryDtoMapperTest { assertEquals("none@example.com", dto.getContact()); } + @Test + public void shouldMapPropertiesProperty() { + Repository repository = createTestRepository(); + repository.setProperty("testKey", "testValue"); + + RepositoryDto dto = mapper.map(repository); + + assertEquals("testValue", dto.getProperties().get("testKey")); + } + @Test @SubjectAware(username = "unpriv") public void shouldCreateLinksForUnprivilegedUser() { From b1ed50affba7219a15a96f8c0cf0de336f9df2bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 11 Jul 2018 14:56:18 +0200 Subject: [PATCH 32/38] Fix internal error when pushing an existing repository --- .../RepositoryAlreadyExistsExceptionMapper.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 scm-webapp/src/main/java/sonia/scm/api/rest/RepositoryAlreadyExistsExceptionMapper.java diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/RepositoryAlreadyExistsExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/rest/RepositoryAlreadyExistsExceptionMapper.java new file mode 100644 index 0000000000..e69b9e98d6 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/RepositoryAlreadyExistsExceptionMapper.java @@ -0,0 +1,16 @@ +package sonia.scm.api.rest; + +import sonia.scm.repository.RepositoryAlreadyExistsException; + +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +@Provider +public class RepositoryAlreadyExistsExceptionMapper implements ExceptionMapper { + @Override + public Response toResponse(RepositoryAlreadyExistsException exception) { + return Response.status(Status.CONFLICT).build(); + } +} From 8593e1dc5890c418c7bf191ee4a8bb61a76fd568 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 11 Jul 2018 15:06:11 +0200 Subject: [PATCH 33/38] Fix creation date set to null on modify --- .../main/java/sonia/scm/repository/DefaultRepositoryManager.java | 1 + 1 file changed, 1 insertion(+) diff --git a/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java b/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java index 877d228f87..ffcf3d8dc3 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java @@ -265,6 +265,7 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager { RepositoryPermissions.modify(oldRepository).check(); fireEvent(HandlerEventType.BEFORE_MODIFY, repository, oldRepository); repository.setLastModified(System.currentTimeMillis()); + repository.setCreationDate(oldRepository.getCreationDate()); getHandler(repository).modify(repository); repositoryDAO.modify(repository); fireEvent(HandlerEventType.MODIFY, repository, oldRepository); From 4e207713bf533afc09dd318bd7a1a5eb01520f7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 12 Jul 2018 10:15:32 +0200 Subject: [PATCH 34/38] Introduce ManagerDaoAdapter for create and modify --- .../src/main/java/sonia/scm/ModelObject.java | 8 +- .../group/GroupAlreadyExistsException.java | 10 +- .../scm/group/GroupNotFoundException.java | 2 +- .../RepositoryNotFoundException.java | 16 +-- .../api/RepositoryServiceFactory.java | 14 +-- .../scm/user/UserAlreadyExistsException.java | 12 +- .../sonia/scm/user/UserNotFoundException.java | 2 +- .../java/sonia/scm/ManagerDaoAdapter.java | 59 +++++++++ .../sonia/scm/group/DefaultGroupManager.java | 84 +++++-------- .../repository/DefaultRepositoryManager.java | 114 ++++++------------ .../sonia/scm/user/DefaultUserManager.java | 67 ++++------ .../AbstractManagerResourceTest.java | 15 +++ 12 files changed, 185 insertions(+), 218 deletions(-) create mode 100644 scm-webapp/src/main/java/sonia/scm/ManagerDaoAdapter.java diff --git a/scm-core/src/main/java/sonia/scm/ModelObject.java b/scm-core/src/main/java/sonia/scm/ModelObject.java index 76ba021e67..cca9608ceb 100644 --- a/scm-core/src/main/java/sonia/scm/ModelObject.java +++ b/scm-core/src/main/java/sonia/scm/ModelObject.java @@ -53,5 +53,11 @@ public interface ModelObject * * @return unique id */ - public String getId(); + String getId(); + + void setLastModified(Long timestamp); + + Long getCreationDate(); + + void setCreationDate(Long timestamp); } diff --git a/scm-core/src/main/java/sonia/scm/group/GroupAlreadyExistsException.java b/scm-core/src/main/java/sonia/scm/group/GroupAlreadyExistsException.java index e63ee1d78b..2b3c73535e 100644 --- a/scm-core/src/main/java/sonia/scm/group/GroupAlreadyExistsException.java +++ b/scm-core/src/main/java/sonia/scm/group/GroupAlreadyExistsException.java @@ -42,15 +42,9 @@ package sonia.scm.group; public class GroupAlreadyExistsException extends GroupException { - /** Field description */ private static final long serialVersionUID = 4042878550219750430L; - /** - * Constructs a new instance. - * - * @param name The name (aka id) of the group - */ - public GroupAlreadyExistsException(String name) { - super(name + " group already exists"); + public GroupAlreadyExistsException(Group group) { + super(group.getName() + " group already exists"); } } diff --git a/scm-core/src/main/java/sonia/scm/group/GroupNotFoundException.java b/scm-core/src/main/java/sonia/scm/group/GroupNotFoundException.java index d4c06a1559..d1769da9e9 100644 --- a/scm-core/src/main/java/sonia/scm/group/GroupNotFoundException.java +++ b/scm-core/src/main/java/sonia/scm/group/GroupNotFoundException.java @@ -53,6 +53,6 @@ public class GroupNotFoundException extends GroupException * */ public GroupNotFoundException() { - super("group does not exists"); + super("group does not exist"); } } diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryNotFoundException.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryNotFoundException.java index f1649efb43..4160fb1424 100644 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryNotFoundException.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryNotFoundException.java @@ -52,17 +52,11 @@ public class RepositoryNotFoundException extends RepositoryException * error detail message. * */ - public RepositoryNotFoundException() {} + public RepositoryNotFoundException() { + super("repository does not exist"); + } - /** - * Constructs a new {@link RepositoryNotFoundException} with the specified - * error detail message. - * - * - * @param message error detail message - */ - public RepositoryNotFoundException(String message) - { - super(message); + public RepositoryNotFoundException(String repositoryId) { + super("repository with id " + repositoryId + " does not exist"); } } diff --git a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java index 5bd737a85a..315156db0d 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java @@ -37,20 +37,19 @@ package sonia.scm.repository.api; import com.github.legman.ReferenceType; import com.github.legman.Subscribe; - import com.google.common.base.Preconditions; import com.google.common.base.Strings; import com.google.common.collect.Sets; import com.google.inject.Inject; import com.google.inject.Singleton; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.HandlerEventType; import sonia.scm.cache.Cache; import sonia.scm.cache.CacheManager; import sonia.scm.config.ScmConfiguration; +import sonia.scm.event.ScmEventBus; +import sonia.scm.repository.ClearRepositoryCacheEvent; import sonia.scm.repository.PostReceiveRepositoryHookEvent; import sonia.scm.repository.PreProcessorUtil; import sonia.scm.repository.Repository; @@ -63,11 +62,9 @@ import sonia.scm.repository.spi.RepositoryServiceProvider; import sonia.scm.repository.spi.RepositoryServiceResolver; import sonia.scm.security.ScmSecurityException; -//~--- JDK imports ------------------------------------------------------------ - import java.util.Set; -import sonia.scm.event.ScmEventBus; -import sonia.scm.repository.ClearRepositoryCacheEvent; + +//~--- JDK imports ------------------------------------------------------------ /** * The {@link RepositoryServiceFactory} is the entrypoint of the repository api. @@ -179,8 +176,7 @@ public final class RepositoryServiceFactory if (repository == null) { - throw new RepositoryNotFoundException( - "could not find a repository with id ".concat(repositoryId)); + throw new RepositoryNotFoundException(repositoryId); } return create(repository); diff --git a/scm-core/src/main/java/sonia/scm/user/UserAlreadyExistsException.java b/scm-core/src/main/java/sonia/scm/user/UserAlreadyExistsException.java index 52b91fa229..1c77cb4f86 100644 --- a/scm-core/src/main/java/sonia/scm/user/UserAlreadyExistsException.java +++ b/scm-core/src/main/java/sonia/scm/user/UserAlreadyExistsException.java @@ -41,19 +41,11 @@ package sonia.scm.user; public class UserAlreadyExistsException extends UserException { - /** Field description */ private static final long serialVersionUID = 9182294539718090814L; //~--- constructors --------------------------------------------------------- - /** - * Constructs a new instance. - * - * @param name The name (aka id) of the user - * @since 1.5 - */ - public UserAlreadyExistsException(String name) - { - super(name + " user already exists"); + public UserAlreadyExistsException(User user) { + super(user.getName() + " user already exists"); } } diff --git a/scm-core/src/main/java/sonia/scm/user/UserNotFoundException.java b/scm-core/src/main/java/sonia/scm/user/UserNotFoundException.java index e5755c2546..0f11615987 100644 --- a/scm-core/src/main/java/sonia/scm/user/UserNotFoundException.java +++ b/scm-core/src/main/java/sonia/scm/user/UserNotFoundException.java @@ -52,6 +52,6 @@ public class UserNotFoundException extends UserException * */ public UserNotFoundException() { - super("user does not exists"); + super("user does not exist"); } } diff --git a/scm-webapp/src/main/java/sonia/scm/ManagerDaoAdapter.java b/scm-webapp/src/main/java/sonia/scm/ManagerDaoAdapter.java new file mode 100644 index 0000000000..0f26a691b3 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/ManagerDaoAdapter.java @@ -0,0 +1,59 @@ +package sonia.scm; + +import com.github.sdorra.ssp.PermissionCheck; +import sonia.scm.util.AssertUtil; + +import java.util.function.Function; +import java.util.function.Supplier; + +public class ManagerDaoAdapter { + + private final GenericDAO dao; + private final Supplier notFoundException; + private final Function alreadyExistsException; + + public ManagerDaoAdapter(GenericDAO dao, Supplier notFoundException, Function alreadyExistsException) { + this.dao = dao; + this.notFoundException = notFoundException; + this.alreadyExistsException = alreadyExistsException; + } + + public void modify(T object, Function permissionCheck, AroundHandler beforeUpdate, AroundHandler afterUpdate) throws E { + String name = object.getId(); + + T notModified = dao.get(name); + if (notModified != null) { + permissionCheck.apply(notModified).check(); + AssertUtil.assertIsValid(object); + + beforeUpdate.handle(notModified); + + object.setLastModified(System.currentTimeMillis()); + object.setCreationDate(notModified.getCreationDate()); + + dao.modify(object); + + afterUpdate.handle(notModified); + } else { + throw notFoundException.get(); + } + } + + public T create(T newObject, Supplier permissionCheck, AroundHandler beforeCreate, AroundHandler afterCreate) throws E { + permissionCheck.get().check(); + AssertUtil.assertIsValid(newObject); + if (dao.contains(newObject)) { + throw alreadyExistsException.apply(newObject); + } + newObject.setCreationDate(System.currentTimeMillis()); + beforeCreate.handle(newObject); + dao.add(newObject); + afterCreate.handle(newObject); + return newObject; + } + + @FunctionalInterface + public interface AroundHandler { + void handle(T notModified) throws E; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupManager.java b/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupManager.java index ed1d7723b2..5c6681cc30 100644 --- a/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupManager.java +++ b/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupManager.java @@ -43,6 +43,7 @@ import com.google.inject.Singleton; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.HandlerEventType; +import sonia.scm.ManagerDaoAdapter; import sonia.scm.SCMContextProvider; import sonia.scm.TransformFilter; import sonia.scm.search.SearchRequest; @@ -84,6 +85,10 @@ public class DefaultGroupManager extends AbstractGroupManager public DefaultGroupManager(GroupDAO groupDAO) { this.groupDAO = groupDAO; + this.managerDaoAdapter = new ManagerDaoAdapter<>( + groupDAO, + GroupNotFoundException::new, + GroupAlreadyExistsException::new); } //~--- methods -------------------------------------------------------------- @@ -101,46 +106,23 @@ public class DefaultGroupManager extends AbstractGroupManager // do nothing } - /** - * Method description - * - * - * @param group - * - * @throws GroupException - * @throws IOException - */ @Override - public Group create(Group group) throws GroupException - { + public Group create(Group group) throws GroupException { String type = group.getType(); - - if (Util.isEmpty(type)) - { + if (Util.isEmpty(type)) { group.setType(groupDAO.getType()); } - String name = group.getName(); - - if (logger.isInfoEnabled()) - { - logger.info("create group {} of type {}", name, - group.getType()); - } - - GroupPermissions.create().check(); - - if (groupDAO.contains(name)) - { - throw new GroupAlreadyExistsException(name); - } + logger.info("create group {} of type {}", group.getName(), group.getType()); removeDuplicateMembers(group); - group.setCreationDate(System.currentTimeMillis()); - fireEvent(HandlerEventType.BEFORE_CREATE, group); - groupDAO.add(group); - fireEvent(HandlerEventType.CREATE, group); - return group; + + return managerDaoAdapter.create( + group, + GroupPermissions::create, + newGroup -> fireEvent(HandlerEventType.BEFORE_CREATE, newGroup), + newGroup -> fireEvent(HandlerEventType.CREATE, newGroup) + ); } /** @@ -195,31 +177,18 @@ public class DefaultGroupManager extends AbstractGroupManager * @throws IOException */ @Override - public void modify(Group group) throws GroupException - { - if (logger.isInfoEnabled()) - { - logger.info("modify group {} of type {}", group.getName(), - group.getType()); - } + public void modify(Group group) throws GroupException { + logger.info("modify group {} of type {}", group.getName(), group.getType()); - String name = group.getName(); - GroupPermissions.modify().check(name); - - Group notModified = groupDAO.get(name); - if (notModified != null) - { - removeDuplicateMembers(group); - fireEvent(HandlerEventType.BEFORE_MODIFY, group, notModified); - group.setLastModified(System.currentTimeMillis()); - group.setCreationDate(notModified.getCreationDate()); - groupDAO.modify(group); - fireEvent(HandlerEventType.MODIFY, group, notModified); - } - else - { - throw new GroupNotFoundException(); - } + managerDaoAdapter.modify( + group, + GroupPermissions::modify, + notModified -> { + removeDuplicateMembers(group); + fireEvent(HandlerEventType.BEFORE_MODIFY, group, notModified); + }, + notModified -> fireEvent(HandlerEventType.MODIFY, group, notModified) + ); } /** @@ -458,4 +427,5 @@ public class DefaultGroupManager extends AbstractGroupManager /** Field description */ private GroupDAO groupDAO; + private final ManagerDaoAdapter managerDaoAdapter; } diff --git a/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java b/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java index ffcf3d8dc3..13f0ce79c3 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java @@ -45,6 +45,7 @@ import org.slf4j.LoggerFactory; import sonia.scm.ArgumentIsInvalidException; import sonia.scm.ConfigurationException; import sonia.scm.HandlerEventType; +import sonia.scm.ManagerDaoAdapter; import sonia.scm.SCMContextProvider; import sonia.scm.Type; import sonia.scm.config.ScmConfiguration; @@ -90,6 +91,7 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager { private final Set types; private RepositoryMatcher repositoryMatcher; private NamespaceStrategy namespaceStrategy; + private final ManagerDaoAdapter managerDaoAdapter; @Inject @@ -116,6 +118,10 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager { for (RepositoryHandler handler : handlerSet) { addHandler(contextProvider, handler); } + managerDaoAdapter = new ManagerDaoAdapter<>( + repositoryDAO, + RepositoryNotFoundException::new, + RepositoryAlreadyExistsException::create); } @@ -128,55 +134,28 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager { } } - /** - * Method description - * - * - * @param repository - * @param initRepository - * - * @throws IOException - * @throws RepositoryException - */ - public Repository create(Repository repository, boolean initRepository) - throws RepositoryException { - logger.info("create repository {} of type {}", repository.getName(), - repository.getType()); - - RepositoryPermissions.create().check(); - AssertUtil.assertIsValid(repository); - - if (repositoryDAO.contains(repository)) { - throw RepositoryAlreadyExistsException.create(repository); - } - - repository.setId(keyGenerator.createKey()); - repository.setCreationDate(System.currentTimeMillis()); - repository.setNamespace(namespaceStrategy.getNamespace()); - - if (initRepository) { - getHandler(repository).create(repository); - } - - fireEvent(HandlerEventType.BEFORE_CREATE, repository); - repositoryDAO.add(repository); - fireEvent(HandlerEventType.CREATE, repository); - return repository; + @Override + public Repository create(Repository repository) throws RepositoryException { + return create(repository, true); } - /** - * Method description - * - * - * @param repository - * - * @throws IOException - * @throws RepositoryException - */ - @Override - public Repository create(Repository repository) - throws RepositoryException { - return create(repository, true); + public Repository create(Repository repository, boolean initRepository) throws RepositoryException { + repository.setId(keyGenerator.createKey()); + repository.setNamespace(namespaceStrategy.getNamespace()); + + logger.info("create repository {} of type {} in namespace {}", repository.getName(), repository.getType(), repository.getNamespace()); + + return managerDaoAdapter.create( + repository, + RepositoryPermissions::create, + newRepository -> { + if (initRepository) { + getHandler(newRepository).create(newRepository); + } + fireEvent(HandlerEventType.BEFORE_CREATE, newRepository); + }, + newRepository -> fireEvent(HandlerEventType.CREATE, newRepository) + ); } /** @@ -209,8 +188,7 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager { repositoryDAO.delete(repository); fireEvent(HandlerEventType.DELETE, repository); } else { - throw new RepositoryNotFoundException( - "repository ".concat(repository.getName()).concat(" not found")); + throw new RepositoryNotFoundException(); } } @@ -249,30 +227,18 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager { * @throws RepositoryException */ @Override - public void modify(Repository repository) - throws RepositoryException { - if (logger.isInfoEnabled()) { - logger.info("modify repository {} of type {}", repository.getName(), - repository.getType()); - } + public void modify(Repository repository) throws RepositoryException { + logger.info("modify repository {} of type {}", repository.getName(), repository.getType()); - AssertUtil.assertIsValid(repository); - - Repository oldRepository = repositoryDAO.get(repository.getType(), - repository.getName()); - - if (oldRepository != null) { - RepositoryPermissions.modify(oldRepository).check(); - fireEvent(HandlerEventType.BEFORE_MODIFY, repository, oldRepository); - repository.setLastModified(System.currentTimeMillis()); - repository.setCreationDate(oldRepository.getCreationDate()); - getHandler(repository).modify(repository); - repositoryDAO.modify(repository); - fireEvent(HandlerEventType.MODIFY, repository, oldRepository); - } else { - throw new RepositoryNotFoundException( - "repository ".concat(repository.getName()).concat(" not found")); - } + managerDaoAdapter.modify( + repository, + RepositoryPermissions::modify, + notModified -> { + fireEvent(HandlerEventType.BEFORE_MODIFY, repository, notModified); + getHandler(repository).modify(repository); + }, + notModified -> fireEvent(HandlerEventType.MODIFY, repository, notModified) + ); } /** @@ -296,8 +262,7 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager { if (fresh != null) { fresh.copyProperties(repository); } else { - throw new RepositoryNotFoundException( - "repository ".concat(repository.getName()).concat(" not found")); + throw new RepositoryNotFoundException(); } } @@ -638,5 +603,4 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager { return handler; } - } diff --git a/scm-webapp/src/main/java/sonia/scm/user/DefaultUserManager.java b/scm-webapp/src/main/java/sonia/scm/user/DefaultUserManager.java index 5a13a7fef8..2b63a33a89 100644 --- a/scm-webapp/src/main/java/sonia/scm/user/DefaultUserManager.java +++ b/scm-webapp/src/main/java/sonia/scm/user/DefaultUserManager.java @@ -41,11 +41,11 @@ import com.google.inject.Singleton; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.HandlerEventType; +import sonia.scm.ManagerDaoAdapter; import sonia.scm.SCMContextProvider; import sonia.scm.TransformFilter; import sonia.scm.search.SearchRequest; import sonia.scm.search.SearchUtil; -import sonia.scm.util.AssertUtil; import sonia.scm.util.CollectionAppender; import sonia.scm.util.IOUtil; import sonia.scm.util.Util; @@ -96,6 +96,10 @@ public class DefaultUserManager extends AbstractUserManager public DefaultUserManager(UserDAO userDAO) { this.userDAO = userDAO; + this.managerDaoAdapter = new ManagerDaoAdapter<>( + userDAO, + UserNotFoundException::new, + UserAlreadyExistsException::new); } //~--- methods -------------------------------------------------------------- @@ -137,33 +141,20 @@ public class DefaultUserManager extends AbstractUserManager * @throws UserException */ @Override - public User create(User user) throws UserException - { + public User create(User user) throws UserException { String type = user.getType(); - - if (Util.isEmpty(type)) - { + if (Util.isEmpty(type)) { user.setType(userDAO.getType()); } - if (logger.isInfoEnabled()) - { - logger.info("create user {} of type {}", user.getName(), user.getType()); - } + logger.info("create user {} of type {}", user.getName(), user.getType()); - UserPermissions.create().check(); - - if (userDAO.contains(user.getName())) - { - throw new UserAlreadyExistsException(user.getName()); - } - - AssertUtil.assertIsValid(user); - user.setCreationDate(System.currentTimeMillis()); - fireEvent(HandlerEventType.BEFORE_CREATE, user); - userDAO.add(user); - fireEvent(HandlerEventType.CREATE, user); - return user; + return managerDaoAdapter.create( + user, + UserPermissions::create, + newUser -> fireEvent(HandlerEventType.BEFORE_CREATE, newUser), + newUser -> fireEvent(HandlerEventType.CREATE, newUser) + ); } /** @@ -227,27 +218,13 @@ public class DefaultUserManager extends AbstractUserManager @Override public void modify(User user) throws UserException { - String name = user.getName(); - if (logger.isInfoEnabled()) - { - logger.info("modify user {} of type {}", user.getName(), user.getType()); - } - - UserPermissions.modify(user).check(); - User notModified = userDAO.get(name); - if (notModified != null) - { - AssertUtil.assertIsValid(user); - fireEvent(HandlerEventType.BEFORE_MODIFY, user, notModified); - user.setLastModified(System.currentTimeMillis()); - user.setCreationDate(notModified.getCreationDate()); - userDAO.modify(user); - fireEvent(HandlerEventType.MODIFY, user, notModified); - } - else - { - throw new UserNotFoundException(); - } + logger.info("modify user {} of type {}", user.getName(), user.getType()); + + managerDaoAdapter.modify( + user, + UserPermissions::modify, + notModified -> fireEvent(HandlerEventType.BEFORE_MODIFY, user, notModified), + notModified -> fireEvent(HandlerEventType.MODIFY, user, notModified)); } /** @@ -497,6 +474,6 @@ public class DefaultUserManager extends AbstractUserManager //~--- fields --------------------------------------------------------------- - /** Field description */ private final UserDAO userDAO; + private final ManagerDaoAdapter managerDaoAdapter; } diff --git a/scm-webapp/src/test/java/sonia/scm/api/rest/resources/AbstractManagerResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/rest/resources/AbstractManagerResourceTest.java index e8421bc417..b3e931188a 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/rest/resources/AbstractManagerResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/rest/resources/AbstractManagerResourceTest.java @@ -105,6 +105,21 @@ public class AbstractManagerResourceTest { return id; } + @Override + public void setLastModified(Long timestamp) { + + } + + @Override + public Long getCreationDate() { + return null; + } + + @Override + public void setCreationDate(Long timestamp) { + + } + @Override public Long getLastModified() { return null; From 42543f6a47e5f0c4be22442ed901729339df8ddc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 12 Jul 2018 10:20:16 +0200 Subject: [PATCH 35/38] Create NotFoundExceptions with id fields --- .../java/sonia/scm/group/GroupNotFoundException.java | 4 ++-- .../scm/repository/RepositoryNotFoundException.java | 4 ++-- .../java/sonia/scm/user/UserNotFoundException.java | 4 ++-- .../src/main/java/sonia/scm/ManagerDaoAdapter.java | 10 ++++------ .../main/java/sonia/scm/group/DefaultGroupManager.java | 4 ++-- .../sonia/scm/repository/DefaultRepositoryManager.java | 4 ++-- .../main/java/sonia/scm/user/DefaultUserManager.java | 4 ++-- 7 files changed, 16 insertions(+), 18 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/group/GroupNotFoundException.java b/scm-core/src/main/java/sonia/scm/group/GroupNotFoundException.java index d1769da9e9..2ea5d16cf0 100644 --- a/scm-core/src/main/java/sonia/scm/group/GroupNotFoundException.java +++ b/scm-core/src/main/java/sonia/scm/group/GroupNotFoundException.java @@ -52,7 +52,7 @@ public class GroupNotFoundException extends GroupException * Constructs a new GroupNotFoundException. * */ - public GroupNotFoundException() { - super("group does not exist"); + public GroupNotFoundException(Group group) { + super("group " + group.getName() + " does not exist"); } } diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryNotFoundException.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryNotFoundException.java index 4160fb1424..11f1a98bfd 100644 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryNotFoundException.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryNotFoundException.java @@ -52,8 +52,8 @@ public class RepositoryNotFoundException extends RepositoryException * error detail message. * */ - public RepositoryNotFoundException() { - super("repository does not exist"); + public RepositoryNotFoundException(Repository repository) { + super("repository " + repository.getName() + "/" + repository.getNamespace() + " does not exist"); } public RepositoryNotFoundException(String repositoryId) { diff --git a/scm-core/src/main/java/sonia/scm/user/UserNotFoundException.java b/scm-core/src/main/java/sonia/scm/user/UserNotFoundException.java index 0f11615987..82b1c2abb8 100644 --- a/scm-core/src/main/java/sonia/scm/user/UserNotFoundException.java +++ b/scm-core/src/main/java/sonia/scm/user/UserNotFoundException.java @@ -51,7 +51,7 @@ public class UserNotFoundException extends UserException * Constructs a new UserNotFoundException. * */ - public UserNotFoundException() { - super("user does not exist"); + public UserNotFoundException(User user) { + super("user " + user.getName() + " does not exist"); } } diff --git a/scm-webapp/src/main/java/sonia/scm/ManagerDaoAdapter.java b/scm-webapp/src/main/java/sonia/scm/ManagerDaoAdapter.java index 0f26a691b3..aa27425283 100644 --- a/scm-webapp/src/main/java/sonia/scm/ManagerDaoAdapter.java +++ b/scm-webapp/src/main/java/sonia/scm/ManagerDaoAdapter.java @@ -9,19 +9,17 @@ import java.util.function.Supplier; public class ManagerDaoAdapter { private final GenericDAO dao; - private final Supplier notFoundException; + private final Function notFoundException; private final Function alreadyExistsException; - public ManagerDaoAdapter(GenericDAO dao, Supplier notFoundException, Function alreadyExistsException) { + public ManagerDaoAdapter(GenericDAO dao, Function notFoundException, Function alreadyExistsException) { this.dao = dao; this.notFoundException = notFoundException; this.alreadyExistsException = alreadyExistsException; } public void modify(T object, Function permissionCheck, AroundHandler beforeUpdate, AroundHandler afterUpdate) throws E { - String name = object.getId(); - - T notModified = dao.get(name); + T notModified = dao.get(object.getId()); if (notModified != null) { permissionCheck.apply(notModified).check(); AssertUtil.assertIsValid(object); @@ -35,7 +33,7 @@ public class ManagerDaoAdapter { afterUpdate.handle(notModified); } else { - throw notFoundException.get(); + throw notFoundException.apply(object); } } diff --git a/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupManager.java b/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupManager.java index 5c6681cc30..b083f9adfe 100644 --- a/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupManager.java +++ b/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupManager.java @@ -154,7 +154,7 @@ public class DefaultGroupManager extends AbstractGroupManager } else { - throw new GroupNotFoundException(); + throw new GroupNotFoundException(group); } } @@ -214,7 +214,7 @@ public class DefaultGroupManager extends AbstractGroupManager if (fresh == null) { - throw new GroupNotFoundException(); + throw new GroupNotFoundException(group); } fresh.copyProperties(group); diff --git a/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java b/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java index 13f0ce79c3..cfa21b1adc 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java @@ -188,7 +188,7 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager { repositoryDAO.delete(repository); fireEvent(HandlerEventType.DELETE, repository); } else { - throw new RepositoryNotFoundException(); + throw new RepositoryNotFoundException(repository); } } @@ -262,7 +262,7 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager { if (fresh != null) { fresh.copyProperties(repository); } else { - throw new RepositoryNotFoundException(); + throw new RepositoryNotFoundException(repository); } } diff --git a/scm-webapp/src/main/java/sonia/scm/user/DefaultUserManager.java b/scm-webapp/src/main/java/sonia/scm/user/DefaultUserManager.java index 2b63a33a89..81e3918ccb 100644 --- a/scm-webapp/src/main/java/sonia/scm/user/DefaultUserManager.java +++ b/scm-webapp/src/main/java/sonia/scm/user/DefaultUserManager.java @@ -185,7 +185,7 @@ public class DefaultUserManager extends AbstractUserManager } else { - throw new UserNotFoundException(); + throw new UserNotFoundException(user); } } @@ -249,7 +249,7 @@ public class DefaultUserManager extends AbstractUserManager if (fresh == null) { - throw new UserNotFoundException(); + throw new UserNotFoundException(user); } fresh.copyProperties(user); From 9131764fbb715433f47e66ba2954b6bc5293011e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 12 Jul 2018 10:30:33 +0200 Subject: [PATCH 36/38] Implement delete in ManagerDaoAdapter --- .../java/sonia/scm/ManagerDaoAdapter.java | 11 +++++ .../sonia/scm/group/DefaultGroupManager.java | 38 ++++------------ .../repository/DefaultRepositoryManager.java | 43 ++++++------------- .../sonia/scm/user/DefaultUserManager.java | 37 ++++------------ 4 files changed, 41 insertions(+), 88 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/ManagerDaoAdapter.java b/scm-webapp/src/main/java/sonia/scm/ManagerDaoAdapter.java index aa27425283..b94ce934f6 100644 --- a/scm-webapp/src/main/java/sonia/scm/ManagerDaoAdapter.java +++ b/scm-webapp/src/main/java/sonia/scm/ManagerDaoAdapter.java @@ -50,6 +50,17 @@ public class ManagerDaoAdapter { return newObject; } + public void delete(T toDelete, Supplier permissionCheck, AroundHandler beforeDelete, AroundHandler afterDelete) throws E { + permissionCheck.get().check(); + if (dao.contains(toDelete)) { + beforeDelete.handle(toDelete); + dao.delete(toDelete); + afterDelete.handle(toDelete); + } else { + throw notFoundException.apply(toDelete); + } + } + @FunctionalInterface public interface AroundHandler { void handle(T notModified) throws E; diff --git a/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupManager.java b/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupManager.java index b083f9adfe..4f39fd3196 100644 --- a/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupManager.java +++ b/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupManager.java @@ -125,37 +125,15 @@ public class DefaultGroupManager extends AbstractGroupManager ); } - /** - * Method description - * - * - * @param group - * - * @throws GroupException - * @throws IOException - */ @Override - public void delete(Group group) throws GroupException - { - if (logger.isInfoEnabled()) - { - logger.info("delete group {} of type {}", group.getName(), - group.getType()); - } - - String name = group.getName(); - GroupPermissions.delete().check(name); - - if (groupDAO.contains(name)) - { - fireEvent(HandlerEventType.BEFORE_DELETE, group); - groupDAO.delete(group); - fireEvent(HandlerEventType.DELETE, group); - } - else - { - throw new GroupNotFoundException(group); - } + public void delete(Group group) throws GroupException { + logger.info("delete group {} of type {}", group.getName(), group.getType()); + managerDaoAdapter.delete( + group, + () -> GroupPermissions.delete(group.getName()), + toDelete -> fireEvent(HandlerEventType.BEFORE_DELETE, toDelete), + toDelete -> fireEvent(HandlerEventType.DELETE, toDelete) + ); } /** diff --git a/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java b/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java index cfa21b1adc..7a078ddfa5 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java @@ -158,38 +158,23 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager { ); } - /** - * Method description - * - * - * @param repository - * - * @throws IOException - * @throws RepositoryException - */ @Override - public void delete(Repository repository) - throws RepositoryException { - if (logger.isInfoEnabled()) { - logger.info("delete repository {} of type {}", repository.getName(), - repository.getType()); - } + public void delete(Repository repository) throws RepositoryException { + logger.info("delete repository {} of type {}", repository.getName(), repository.getType()); + managerDaoAdapter.delete( + repository, + () -> RepositoryPermissions.delete(repository), + this::preDelete, + toDelete -> fireEvent(HandlerEventType.DELETE, toDelete) + ); + } - RepositoryPermissions.delete(repository).check(); - - if (configuration.isEnableRepositoryArchive() && !repository.isArchived()) { - throw new RepositoryIsNotArchivedException( - "Repository could not deleted, because it is not archived."); - } - - if (repositoryDAO.contains(repository)) { - fireEvent(HandlerEventType.BEFORE_DELETE, repository); - getHandler(repository).delete(repository); - repositoryDAO.delete(repository); - fireEvent(HandlerEventType.DELETE, repository); - } else { - throw new RepositoryNotFoundException(repository); + private void preDelete(Repository toDelete) throws RepositoryException { + if (configuration.isEnableRepositoryArchive() && !toDelete.isArchived()) { + throw new RepositoryIsNotArchivedException("Repository could not deleted, because it is not archived."); } + fireEvent(HandlerEventType.BEFORE_DELETE, toDelete); + getHandler(toDelete).delete(toDelete); } /** diff --git a/scm-webapp/src/main/java/sonia/scm/user/DefaultUserManager.java b/scm-webapp/src/main/java/sonia/scm/user/DefaultUserManager.java index 81e3918ccb..c59ec40d4f 100644 --- a/scm-webapp/src/main/java/sonia/scm/user/DefaultUserManager.java +++ b/scm-webapp/src/main/java/sonia/scm/user/DefaultUserManager.java @@ -157,36 +157,15 @@ public class DefaultUserManager extends AbstractUserManager ); } - /** - * Method description - * - * - * @param user - * - * @throws IOException - * @throws UserException - */ @Override - public void delete(User user) throws UserException - { - if (logger.isInfoEnabled()) - { - logger.info("delete user {} of type {}", user.getName(), user.getType()); - } - - String name = user.getName(); - UserPermissions.delete(name).check(); - - if (userDAO.contains(name)) - { - fireEvent(HandlerEventType.BEFORE_DELETE, user); - userDAO.delete(user); - fireEvent(HandlerEventType.DELETE, user); - } - else - { - throw new UserNotFoundException(user); - } + public void delete(User user) throws UserException { + logger.info("delete user {} of type {}", user.getName(), user.getType()); + managerDaoAdapter.delete( + user, + () -> UserPermissions.delete(user.getName()), + toDelete -> fireEvent(HandlerEventType.BEFORE_DELETE, toDelete), + toDelete -> fireEvent(HandlerEventType.DELETE, toDelete) + ); } /** From 76ad3dfc897d59c270edfa21103fbe16e6047acb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 12 Jul 2018 11:00:32 +0200 Subject: [PATCH 37/38] Fix unit test for delete with archive enabled --- .../DefaultRepositoryManagerTest.java | 154 ++++++------------ 1 file changed, 47 insertions(+), 107 deletions(-) diff --git a/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java index d057a97a84..2c8c6879f4 100644 --- a/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java @@ -37,7 +37,6 @@ import com.github.sdorra.shiro.ShiroRule; import com.github.sdorra.shiro.SubjectAware; import com.google.common.collect.ImmutableSet; import org.apache.shiro.authz.UnauthorizedException; -import org.apache.shiro.util.ThreadContext; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; @@ -45,7 +44,9 @@ import org.mockito.invocation.InvocationOnMock; import sonia.scm.HandlerEventType; import sonia.scm.Manager; import sonia.scm.ManagerTestBase; +import sonia.scm.ModelObject; import sonia.scm.Type; +import sonia.scm.TypedObject; import sonia.scm.config.ScmConfiguration; import sonia.scm.event.ScmEventBus; import sonia.scm.repository.api.HookContext; @@ -58,7 +59,6 @@ import sonia.scm.security.KeyGenerator; import sonia.scm.store.ConfigurationStoreFactory; import sonia.scm.store.JAXBConfigurationStoreFactory; -import java.io.IOException; import java.util.Collection; import java.util.Collections; import java.util.HashSet; @@ -99,14 +99,13 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase keys = new Stack<>(); @@ -337,12 +304,9 @@ public class DefaultRepositoryManagerTest extends ManagerTestBaseemptySet()); } - private Repository createRepository(Repository repository) throws RepositoryException, IOException { + private Repository createRepository(Repository repository) throws RepositoryException { manager.create(repository); assertNotNull(repository.getId()); assertNotNull(manager.get(repository.getId())); @@ -600,17 +540,17 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase manager, Repository repository) - throws RepositoryException, IOException { + throws RepositoryException { String id = repository.getId(); From 212bbd6320552e6c875d038b83abc07578b510bc Mon Sep 17 00:00:00 2001 From: Johannes Schnatterer Date: Thu, 12 Jul 2018 13:38:52 +0000 Subject: [PATCH 38/38] Close branch feature/manager_dao_adapter