diff --git a/scm-core/src/main/java/sonia/scm/HandlerBase.java b/scm-core/src/main/java/sonia/scm/HandlerBase.java index 0baea8d929..6ce7d9a6a6 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; /** * Removes a persistent object. @@ -70,7 +66,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 +77,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..3b90002d13 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,16 +77,16 @@ public class ManagerDecorator * {@inheritDoc} */ @Override - public void create(T object) throws E, IOException + public T create(T object) throws E { - decorated.create(object); + return decorated.create(object); } /** * {@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/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 8389900098..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 message exception message - */ - public GroupAlreadyExistsException(String message) { - super(message); + 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 f4b9934128..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,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(Group group) { + super("group " + group.getName() + " does not exist"); } } 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..b49d4355b9 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,8 +105,8 @@ public abstract class AbstractSimpleRepositoryHandler getByNamespace(String namespace, String name) { + return getAll() + .stream() + .filter(r -> r.getName().equals(name) && r.getNamespace().equals(namespace)) + .findFirst(); + } } 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/repository/RepositoryNotFoundException.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryNotFoundException.java index f1649efb43..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,17 +52,11 @@ public class RepositoryNotFoundException extends RepositoryException * error detail message. * */ - public RepositoryNotFoundException() {} + public RepositoryNotFoundException(Repository repository) { + super("repository " + repository.getName() + "/" + repository.getNamespace() + " 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..ee96d583da 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,37 +37,26 @@ 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.repository.PostReceiveRepositoryHookEvent; -import sonia.scm.repository.PreProcessorUtil; -import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryCacheKeyPredicate; -import sonia.scm.repository.RepositoryEvent; -import sonia.scm.repository.RepositoryManager; -import sonia.scm.repository.RepositoryNotFoundException; -import sonia.scm.repository.RepositoryPermissions; +import sonia.scm.event.ScmEventBus; +import sonia.scm.repository.*; 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 +168,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/security/SyncingRealmHelper.java b/scm-core/src/main/java/sonia/scm/security/SyncingRealmHelper.java index 4c5b5c4e53..9e0c97f234 100644 --- a/scm-core/src/main/java/sonia/scm/security/SyncingRealmHelper.java +++ b/scm-core/src/main/java/sonia/scm/security/SyncingRealmHelper.java @@ -30,12 +30,10 @@ package sonia.scm.security; import com.google.common.collect.ImmutableList; import com.google.inject.Inject; - import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.subject.SimplePrincipalCollection; - import sonia.scm.group.Group; import sonia.scm.group.GroupException; import sonia.scm.group.GroupManager; @@ -46,9 +44,6 @@ import sonia.scm.user.UserException; import sonia.scm.user.UserManager; import sonia.scm.web.security.AdministrationContext; - -import java.io.IOException; - import java.util.Collection; /** @@ -134,7 +129,7 @@ public final class SyncingRealmHelper { groupManager.create(group); } } - catch (GroupException | IOException ex) { + catch (GroupException ex) { throw new AuthenticationException("could not store group", ex); } }); @@ -155,7 +150,7 @@ public final class SyncingRealmHelper { userManager.create(user); } } - catch (UserException | IOException ex) { + catch (UserException ex) { throw new AuthenticationException("could not store user", ex); } }); 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 8c8f2542f0..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 message message of exception - * @since 1.5 - */ - public UserAlreadyExistsException(String message) - { - super(message); + 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 16ee045a75..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,39 +51,7 @@ public class UserNotFoundException extends UserException * Constructs a new UserNotFoundException. * */ - public UserNotFoundException() {} - - /** - * Constructs a new UserNotFoundException. - * - * - * @param message message for the exception - */ - public UserNotFoundException(String message) - { - super(message); - } - - /** - * Constructs a new UserNotFoundException. - * - * - * @param throwable root cause - */ - public UserNotFoundException(Throwable throwable) - { - super(throwable); - } - - /** - * Constructs a new UserNotFoundException. - * - * - * @param message message for the exception - * @param throwable root cause - */ - public UserNotFoundException(String message, Throwable throwable) - { - super(message, throwable); + public UserNotFoundException(User user) { + super("user " + user.getName() + " does not exist"); } } 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 31ea2222c9..9642089bfa 100644 --- a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java +++ b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java @@ -14,8 +14,10 @@ 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; + public static final String REPOSITORY_COLLECTION = PREFIX + "repositoryCollection" + SUFFIX; public static final String GLOBAL_CONFIG = PREFIX + "global_config" + SUFFIX; diff --git a/scm-core/src/test/java/sonia/scm/ManagerTest.java b/scm-core/src/test/java/sonia/scm/ManagerTest.java index 749e579386..06c8eb3ea6 100644 --- a/scm-core/src/test/java/sonia/scm/ManagerTest.java +++ b/scm-core/src/test/java/sonia/scm/ManagerTest.java @@ -88,7 +88,7 @@ public class ManagerTest { public Collection getAll(Comparator comparator, int start, int limit) { return null; } @Override - public void create(TypedObject object) {} + public TypedObject create(TypedObject object) { return null; } @Override public void delete(TypedObject object) {} 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..b94ce934f6 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/ManagerDaoAdapter.java @@ -0,0 +1,68 @@ +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 Function notFoundException; + private final 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 { + T notModified = dao.get(object.getId()); + 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.apply(object); + } + } + + 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; + } + + 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/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(); + } +} 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..27ceb03a36 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 @@ -48,13 +48,13 @@ import sonia.scm.util.AssertUtil; import sonia.scm.util.HttpUtil; import sonia.scm.util.Util; -import javax.ws.rs.core.CacheControl; -import javax.ws.rs.core.EntityTag; -import javax.ws.rs.core.GenericEntity; -import javax.ws.rs.core.Request; -import javax.ws.rs.core.Response; +import javax.ws.rs.core.*; 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 +76,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 +524,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 +551,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,17 +572,32 @@ public abstract class AbstractManagerResource fetchPage(String sortby, boolean desc, int pageNumber, + // We have to handle IntrospectionException here, because it's a checked exception + // It shouldn't occur really - so creating a new unchecked exception would be over-engineered here + @SuppressWarnings("squid:S00112") + private void checkSortByField(String sortBy) { + try { + BeanInfo info = Introspector.getBeanInfo(type); + PropertyDescriptor[] pds = info.getPropertyDescriptors(); + if (Arrays.stream(pds).noneMatch(p -> 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); 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 ---------------------------------------------------------- @@ -676,16 +669,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..42816abd93 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,34 +41,17 @@ 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 javax.ws.rs.*; +import javax.ws.rs.core.*; import java.util.Collection; -import javax.ws.rs.Consumes; -import javax.ws.rs.DELETE; -import javax.ws.rs.DefaultValue; -import javax.ws.rs.GET; -import javax.ws.rs.POST; -import javax.ws.rs.PUT; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.Produces; -import javax.ws.rs.QueryParam; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.GenericEntity; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Request; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.UriInfo; +//~--- JDK imports ------------------------------------------------------------ /** * RESTful Web Service Resource to manage groups and their members. @@ -97,7 +80,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/RepositoryImportResource.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryImportResource.java index 0fe6f8df1d..22f83b8bcf 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryImportResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryImportResource.java @@ -40,24 +40,17 @@ import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.io.Files; import com.google.inject.Inject; - +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 org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.NotSupportedFeatuerException; import sonia.scm.Type; import sonia.scm.api.rest.RestActionUploadResult; -import sonia.scm.repository.AdvancedImportHandler; -import sonia.scm.repository.ImportHandler; -import sonia.scm.repository.ImportResult; -import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryAlreadyExistsException; -import sonia.scm.repository.RepositoryException; -import sonia.scm.repository.RepositoryHandler; -import sonia.scm.repository.RepositoryManager; -import sonia.scm.repository.RepositoryType; +import sonia.scm.repository.*; import sonia.scm.repository.api.Command; import sonia.scm.repository.api.RepositoryService; import sonia.scm.repository.api.RepositoryServiceFactory; @@ -65,45 +58,24 @@ import sonia.scm.repository.api.UnbundleCommandBuilder; import sonia.scm.security.Role; import sonia.scm.util.IOUtil; -import static com.google.common.base.Preconditions.*; - -//~--- JDK imports ------------------------------------------------------------ - -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 javax.ws.rs.*; +import javax.ws.rs.core.*; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlRootElement; import java.io.File; import java.io.IOException; import java.io.InputStream; - import java.net.URI; - import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Set; -import javax.ws.rs.Consumes; -import javax.ws.rs.DefaultValue; -import javax.ws.rs.FormParam; -import javax.ws.rs.GET; -import javax.ws.rs.POST; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.Produces; -import javax.ws.rs.QueryParam; -import javax.ws.rs.WebApplicationException; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.GenericEntity; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.UriInfo; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlRootElement; +//~--- JDK imports ------------------------------------------------------------ /** * Rest resource for importing repositories. @@ -564,10 +536,6 @@ public class RepositoryImportResource { handleGenericCreationFailure(ex, type, name); } - catch (IOException ex) - { - handleGenericCreationFailure(ex, type, name); - } return repository; } @@ -716,10 +684,6 @@ public class RepositoryImportResource { manager.delete(repository); } - catch (IOException e) - { - logger.error("can not delete repository", e); - } catch (RepositoryException e) { logger.error("can not delete repository", e); 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..7e549309ff 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,52 +46,16 @@ 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; -import sonia.scm.repository.Changeset; -import sonia.scm.repository.ChangesetPagingResult; -import sonia.scm.repository.HealthChecker; -import sonia.scm.repository.Permission; -import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryException; -import sonia.scm.repository.RepositoryIsNotArchivedException; -import sonia.scm.repository.RepositoryManager; -import sonia.scm.repository.RepositoryNotFoundException; -import sonia.scm.repository.Tags; -import sonia.scm.repository.api.BlameCommandBuilder; -import sonia.scm.repository.api.BrowseCommandBuilder; -import sonia.scm.repository.api.CatCommandBuilder; -import sonia.scm.repository.api.CommandNotSupportedException; -import sonia.scm.repository.api.DiffCommandBuilder; -import sonia.scm.repository.api.DiffFormat; -import sonia.scm.repository.api.LogCommandBuilder; -import sonia.scm.repository.api.RepositoryService; -import sonia.scm.repository.api.RepositoryServiceFactory; +import sonia.scm.repository.*; +import sonia.scm.repository.api.*; import sonia.scm.util.AssertUtil; import sonia.scm.util.HttpUtil; import sonia.scm.util.IOUtil; import sonia.scm.util.Util; -import javax.ws.rs.Consumes; -import javax.ws.rs.DELETE; -import javax.ws.rs.DefaultValue; -import javax.ws.rs.GET; -import javax.ws.rs.POST; -import javax.ws.rs.PUT; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.Produces; -import javax.ws.rs.QueryParam; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.GenericEntity; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Request; -import javax.ws.rs.core.Response; +import javax.ws.rs.*; +import javax.ws.rs.core.*; import javax.ws.rs.core.Response.Status; -import javax.ws.rs.core.StreamingOutput; -import javax.ws.rs.core.UriInfo; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; @@ -120,19 +84,15 @@ 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/BaseMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BaseMapper.java index bae1a89308..94d884c437 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BaseMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BaseMapper.java @@ -9,7 +9,7 @@ import java.time.Instant; abstract class BaseMapper { @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/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/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/ResourceManagerAdapter.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/CollectionResourceManagerAdapter.java similarity index 59% 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..3169088331 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 @@ -8,7 +8,6 @@ import sonia.scm.api.rest.resources.AbstractManagerResource; import javax.ws.rs.core.GenericEntity; import javax.ws.rs.core.Response; -import java.io.IOException; import java.net.URI; import java.util.Collection; import java.util.function.Function; @@ -17,50 +16,23 @@ 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 Collection resources (e.g. {@code /users}). * * 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 SingleResourceManagerAdapter */ @SuppressWarnings("squid:S00119") // "MODEL_OBJECT" is much more meaningful than "M", right? -class ResourceManagerAdapter extends AbstractManagerResource { - ResourceManagerAdapter(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); + CollectionResourceManagerAdapter(Manager manager, Class type) { + super(manager, type); } /** @@ -76,13 +48,13 @@ class ResourceManagerAdapter modelObjectSupplier, Function uriCreator) throws IOException, EXCEPTION { + public Response create(DTO dto, Supplier modelObjectSupplier, Function uriCreator) throws EXCEPTION { if (dto == null) { return Response.status(BAD_REQUEST).build(); } MODEL_OBJECT modelObject = modelObjectSupplier.get(); - manager.create(modelObject); - return Response.created(URI.create(uriCreator.apply(modelObject))).build(); + MODEL_OBJECT created = manager.create(modelObject); + return Response.created(URI.create(uriCreator.apply(created))).build(); } @Override 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..0f68894a64 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 @@ -19,23 +19,24 @@ 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, Group.class); } /** * 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 @@ -44,6 +45,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/GroupResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupResource.java index 3c1f2aeb64..10818ec953 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 @@ -9,27 +9,21 @@ import sonia.scm.group.GroupManager; import sonia.scm.web.VndMediaType; import javax.inject.Inject; -import javax.ws.rs.Consumes; -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; +import javax.ws.rs.*; import javax.ws.rs.core.Response; 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, Group.class); } /** 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/IdResourceManagerAdapter.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IdResourceManagerAdapter.java new file mode 100644 index 0000000000..ded4bff309 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IdResourceManagerAdapter.java @@ -0,0 +1,66 @@ +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.Optional; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; + +/** + * 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 { + + private final Manager manager; + + private final SingleResourceManagerAdapter singleAdapter; + private final CollectionResourceManagerAdapter collectionAdapter; + + IdResourceManagerAdapter(Manager manager, Class type) { + this.manager = manager; + singleAdapter = new SingleResourceManagerAdapter<>(manager, type); + collectionAdapter = new CollectionResourceManagerAdapter<>(manager, type); + } + + Response get(String id, Function mapToDto) { + return singleAdapter.get(loadBy(id), mapToDto); + } + + public Response update(String id, Function applyChanges) { + return singleAdapter.update( + loadBy(id), + applyChanges, + idStaysTheSame(id) + ); + } + + 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); + } + + private Supplier> loadBy(String id) { + return () -> Optional.ofNullable(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/MapperModule.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java index 72a4e5e062..84805f8aff 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 @@ -18,6 +18,9 @@ public class MapperModule extends AbstractModule { bind(ScmConfigurationToGlobalConfigDtoMapper.class).to(Mappers.getMapper(ScmConfigurationToGlobalConfigDtoMapper.class).getClass()); bind(GlobalConfigDtoToScmConfigurationMapper.class).to(Mappers.getMapper(GlobalConfigDtoToScmConfigurationMapper.class).getClass()); + 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/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/RepositoryCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java new file mode 100644 index 0000000000..7a70957b91 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java @@ -0,0 +1,84 @@ +package sonia.scm.api.v2.resources; + +import com.webcohesion.enunciate.metadata.rs.*; +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.*; +import javax.ws.rs.core.Response; + +public class RepositoryCollectionResource { + + private static final int DEFAULT_PAGE_SIZE = 10; + + private final CollectionResourceManagerAdapter adapter; + private final RepositoryCollectionToDtoMapper repositoryCollectionToDtoMapper; + private final RepositoryDtoToRepositoryMapper dtoToRepositoryMapper; + private final ResourceLinks resourceLinks; + + @Inject + public RepositoryCollectionResource(RepositoryManager manager, RepositoryCollectionToDtoMapper repositoryCollectionToDtoMapper, RepositoryDtoToRepositoryMapper dtoToRepositoryMapper, ResourceLinks resourceLinks) { + this.adapter = new CollectionResourceManagerAdapter<>(manager, Repository.class); + this.repositoryCollectionToDtoMapper = repositoryCollectionToDtoMapper; + this.dtoToRepositoryMapper = dtoToRepositoryMapper; + 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) + @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 does not have the \"repository\" privilege"), + @ResponseCode(code = 500, condition = "internal server error") + }) + public Response getAll(@DefaultValue("0") @QueryParam("page") int page, + @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) + @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 RepositoryException { + return adapter.create(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/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/RepositoryDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java new file mode 100644 index 0000000000..bcc8e16ebb --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java @@ -0,0 +1,33 @@ +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; + +import java.time.Instant; +import java.util.List; +import java.util.Map; + +@Getter @Setter +public class RepositoryDto extends HalRepresentation { + + 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 boolean archived = false; + private String type; + protected Map properties; + + @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/RepositoryDtoToRepositoryMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDtoToRepositoryMapper.java new file mode 100644 index 0000000000..61e953602f --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDtoToRepositoryMapper.java @@ -0,0 +1,21 @@ +package sonia.scm.api.v2.resources; + +import org.mapstruct.*; +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, @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 new file mode 100644 index 0000000000..34896149c3 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java @@ -0,0 +1,158 @@ +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.inject.Provider; +import javax.ws.rs.*; +import javax.ws.rs.core.Response; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.function.Supplier; + +public class RepositoryResource { + + private final RepositoryToRepositoryDtoMapper repositoryToDtoMapper; + private final RepositoryDtoToRepositoryMapper dtoToRepositoryMapper; + + private final RepositoryManager manager; + private final SingleResourceManagerAdapter adapter; + private final Provider tagRootResource; + private final Provider branchRootResource; + private final Provider changesetRootResource; + private final Provider sourceRootResource; + private final Provider permissionRootResource; + + @Inject + public RepositoryResource( + RepositoryToRepositoryDtoMapper repositoryToDtoMapper, + 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, Repository.class); + this.tagRootResource = tagRootResource; + this.branchRootResource = branchRootResource; + this.changesetRootResource = changesetRootResource; + this.sourceRootResource = sourceRootResource; + 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) + @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(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({ + @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) { + 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) + @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( + loadBy(namespace, name), + existing -> dtoToRepositoryMapper.map(repositoryDto, existing.getId()), + nameAndNamespaceStaysTheSame(namespace, name) + ); + } + + @Path("tags/") + public TagRootResource tags() { + return tagRootResource.get(); + } + + @Path("branches/") + public BranchRootResource branches() { + return branchRootResource.get(); + } + + @Path("changesets/") + public ChangesetRootResource changesets() { + return changesetRootResource.get(); + } + + @Path("sources/") + public SourceRootResource sources() { + return sourceRootResource.get(); + } + + @Path("permissions/") + 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); + } +} 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..a7a6365c37 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRootResource.java @@ -0,0 +1,32 @@ +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; + private final Provider repositoryCollectionResource; + + @Inject + 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/RepositoryToRepositoryDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java new file mode 100644 index 0000000000..2f13723d39 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java @@ -0,0 +1,41 @@ +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.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") +@Mapper +public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper { + + @Inject + private ResourceLinks resourceLinks; + + abstract HealthCheckFailureDto toDto(HealthCheckFailure failure); + + @AfterMapping + void appendLinks(Repository repository, @MappingTarget RepositoryDto target) { + Links.Builder linksBuilder = linkingTo().self(resourceLinks.repository().self(target.getNamespace(), target.getName())); + if (RepositoryPermissions.delete(repository).isPermitted()) { + linksBuilder.single(link("delete", resourceLinks.repository().delete(target.getNamespace(), target.getName()))); + } + if (RepositoryPermissions.modify(repository).isPermitted()) { + linksBuilder.single(link("update", resourceLinks.repository().update(target.getNamespace(), target.getName()))); + linksBuilder.single(link("permissions", resourceLinks.permissionCollection().self(target.getNamespace(), target.getName()))); + } + linksBuilder.single(link("tags", resourceLinks.tagCollection().self(target.getNamespace(), target.getName()))); + linksBuilder.single(link("branches", resourceLinks.branchCollection().self(target.getNamespace(), target.getName()))); + linksBuilder.single(link("changesets", resourceLinks.changesetCollection().self(target.getNamespace(), target.getName()))); + linksBuilder.single(link("sources", resourceLinks.sourceCollection().self(target.getNamespace(), target.getName()))); + target.add(linksBuilder.build()); + } +} 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 83faa57e35..979782768b 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 GlobalConfigLinks { private final LinkBuilder globalConfigLinkBuilder; - private GlobalConfigLinks(UriInfo uriInfo) { + GlobalConfigLinks(UriInfo uriInfo) { globalConfigLinkBuilder = new LinkBuilder(uriInfo, GlobalConfigResource.class); } @@ -120,4 +120,128 @@ class ResourceLinks { return globalConfigLinkBuilder.method("update").parameters().href(); } } + + public RepositoryLinks repository() { + return new RepositoryLinks(uriInfoStore.get()); + } + + static class RepositoryLinks { + private final LinkBuilder repositoryLinkBuilder; + + RepositoryLinks(UriInfo uriInfo) { + repositoryLinkBuilder = new LinkBuilder(uriInfo, RepositoryRootResource.class, RepositoryResource.class); + } + + String self(String namespace, String name) { + return repositoryLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("get").parameters().href(); + } + + String delete(String namespace, String name) { + return repositoryLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("delete").parameters().href(); + } + + String update(String namespace, String name) { + return repositoryLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("update").parameters().href(); + } + } + + 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()); + } + + static class TagCollectionLinks { + private final LinkBuilder tagLinkBuilder; + + TagCollectionLinks(UriInfo uriInfo) { + tagLinkBuilder = new LinkBuilder(uriInfo, RepositoryRootResource.class, RepositoryResource.class, TagRootResource.class, TagCollectionResource.class); + } + + String self(String namespace, String name) { + return tagLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("tags").parameters().method("getTagCollectionResource").parameters().method("getAll").parameters().href(); + } + } + + public BranchCollectionLinks branchCollection() { + return new BranchCollectionLinks(uriInfoStore.get()); + } + + static class BranchCollectionLinks { + private final LinkBuilder branchLinkBuilder; + + BranchCollectionLinks(UriInfo uriInfo) { + branchLinkBuilder = new LinkBuilder(uriInfo, RepositoryRootResource.class, RepositoryResource.class, BranchRootResource.class, BranchCollectionResource.class); + } + + String self(String namespace, String name) { + return branchLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("branches").parameters().method("getBranchCollectionResource").parameters().method("getAll").parameters().href(); + } + } + + public ChangesetCollectionLinks changesetCollection() { + return new ChangesetCollectionLinks(uriInfoStore.get()); + } + + static class ChangesetCollectionLinks { + private final LinkBuilder changesetLinkBuilder; + + ChangesetCollectionLinks(UriInfo uriInfo) { + changesetLinkBuilder = new LinkBuilder(uriInfo, RepositoryRootResource.class, RepositoryResource.class, ChangesetRootResource.class, ChangesetCollectionResource.class); + } + + String self(String namespace, String name) { + return changesetLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("changesets").parameters().method("getChangesetCollectionResource").parameters().method("getAll").parameters().href(); + } + } + + public SourceCollectionLinks sourceCollection() { + return new SourceCollectionLinks(uriInfoStore.get()); + } + + static class SourceCollectionLinks { + private final LinkBuilder sourceLinkBuilder; + + SourceCollectionLinks(UriInfo uriInfo) { + sourceLinkBuilder = new LinkBuilder(uriInfo, RepositoryRootResource.class, RepositoryResource.class, SourceRootResource.class, SourceCollectionResource.class); + } + + String self(String namespace, String name) { + return sourceLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("sources").parameters().method("getSourceCollectionResource").parameters().method("getAll").parameters().href(); + } + } + + public PermissionCollectionLinks permissionCollection() { + return new PermissionCollectionLinks(uriInfoStore.get()); + } + + static class PermissionCollectionLinks { + private final LinkBuilder permissionLinkBuilder; + + PermissionCollectionLinks(UriInfo uriInfo) { + permissionLinkBuilder = new LinkBuilder(uriInfo, RepositoryRootResource.class, RepositoryResource.class, PermissionRootResource.class, PermissionCollectionResource.class); + } + + String self(String namespace, String name) { + return permissionLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("permissions").parameters().method("getPermissionCollectionResource").parameters().method("getAll").parameters().href(); + } + } } 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..7f8b115dee --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java @@ -0,0 +1,87 @@ +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.Optional; +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; + +/** + * 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 extends AbstractManagerResource { + + SingleResourceManagerAdapter(Manager manager, Class type) { + super(manager, type); + } + + /** + * 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) { + 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) { + Optional existingModelObject = reader.get(); + if (!existingModelObject.isPresent()) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + 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.get()), changedModelObject); + } + + public Response delete(Supplier> reader) { + return reader.get() + .map(MODEL_OBJECT::getId) + .map(this::delete) + .orElse(null); + } + + @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/SourceCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SourceCollectionResource.java new file mode 100644 index 0000000000..d3f00a0b51 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SourceCollectionResource.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 SourceCollectionResource { + @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/SourceRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SourceRootResource.java new file mode 100644 index 0000000000..f58cbb6dff --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SourceRootResource.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 SourceRootResource { + + private final Provider sourceCollectionResource; + + @Inject + public SourceRootResource(Provider sourceCollectionResource) { + this.sourceCollectionResource = sourceCollectionResource; + } + + @Path("") + public SourceCollectionResource getSourceCollectionResource() { + return sourceCollectionResource.get(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/TagCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/TagCollectionResource.java new file mode 100644 index 0000000000..3dbba0cb0e --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/TagCollectionResource.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 TagCollectionResource { + @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/TagRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/TagRootResource.java new file mode 100644 index 0000000000..29a2a922ca --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/TagRootResource.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 TagRootResource { + + private final Provider tagCollectionResource; + + @Inject + public TagRootResource(Provider tagCollectionResource) { + this.tagCollectionResource = tagCollectionResource; + } + + @Path("") + public TagCollectionResource getTagCollectionResource() { + return tagCollectionResource.get(); + } +} 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..ef826980b3 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 @@ -18,14 +18,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, User.class); this.resourceLinks = resourceLinks; } @@ -33,9 +33,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 @@ -44,6 +45,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") 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..f05c8165cb 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 @@ -9,13 +9,7 @@ import sonia.scm.user.UserManager; import sonia.scm.web.VndMediaType; import javax.inject.Inject; -import javax.ws.rs.Consumes; -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; +import javax.ws.rs.*; import javax.ws.rs.core.Response; public class UserResource { @@ -23,13 +17,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, User.class); } /** 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 b4e9df2430..f22a5519dc 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; @@ -79,6 +80,10 @@ public class DefaultGroupManager extends AbstractGroupManager public DefaultGroupManager(GroupDAO groupDAO) { this.groupDAO = groupDAO; + this.managerDaoAdapter = new ManagerDaoAdapter<>( + groupDAO, + GroupNotFoundException::new, + GroupAlreadyExistsException::new); } //~--- methods -------------------------------------------------------------- @@ -96,78 +101,34 @@ public class DefaultGroupManager extends AbstractGroupManager // do nothing } - /** - * Method description - * - * - * @param group - * - * @throws GroupException - * @throws IOException - */ @Override - public void create(Group group) throws GroupException, IOException - { + 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.concat(" group already exists")); - } + 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 managerDaoAdapter.create( + group, + GroupPermissions::create, + newGroup -> fireEvent(HandlerEventType.BEFORE_CREATE, newGroup), + newGroup -> fireEvent(HandlerEventType.CREATE, newGroup) + ); } - /** - * Method description - * - * - * @param group - * - * @throws GroupException - * @throws IOException - */ @Override - public void delete(Group group) throws GroupException, IOException - { - 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("user does not exists"); - } + 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) + ); } /** @@ -189,31 +150,18 @@ public class DefaultGroupManager extends AbstractGroupManager * @throws IOException */ @Override - public void modify(Group group) throws GroupException, IOException - { - 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("group does not exists"); - } + managerDaoAdapter.modify( + group, + GroupPermissions::modify, + notModified -> { + removeDuplicateMembers(group); + fireEvent(HandlerEventType.BEFORE_MODIFY, group, notModified); + }, + notModified -> fireEvent(HandlerEventType.MODIFY, group, notModified) + ); } /** @@ -226,7 +174,7 @@ public class DefaultGroupManager extends AbstractGroupManager * @throws IOException */ @Override - public void refresh(Group group) throws GroupException, IOException + public void refresh(Group group) throws GroupException { String name = group.getName(); if (logger.isInfoEnabled()) @@ -239,7 +187,7 @@ public class DefaultGroupManager extends AbstractGroupManager if (fresh == null) { - throw new GroupNotFoundException("group does not exists"); + throw new GroupNotFoundException(group); } fresh.copyProperties(group); @@ -452,4 +400,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 00271a75cb..da8a1911e2 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java @@ -42,29 +42,14 @@ import com.google.inject.Singleton; import org.apache.shiro.concurrent.SubjectAwareExecutorService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sonia.scm.ArgumentIsInvalidException; -import sonia.scm.ConfigurationException; -import sonia.scm.HandlerEventType; -import sonia.scm.SCMContextProvider; -import sonia.scm.Type; +import sonia.scm.*; import sonia.scm.config.ScmConfiguration; import sonia.scm.security.KeyGenerator; -import sonia.scm.util.AssertUtil; -import sonia.scm.util.CollectionAppender; -import sonia.scm.util.HttpUtil; -import sonia.scm.util.IOUtil; -import sonia.scm.util.Util; +import sonia.scm.util.*; import javax.servlet.http.HttpServletRequest; import java.io.IOException; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; @@ -90,6 +75,7 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager { private final Set types; private RepositoryMatcher repositoryMatcher; private NamespaceStrategy namespaceStrategy; + private final ManagerDaoAdapter managerDaoAdapter; @Inject @@ -116,6 +102,10 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager { for (RepositoryHandler handler : handlerSet) { addHandler(contextProvider, handler); } + managerDaoAdapter = new ManagerDaoAdapter<>( + repositoryDAO, + RepositoryNotFoundException::new, + RepositoryAlreadyExistsException::create); } @@ -128,89 +118,47 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager { } } - /** - * Method description - * - * - * @param repository - * @param initRepository - * - * @throws IOException - * @throws RepositoryException - */ - public void create(Repository repository, boolean initRepository) - throws RepositoryException, IOException { - logger.info("create repository {} of type {}", repository.getName(), - repository.getType()); - - RepositoryPermissions.create().check(); - AssertUtil.assertIsValid(repository); - - if (repositoryDAO.contains(repository)) { - throw RepositoryAlreadyExistsException.create(repository); - } + @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.setCreationDate(System.currentTimeMillis()); repository.setNamespace(namespaceStrategy.getNamespace()); - if (initRepository) { - getHandler(repository).create(repository); - } + logger.info("create repository {} of type {} in namespace {}", repository.getName(), repository.getType(), repository.getNamespace()); - fireEvent(HandlerEventType.BEFORE_CREATE, repository); - repositoryDAO.add(repository); - fireEvent(HandlerEventType.CREATE, repository); + return managerDaoAdapter.create( + repository, + RepositoryPermissions::create, + newRepository -> { + if (initRepository) { + getHandler(newRepository).create(newRepository); + } + fireEvent(HandlerEventType.BEFORE_CREATE, newRepository); + }, + newRepository -> fireEvent(HandlerEventType.CREATE, newRepository) + ); } - /** - * Method description - * - * - * @param repository - * - * @throws IOException - * @throws RepositoryException - */ @Override - public void create(Repository repository) - throws RepositoryException, IOException { - create(repository, true); + 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) + ); } - /** - * Method description - * - * - * @param repository - * - * @throws IOException - * @throws RepositoryException - */ - @Override - public void delete(Repository repository) - throws RepositoryException, IOException { - if (logger.isInfoEnabled()) { - logger.info("delete repository {} of type {}", repository.getName(), - repository.getType()); - } - - 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 ".concat(repository.getName()).concat(" not found")); + 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); } /** @@ -248,29 +196,18 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager { * @throws RepositoryException */ @Override - public void modify(Repository repository) - throws RepositoryException, IOException { - 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()); - 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) + ); } /** @@ -284,7 +221,7 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager { */ @Override public void refresh(Repository repository) - throws RepositoryException, IOException { + throws RepositoryException { AssertUtil.assertIsNotNull(repository); RepositoryPermissions.read(repository).check(); @@ -294,8 +231,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(repository); } } @@ -636,5 +572,4 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager { return handler; } - } diff --git a/scm-webapp/src/main/java/sonia/scm/repository/LastModifiedUpdateListener.java b/scm-webapp/src/main/java/sonia/scm/repository/LastModifiedUpdateListener.java index f8afe61200..bf868b2027 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/LastModifiedUpdateListener.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/LastModifiedUpdateListener.java @@ -35,10 +35,8 @@ package sonia.scm.repository; import com.google.common.eventbus.Subscribe; import com.google.inject.Inject; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.EagerSingleton; import sonia.scm.plugin.Extension; import sonia.scm.web.security.AdministrationContext; @@ -46,8 +44,6 @@ import sonia.scm.web.security.PrivilegedAction; //~--- JDK imports ------------------------------------------------------------ -import java.io.IOException; - /** * * @author Sebastian Sdorra @@ -154,7 +150,7 @@ public final class LastModifiedUpdateListener { repositoryManager.modify(dbr); } - catch (RepositoryException | IOException ex) + catch (RepositoryException ex) { logger.error("could not modify repository", ex); } 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 72ead40c58..563e140434 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; @@ -55,11 +55,7 @@ import javax.xml.bind.JAXBException; import javax.xml.bind.Unmarshaller; import java.io.IOException; import java.io.InputStream; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; +import java.util.*; //~--- JDK imports ------------------------------------------------------------ @@ -96,6 +92,10 @@ public class DefaultUserManager extends AbstractUserManager public DefaultUserManager(UserDAO userDAO) { this.userDAO = userDAO; + this.managerDaoAdapter = new ManagerDaoAdapter<>( + userDAO, + UserNotFoundException::new, + UserAlreadyExistsException::new); } //~--- methods -------------------------------------------------------------- @@ -137,64 +137,31 @@ public class DefaultUserManager extends AbstractUserManager * @throws UserException */ @Override - public void create(User user) throws UserException, IOException - { + 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().concat(" user already exists")); - } - - AssertUtil.assertIsValid(user); - user.setCreationDate(System.currentTimeMillis()); - fireEvent(HandlerEventType.BEFORE_CREATE, user); - userDAO.add(user); - fireEvent(HandlerEventType.CREATE, user); + return managerDaoAdapter.create( + user, + UserPermissions::create, + newUser -> fireEvent(HandlerEventType.BEFORE_CREATE, newUser), + newUser -> fireEvent(HandlerEventType.CREATE, newUser) + ); } - /** - * Method description - * - * - * @param user - * - * @throws IOException - * @throws UserException - */ @Override - public void delete(User user) throws UserException, IOException - { - 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 does not exists"); - } + 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) + ); } /** @@ -224,29 +191,15 @@ public class DefaultUserManager extends AbstractUserManager * @throws UserException */ @Override - public void modify(User user) throws UserException, IOException + 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("user does not exists"); - } + 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)); } /** @@ -259,7 +212,7 @@ public class DefaultUserManager extends AbstractUserManager * @throws UserException */ @Override - public void refresh(User user) throws UserException, IOException + public void refresh(User user) throws UserException { if (logger.isInfoEnabled()) { @@ -271,7 +224,7 @@ public class DefaultUserManager extends AbstractUserManager if (fresh == null) { - throw new UserNotFoundException("user does not exists"); + throw new UserNotFoundException(user); } fresh.copyProperties(user); @@ -496,6 +449,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 new file mode 100644 index 0000000000..b3e931188a --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/rest/resources/AbstractManagerResourceTest.java @@ -0,0 +1,137 @@ +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; + +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.Matchers.eq; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class AbstractManagerResourceTest { + + @Mock + private Manager manager; + @Mock + private Request request; + @Captor + private ArgumentCaptor> comparatorCaptor; + + 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(); + 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); + } + + + private class SimpleManagerResource extends AbstractManagerResource { + + { + disableCache = true; + } + + private 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; + + Simple(String id, String data) { + this.id = id; + this.data = data; + } + + public String getData() { + return data; + } + + @Override + public String getId() { + 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; + } + + @Override + public String getType() { + return null; + } + @Override + public boolean isValid() { + return false; + } + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GlobalConfigResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GlobalConfigResourceTest.java index 49945bedd2..e8e0f490e7 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GlobalConfigResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GlobalConfigResourceTest.java @@ -10,9 +10,7 @@ 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.config.ScmConfiguration; import sonia.scm.web.VndMediaType; @@ -40,8 +38,9 @@ public class GlobalConfigResourceTest { private Dispatcher dispatcher = MockDispatcherFactory.createDispatcher(); - @Mock(answer = Answers.RETURNS_DEEP_STUBS) - private ResourceLinks resourceLinks; + private final URI baseUri = URI.create("/"); + @SuppressWarnings("unused") // Is injected + private ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); @InjectMocks private GlobalConfigDtoToScmConfigurationMapperImpl dtoToConfigMapper; @@ -49,11 +48,9 @@ public class GlobalConfigResourceTest { private ScmConfigurationToGlobalConfigDtoMapperImpl configToDtoMapper; @Before - public void prepareEnvironment() throws IOException { + public void prepareEnvironment() { initMocks(this); - ResourceLinksMock.initMock(resourceLinks, URI.create("/")); - GlobalConfigResource globalConfigResource = new GlobalConfigResource(dtoToConfigMapper, configToDtoMapper, createConfiguration()); 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 9c943efdfa..dff9e7e99b 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; @@ -28,9 +27,7 @@ import java.net.URL; import java.util.Collections; import static java.util.Collections.singletonList; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; import static org.mockito.Matchers.any; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.doNothing; @@ -49,8 +46,7 @@ public class GroupRootResourceTest { private Dispatcher dispatcher = MockDispatcherFactory.createDispatcher(); - @Mock(answer = Answers.RETURNS_DEEP_STUBS) - private ResourceLinks resourceLinks; + private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(URI.create("/")); @Mock private GroupManager groupManager; @@ -64,15 +60,13 @@ public class GroupRootResourceTest { @Before public void prepareEnvironment() throws IOException, GroupException { initMocks(this); - doNothing().when(groupManager).create(groupCaptor.capture()); + when(groupManager.create(groupCaptor.capture())).thenAnswer(invocation -> invocation.getArguments()[0]); doNothing().when(groupManager).modify(groupCaptor.capture()); Group group = createDummyGroup(); 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 1db089cd0b..e519a9c3e5 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,9 +7,7 @@ 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; import java.net.URI; @@ -24,8 +22,9 @@ import static org.mockito.MockitoAnnotations.initMocks; public class GroupToGroupDtoMapperTest { - @Mock(answer = Answers.RETURNS_DEEP_STUBS) - private ResourceLinks resourceLinks; + private final URI baseUri = URI.create("http://example.com/base/"); + @SuppressWarnings("unused") // Is injected + private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); @InjectMocks private GroupToGroupDtoMapperImpl mapper; @@ -38,12 +37,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/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); + } + } +} 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..a8616fe2ce --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java @@ -0,0 +1,232 @@ +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; +import org.jboss.resteasy.mock.MockHttpResponse; +import org.junit.Before; +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.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; +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.*; +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.*; +import static org.mockito.MockitoAnnotations.initMocks; + +@SubjectAware( + username = "trillian", + password = "secret", + configuration = "classpath:sonia/scm/repository/shiro.ini" +) +public class RepositoryRootResourceTest { + + private final Dispatcher dispatcher = MockDispatcherFactory.createDispatcher(); + + @Rule + public ShiroRule shiro = new ShiroRule(); + + @Mock + private RepositoryManager repositoryManager; + + private final URI baseUri = URI.create("/"); + private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); + + @InjectMocks + private RepositoryToRepositoryDtoMapperImpl repositoryToDtoMapper; + @InjectMocks + private RepositoryDtoToRepositoryMapperImpl dtoToRepositoryMapper; + + @Before + public void prepareEnvironment() { + 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, dtoToRepositoryMapper, resourceLinks); + RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider.of(repositoryResource), MockProvider.of(repositoryCollectionResource)); + dispatcher.getRegistry().addSingletonResource(repositoryRootResource); + } + + @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"); + 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\"")); + } + + @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")); + 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\"")); + } + + @Test + 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") + .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 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"); + + 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()); + } + + @Test + 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); + + 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()); + assertEquals("/v2/repositories/otherspace/repo", response.getOutputHeaders().get("Location").get(0).toString()); + verify(repositoryManager).create(any(Repository.class)); + } + + 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); + repository.setName(name); + String id = namespace + "-" + name; + repository.setId(id); + when(repositoryManager.getByNamespace(namespace, name)).thenReturn(of(repository)); + when(repositoryManager.get(id)).thenReturn(repository); + return repository; + } +} 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..ad6ff3cae7 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java @@ -0,0 +1,159 @@ +package sonia.scm.api.v2.resources; + +import com.github.sdorra.shiro.ShiroRule; +import com.github.sdorra.shiro.SubjectAware; +import org.apache.shiro.util.ThreadContext; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.InjectMocks; +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 static java.util.Collections.singletonList; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.mockito.MockitoAnnotations.initMocks; + +@SubjectAware( + username = "trillian", + password = "secret", + configuration = "classpath:sonia/scm/repository/shiro.ini" +) +public class RepositoryToRepositoryDtoMapperTest { + + @Rule + public final ShiroRule rule = new ShiroRule(); + + private final URI baseUri = URI.create("http://example.com/base/"); + @SuppressWarnings("unused") // Is injected + private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); + + @InjectMocks + private RepositoryToRepositoryDtoMapperImpl mapper; + + @Before + public void init() { + initMocks(this); + } + + @After + public void cleanup() { + ThreadContext.unbindSubject(); + } + + @Test + public void shouldMapSimpleProperties() { + RepositoryDto dto = mapper.map(createTestRepository()); + assertEquals("testspace", dto.getNamespace()); + assertEquals("test", dto.getName()); + assertEquals("description", dto.getDescription()); + assertEquals("git", dto.getType()); + 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() { + RepositoryDto dto = mapper.map(createTestRepository()); + assertEquals( + "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()); + assertFalse(dto.getLinks().getLinkBy("permissions").isPresent()); + } + + @Test + public void shouldCreateDeleteLink() { + RepositoryDto dto = mapper.map(createTestRepository()); + assertEquals( + "http://example.com/base/v2/repositories/testspace/test", + dto.getLinks().getLinkBy("delete").get().getHref()); + } + + @Test + public void shouldCreateUpdateLink() { + RepositoryDto dto = mapper.map(createTestRepository()); + assertEquals( + "http://example.com/base/v2/repositories/testspace/test", + dto.getLinks().getLinkBy("update").get().getHref()); + } + + @Test + public void shouldMapHealthCheck() { + RepositoryDto dto = mapper.map(createTestRepository()); + assertEquals(1, dto.getHealthCheckFailures().size()); + assertEquals("summary", dto.getHealthCheckFailures().get(0).getSummary()); + } + + @Test + public void shouldCreateTagsLink() { + RepositoryDto dto = mapper.map(createTestRepository()); + assertEquals( + "http://example.com/base/v2/repositories/testspace/test/tags/", + dto.getLinks().getLinkBy("tags").get().getHref()); + } + + @Test + public void shouldCreateBranchesLink() { + RepositoryDto dto = mapper.map(createTestRepository()); + assertEquals( + "http://example.com/base/v2/repositories/testspace/test/branches/", + dto.getLinks().getLinkBy("branches").get().getHref()); + } + + @Test + public void shouldCreateChangesetsLink() { + RepositoryDto dto = mapper.map(createTestRepository()); + assertEquals( + "http://example.com/base/v2/repositories/testspace/test/changesets/", + 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()); + } + + @Test + public void shouldCreatePermissionsLink() { + RepositoryDto dto = mapper.map(createTestRepository()); + assertEquals( + "http://example.com/base/v2/repositories/testspace/test/permissions/", + dto.getLinks().getLinkBy("permissions").get().getHref()); + } + + private Repository createTestRepository() { + Repository repository = new Repository(); + repository.setNamespace("testspace"); + repository.setName("test"); + repository.setDescription("description"); + repository.setType("git"); + repository.setContact("none@example.com"); + repository.setId("1"); + repository.setCreationDate(System.currentTimeMillis()); + repository.setHealthCheckFailures(singletonList(new HealthCheckFailure("1", "summary", "url", "failure"))); + repository.setPermissions(singletonList(new Permission("permission", PermissionType.READ))); + + return repository; + } +} 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 dc1c277984..1e2f5b102b 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,30 +1,30 @@ 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.GlobalConfigResource.GLOBAL_CONFIG_PATH_V2; -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]); + public static ResourceLinks createMock(URI baseUri) { + ResourceLinks resourceLinks = mock(ResourceLinks.class); - 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.globalConfig().self()).thenAnswer(invocation -> baseUri + GLOBAL_CONFIG_PATH_V2); - when(resourceLinks.globalConfig().update()).thenAnswer(invocation -> baseUri + GLOBAL_CONFIG_PATH_V2); + 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.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)); + when(resourceLinks.sourceCollection()).thenReturn(new ResourceLinks.SourceCollectionLinks(uriInfo)); + when(resourceLinks.permissionCollection()).thenReturn(new ResourceLinks.PermissionCollectionLinks(uriInfo)); + when(resourceLinks.globalConfig()).thenReturn(new ResourceLinks.GlobalConfigLinks(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 53a8578496..9b2b9ffcc8 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,54 @@ 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); + } + + @Test + public void shouldCreateCorrectBranchCollectionUrl() { + String url = resourceLinks.branchCollection().self("space", "repo"); + assertEquals(BASE_URL + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/branches/", url); + } + + @Test + public void shouldCreateCorrectChangesetCollectionUrl() { + String url = resourceLinks.changesetCollection().self("space", "repo"); + 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); + } + + @Test + public void shouldCreateCorrectPermissionCollectionUrl() { + String url = resourceLinks.sourceCollection().self("space", "repo"); + assertEquals(BASE_URL + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/sources/", url); + } + @Test public void shouldCreateCorrectGlobalConfigSelfUrl() { String url = resourceLinks.globalConfig().self(); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ScmConfigurationToGlobalConfigDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ScmConfigurationToGlobalConfigDtoMapperTest.java index e67e8764d5..d62924050b 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ScmConfigurationToGlobalConfigDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ScmConfigurationToGlobalConfigDtoMapperTest.java @@ -6,14 +6,11 @@ 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.config.ScmConfiguration; import sonia.scm.security.Role; import java.net.URI; -import java.net.URISyntaxException; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -24,8 +21,9 @@ import static sonia.scm.api.v2.resources.GlobalConfigResourceTest.createConfigur public class ScmConfigurationToGlobalConfigDtoMapperTest { - @Mock(answer = Answers.RETURNS_DEEP_STUBS) - private ResourceLinks resourceLinks; + private URI baseUri = URI.create("http://example.com/base/"); + @SuppressWarnings("unused") // Is injected + private ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); @InjectMocks private ScmConfigurationToGlobalConfigDtoMapperImpl mapper; @@ -36,12 +34,10 @@ public class ScmConfigurationToGlobalConfigDtoMapperTest { 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(GlobalConfigResource.GLOBAL_CONFIG_PATH_V2); subjectThreadState.bind(); - ResourceLinksMock.initMock(resourceLinks, baseUri); ThreadContext.bind(subject); } 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..6ca0ddec4a 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; @@ -19,17 +18,15 @@ import java.util.Arrays; import java.util.List; import static java.util.stream.Collectors.toList; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; import static sonia.scm.PageResult.createPage; public class UserCollectionToDtoMapperTest { - @Mock(answer = Answers.RETURNS_DEEP_STUBS) - 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 @@ -45,9 +42,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 077cbef9b2..1eee9b2270 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; @@ -28,15 +27,10 @@ import java.net.URISyntaxException; import java.net.URL; import static java.util.Collections.singletonList; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; import static org.mockito.Matchers.any; import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; import static org.mockito.MockitoAnnotations.initMocks; @SubjectAware( @@ -51,8 +45,7 @@ public class UserRootResourceTest { private Dispatcher dispatcher = MockDispatcherFactory.createDispatcher(); - @Mock(answer = Answers.RETURNS_DEEP_STUBS) - private ResourceLinks resourceLinks; + private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(URI.create("/")); @Mock private PasswordService passwordService; @@ -66,15 +59,13 @@ public class UserRootResourceTest { private ArgumentCaptor userCaptor = ArgumentCaptor.forClass(User.class); @Before - public void prepareEnvironment() throws IOException, UserException { + public void prepareEnvironment() throws 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()); - 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 ae3168cc07..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 @@ -7,14 +7,11 @@ 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; import sonia.scm.user.User; import java.net.URI; -import java.net.URISyntaxException; import java.time.Instant; import static org.junit.Assert.assertEquals; @@ -25,8 +22,9 @@ import static org.mockito.MockitoAnnotations.initMocks; public class UserToUserDtoMapperTest { - @Mock(answer = Answers.RETURNS_DEEP_STUBS) - private ResourceLinks resourceLinks; + private final URI baseUri = URI.create("http://example.com/base/"); + @SuppressWarnings("unused") // Is injected + private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); @InjectMocks private UserToUserDtoMapperImpl mapper; @@ -37,12 +35,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); } 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..e215127121 100644 --- a/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java @@ -41,10 +41,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.mockito.invocation.InvocationOnMock; -import sonia.scm.HandlerEventType; -import sonia.scm.Manager; -import sonia.scm.ManagerTestBase; -import sonia.scm.Type; +import sonia.scm.*; import sonia.scm.config.ScmConfiguration; import sonia.scm.event.ScmEventBus; import sonia.scm.repository.api.HookContext; @@ -57,24 +54,10 @@ 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; -import java.util.Set; -import java.util.Stack; +import java.util.*; -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.hamcrest.Matchers.hasProperty; -import static org.hamcrest.Matchers.is; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNotSame; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -98,14 +81,13 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase keys = new Stack<>(); @@ -336,12 +286,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())); @@ -599,17 +522,17 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase manager, Repository repository) - throws RepositoryException, IOException { + throws RepositoryException { String id = repository.getId(); 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" +}