From ddcc21c1a8ae19d738e52cfeebd7622047a5fede Mon Sep 17 00:00:00 2001 From: Mohamed Karray Date: Mon, 8 Oct 2018 13:39:33 +0200 Subject: [PATCH 1/9] add autocomplete endpoint --- .../src/main/java/sonia/scm/GenericDAO.java | 9 + scm-core/src/main/java/sonia/scm/Manager.java | 9 + .../main/java/sonia/scm/ManagerDecorator.java | 5 + .../src/main/java/sonia/scm/ModelObject.java | 2 +- .../java/sonia/scm/ReducedModelObject.java | 15 + .../src/main/java/sonia/scm/group/Group.java | 9 +- .../java/sonia/scm/repository/Changeset.java | 5 + .../java/sonia/scm/repository/Repository.java | 10 +- .../src/main/java/sonia/scm/user/User.java | 2 +- .../main/java/sonia/scm/web/VndMediaType.java | 1 + .../src/test/java/sonia/scm/ManagerTest.java | 5 + .../java/sonia/scm/xml/AbstractXmlDAO.java | 18 +- .../AutoCompleteBadParamException.java | 11 + .../AutoCompleteBadParamExceptionMapper.java | 16 + .../v2/resources/AutoCompleteResource.java | 113 ++++++ .../scm/api/v2/resources/MapperModule.java | 2 + .../v2/resources/ReducedObjectModelDto.java | 16 + .../ReducedObjectModelToDtoMapper.java | 13 + .../sonia/scm/group/DefaultGroupManager.java | 5 + .../repository/DefaultRepositoryManager.java | 6 + .../DefaultAuthorizationCollector.java | 17 + .../sonia/scm/user/DefaultUserManager.java | 6 + .../AbstractManagerResourceTest.java | 5 + .../resources/AutoCompleteResourceTest.java | 368 ++++++++++++++++++ .../scm/api/v2/resources/DispatcherMock.java | 1 + .../v2/resources/UserRootResourceTest.java | 1 - .../DefaultAuthorizationCollectorTest.java | 8 +- 27 files changed, 664 insertions(+), 14 deletions(-) create mode 100644 scm-core/src/main/java/sonia/scm/ReducedModelObject.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/AutoCompleteBadParamException.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/AutoCompleteBadParamExceptionMapper.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/AutoCompleteResource.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/ReducedObjectModelDto.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/ReducedObjectModelToDtoMapper.java create mode 100644 scm-webapp/src/test/java/sonia/scm/api/v2/resources/AutoCompleteResourceTest.java diff --git a/scm-core/src/main/java/sonia/scm/GenericDAO.java b/scm-core/src/main/java/sonia/scm/GenericDAO.java index b63a96f733..a369c2f8c2 100644 --- a/scm-core/src/main/java/sonia/scm/GenericDAO.java +++ b/scm-core/src/main/java/sonia/scm/GenericDAO.java @@ -114,4 +114,13 @@ public interface GenericDAO * @return all items */ public Collection getAll(); + + /** + * Returns items containing the searched string + * + * @param searched the search character + * @param limit the max count of the result entities. if limit is <= 0 return all filtered entities + * @return searched items + */ + Collection getFiltered(String searched, int limit); } diff --git a/scm-core/src/main/java/sonia/scm/Manager.java b/scm-core/src/main/java/sonia/scm/Manager.java index 2925b5b6b4..62384e41d4 100644 --- a/scm-core/src/main/java/sonia/scm/Manager.java +++ b/scm-core/src/main/java/sonia/scm/Manager.java @@ -77,6 +77,15 @@ public interface Manager */ Collection getAll(); + /** + * Returns a {@link java.util.Collection} of filtered objects + * + * @param filter the searched string + * @param limit the max count of the result entities. if limit is <= 0 return all filtered entities + * @return all object in the store + */ + Collection getFiltered(String filter, int limit); + /** * Returns all object of the store sorted by the given {@link java.util.Comparator} * diff --git a/scm-core/src/main/java/sonia/scm/ManagerDecorator.java b/scm-core/src/main/java/sonia/scm/ManagerDecorator.java index 7b3f03ee8c..3938121668 100644 --- a/scm-core/src/main/java/sonia/scm/ManagerDecorator.java +++ b/scm-core/src/main/java/sonia/scm/ManagerDecorator.java @@ -91,6 +91,11 @@ public class ManagerDecorator implements Manager { decorated.refresh(object); } + @Override + public Collection getFiltered(String filter, int limit) { + return decorated.getFiltered(filter, limit); + } + @Override public T get(String id) { diff --git a/scm-core/src/main/java/sonia/scm/ModelObject.java b/scm-core/src/main/java/sonia/scm/ModelObject.java index cca9608ceb..5c387e9ca4 100644 --- a/scm-core/src/main/java/sonia/scm/ModelObject.java +++ b/scm-core/src/main/java/sonia/scm/ModelObject.java @@ -44,7 +44,7 @@ import java.io.Serializable; */ public interface ModelObject extends TypedObject, LastModifiedAware, Cloneable, Validateable, - Serializable + Serializable, ReducedModelObject { /** diff --git a/scm-core/src/main/java/sonia/scm/ReducedModelObject.java b/scm-core/src/main/java/sonia/scm/ReducedModelObject.java new file mode 100644 index 0000000000..b8db8c3ee0 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/ReducedModelObject.java @@ -0,0 +1,15 @@ +package sonia.scm; + + +/** + * This is a reduced form of a model object. + * It can be used as search result to avoid returning the whole object properties. + * + * @author Mohamed Karray + */ +public interface ReducedModelObject { + + String getId(); + + String getDisplayName(); +} diff --git a/scm-core/src/main/java/sonia/scm/group/Group.java b/scm-core/src/main/java/sonia/scm/group/Group.java index 5e7f596c58..6ee27be2fc 100644 --- a/scm-core/src/main/java/sonia/scm/group/Group.java +++ b/scm-core/src/main/java/sonia/scm/group/Group.java @@ -55,12 +55,12 @@ import java.util.List; /** * Organizes users into a group for easier permissions management. - * + * * TODO for 2.0: Use a set instead of a list for members * * @author Sebastian Sdorra */ -@StaticPermissions(value = "group", globalPermissions = {"create", "list"}) +@StaticPermissions(value = "group", globalPermissions = {"create", "list", "autocomplete"}) @XmlRootElement(name = "groups") @XmlAccessorType(XmlAccessType.FIELD) public class Group extends BasicPropertiesAware @@ -309,6 +309,11 @@ public class Group extends BasicPropertiesAware return name; } + @Override + public String getDisplayName() { + return description; + } + /** * Returns a timestamp of the last modified date of this group. * diff --git a/scm-core/src/main/java/sonia/scm/repository/Changeset.java b/scm-core/src/main/java/sonia/scm/repository/Changeset.java index 7397fecabe..7d3a8d6dbf 100644 --- a/scm-core/src/main/java/sonia/scm/repository/Changeset.java +++ b/scm-core/src/main/java/sonia/scm/repository/Changeset.java @@ -256,6 +256,11 @@ public class Changeset extends BasicPropertiesAware implements ModelObject { return id; } + @Override + public String getDisplayName() { + return id; + } + @Override public void setLastModified(Long timestamp) { throw new UnsupportedOperationException("changesets are immutable"); diff --git a/scm-core/src/main/java/sonia/scm/repository/Repository.java b/scm-core/src/main/java/sonia/scm/repository/Repository.java index cad36f2d88..7f2aab6865 100644 --- a/scm-core/src/main/java/sonia/scm/repository/Repository.java +++ b/scm-core/src/main/java/sonia/scm/repository/Repository.java @@ -60,11 +60,12 @@ import java.util.List; */ @StaticPermissions( value = "repository", - permissions = {"read", "modify", "delete", "healthCheck", "pull", "push", "permissionRead", "permissionWrite"} + permissions = {"read", "modify", "delete", "healthCheck", "pull", "push", "permissionRead", "permissionWrite"}, + globalPermissions = {"create", "autocomplete"} ) @XmlAccessorType(XmlAccessType.FIELD) @XmlRootElement(name = "repositories") -public class Repository extends BasicPropertiesAware implements ModelObject, PermissionObject { +public class Repository extends BasicPropertiesAware implements ModelObject, PermissionObject{ private static final long serialVersionUID = 3486560714961909711L; @@ -183,6 +184,11 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per return id; } + @Override + public String getDisplayName() { + return getNamespaceAndName().toString(); + } + @Override public Long getLastModified() { return lastModified; diff --git a/scm-core/src/main/java/sonia/scm/user/User.java b/scm-core/src/main/java/sonia/scm/user/User.java index 0d909bec8d..2a35f920a8 100644 --- a/scm-core/src/main/java/sonia/scm/user/User.java +++ b/scm-core/src/main/java/sonia/scm/user/User.java @@ -55,7 +55,7 @@ import java.security.Principal; * * @author Sebastian Sdorra */ -@StaticPermissions(value = "user", globalPermissions = {"create", "list"}) +@StaticPermissions(value = "user", globalPermissions = {"create", "list", "autocomplete"}) @XmlRootElement(name = "users") @XmlAccessorType(XmlAccessType.FIELD) public class User extends BasicPropertiesAware implements Principal, ModelObject, PermissionObject 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 f0711cd1e4..b6f2210d80 100644 --- a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java +++ b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java @@ -18,6 +18,7 @@ public class VndMediaType { public static final String INDEX = PREFIX + "index" + SUFFIX; public static final String USER = PREFIX + "user" + SUFFIX; public static final String GROUP = PREFIX + "group" + SUFFIX; + public static final String AUTOCOMPLETE = PREFIX + "autocomplete" + SUFFIX; public static final String REPOSITORY = PREFIX + "repository" + SUFFIX; public static final String PERMISSION = PREFIX + "permission" + SUFFIX; public static final String CHANGESET = PREFIX + "changeset" + SUFFIX; diff --git a/scm-core/src/test/java/sonia/scm/ManagerTest.java b/scm-core/src/test/java/sonia/scm/ManagerTest.java index 06c8eb3ea6..309a68e8ca 100644 --- a/scm-core/src/test/java/sonia/scm/ManagerTest.java +++ b/scm-core/src/test/java/sonia/scm/ManagerTest.java @@ -78,6 +78,11 @@ public class ManagerTest { return IntStream.range(0, givenItemCount).boxed().collect(toList()); } + @Override + public Collection getFiltered(String filter, int limit) { + return null; + } + @Override public Collection getAll(Comparator comparator) { return getAll(); } diff --git a/scm-dao-xml/src/main/java/sonia/scm/xml/AbstractXmlDAO.java b/scm-dao-xml/src/main/java/sonia/scm/xml/AbstractXmlDAO.java index 6b74fce7ca..f0907538cf 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/xml/AbstractXmlDAO.java +++ b/scm-dao-xml/src/main/java/sonia/scm/xml/AbstractXmlDAO.java @@ -41,11 +41,13 @@ import org.slf4j.LoggerFactory; import sonia.scm.GenericDAO; import sonia.scm.ModelObject; import sonia.scm.group.xml.XmlGroupDAO; - -//~--- JDK imports ------------------------------------------------------------ +import sonia.scm.store.ConfigurationStore; +import sonia.scm.util.AssertUtil; import java.util.Collection; -import sonia.scm.store.ConfigurationStore; +import java.util.stream.Collectors; + +//~--- JDK imports ------------------------------------------------------------ /** * @@ -234,6 +236,16 @@ public abstract class AbstractXmlDAO getFiltered(String searched, int limit) { + int size = db.values().size(); + AssertUtil.assertIsNotEmpty(searched); + return ImmutableList.copyOf(db.values().stream() + .filter(item -> item.getId().contains(searched) || (item.getDisplayName() != null && item.getDisplayName().contains(searched))) + .limit(limit <= 0 ? size : limit) + .collect(Collectors.toList())); + } + /** * Method description * diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AutoCompleteBadParamException.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AutoCompleteBadParamException.java new file mode 100644 index 0000000000..1f3f7177bd --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AutoCompleteBadParamException.java @@ -0,0 +1,11 @@ +package sonia.scm.api.v2.resources; + +public class AutoCompleteBadParamException extends Exception { + + public static final String PARAMETER_IS_REQUIRED = "The parameter is required."; + public static final String INVALID_PARAMETER_LENGTH = "Invalid parameter length."; + + public AutoCompleteBadParamException(String message) { + super(message); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AutoCompleteBadParamExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AutoCompleteBadParamExceptionMapper.java new file mode 100644 index 0000000000..d2b2eeaed3 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AutoCompleteBadParamExceptionMapper.java @@ -0,0 +1,16 @@ +package sonia.scm.api.v2.resources; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +@Provider +public class AutoCompleteBadParamExceptionMapper implements ExceptionMapper { + + @Override + public Response toResponse(AutoCompleteBadParamException exception) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(exception.getMessage()) + .build(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AutoCompleteResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AutoCompleteResource.java new file mode 100644 index 0000000000..5dfa78a762 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AutoCompleteResource.java @@ -0,0 +1,113 @@ +package sonia.scm.api.v2.resources; + +import com.webcohesion.enunciate.metadata.rs.ResponseCode; +import com.webcohesion.enunciate.metadata.rs.StatusCodes; +import org.apache.commons.lang.StringUtils; +import sonia.scm.group.GroupManager; +import sonia.scm.repository.RepositoryManager; +import sonia.scm.user.UserManager; +import sonia.scm.web.VndMediaType; + +import javax.inject.Inject; +import javax.validation.Valid; +import javax.ws.rs.DefaultValue; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Response; +import java.util.stream.Collectors; + +import static sonia.scm.api.v2.resources.AutoCompleteBadParamException.INVALID_PARAMETER_LENGTH; +import static sonia.scm.api.v2.resources.AutoCompleteBadParamException.PARAMETER_IS_REQUIRED; + +@Path(AutoCompleteResource.PATH) +public class AutoCompleteResource { + public static final String PATH = "v2/autocomplete/"; + public static final String DEFAULT_LIMIT = "5"; + public static final int MIN_SEARCHED_CHARS = 1; + + private ReducedObjectModelToDtoMapper mapper; + + private UserManager userManager; + private GroupManager groupManager; + private RepositoryManager repositoryManager; + + @Inject + public AutoCompleteResource(ReducedObjectModelToDtoMapper mapper, UserManager userManager, GroupManager groupManager, RepositoryManager repositoryManager) { + this.mapper = mapper; + this.userManager = userManager; + this.groupManager = groupManager; + this.repositoryManager = repositoryManager; + } + + @GET + @Path("user") + @Produces(VndMediaType.AUTOCOMPLETE) + @StatusCodes({ + @ResponseCode(code = 200, condition = "success"), + @ResponseCode(code = 400, condition = "if the searched string contains less than 2 characters"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"user:autocomplete\" privilege"), + @ResponseCode(code = 500, condition = "internal server error") + }) + public Response searchUser(@QueryParam("filter") String filter, + @DefaultValue(DEFAULT_LIMIT) @QueryParam("limit") Integer limit) throws AutoCompleteBadParamException { + validateParams(filter); + return Response.ok(userManager.getFiltered(filter, limit) + .stream() + .map(mapper::map) + .collect(Collectors.toList())) + .build(); + } + + @GET + @Path("group") + @Produces(VndMediaType.AUTOCOMPLETE) + @StatusCodes({ + @ResponseCode(code = 200, condition = "success"), + @ResponseCode(code = 400, condition = "if the searched string contains less than 2 characters"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"group:autocomplete\" privilege"), + @ResponseCode(code = 500, condition = "internal server error") + }) + public Response searchGroup(@Valid @QueryParam("filter") String filter, + @DefaultValue(DEFAULT_LIMIT) @QueryParam("limit") Integer limit) throws AutoCompleteBadParamException { + validateParams(filter); + return Response.ok(groupManager.getFiltered(filter, limit) + .stream() + .map(mapper::map) + .collect(Collectors.toList())) + .build(); + } + + @GET + @Path("repository") + @Produces(VndMediaType.AUTOCOMPLETE) + @StatusCodes({ + @ResponseCode(code = 200, condition = "success"), + @ResponseCode(code = 400, condition = "if the searched string contains less than 2 characters"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"repository:autocomplete\" privilege"), + @ResponseCode(code = 500, condition = "internal server error") + }) + public Response searchRepo(@Valid @QueryParam("filter") String filter, + @DefaultValue(DEFAULT_LIMIT) @QueryParam("limit") Integer limit) throws AutoCompleteBadParamException { + validateParams(filter); + return Response.ok(repositoryManager.getFiltered(filter, limit) + .stream() + .map(mapper::map) + .collect(Collectors.toList())) + .build(); + } + + void validateParams(String filter) throws AutoCompleteBadParamException { + if (StringUtils.isBlank(filter)) { + throw new AutoCompleteBadParamException(PARAMETER_IS_REQUIRED); + } + if (filter.length() <= MIN_SEARCHED_CHARS) { + throw new AutoCompleteBadParamException(INVALID_PARAMETER_LENGTH); + } + } + +} 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 03b5728627..6497cb9315 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 @@ -37,6 +37,8 @@ public class MapperModule extends AbstractModule { bind(FileObjectToFileObjectDtoMapper.class).to(Mappers.getMapper(FileObjectToFileObjectDtoMapper.class).getClass()); bind(ModificationsToDtoMapper.class).to(Mappers.getMapper(ModificationsToDtoMapper.class).getClass()); + bind(ReducedObjectModelToDtoMapper.class).to(Mappers.getMapper(ReducedObjectModelToDtoMapper.class).getClass()); + // no mapstruct required bind(UIPluginDtoMapper.class); bind(UIPluginDtoCollectionMapper.class); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ReducedObjectModelDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ReducedObjectModelDto.java new file mode 100644 index 0000000000..821af03995 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ReducedObjectModelDto.java @@ -0,0 +1,16 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.HalRepresentation; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +public class ReducedObjectModelDto extends HalRepresentation { + + private String id; + + private String displayName; +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ReducedObjectModelToDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ReducedObjectModelToDtoMapper.java new file mode 100644 index 0000000000..e188de7d65 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ReducedObjectModelToDtoMapper.java @@ -0,0 +1,13 @@ +package sonia.scm.api.v2.resources; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import sonia.scm.ReducedModelObject; + +@Mapper +public abstract class ReducedObjectModelToDtoMapper { + + @Mapping(target = "attributes", ignore = true) // We do not map HAL attributes + public abstract ReducedObjectModelDto map(ReducedModelObject modelObject); + +} 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 c3dcb6db8c..823ba5a580 100644 --- a/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupManager.java +++ b/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupManager.java @@ -242,6 +242,11 @@ public class DefaultGroupManager extends AbstractGroupManager return group; } + @Override + public Collection getFiltered(String filter, int limit) { + GroupPermissions.autocomplete().check(); + return groupDAO.getFiltered(filter, limit); + } /** * Method description * 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 8cb325b818..defd8f2068 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java @@ -243,6 +243,12 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager { return repository; } + @Override + public Collection getFiltered(String filter, int limit) { + RepositoryPermissions.autocomplete().check(); + return repositoryDAO.getFiltered(filter, limit); + } + @Override public Collection getAll(Comparator comparator) { List repositories = Lists.newArrayList(); diff --git a/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java b/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java index bf46eb2a6f..f2e50029bd 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java +++ b/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java @@ -52,9 +52,11 @@ import org.slf4j.LoggerFactory; import sonia.scm.cache.Cache; import sonia.scm.cache.CacheManager; import sonia.scm.group.GroupNames; +import sonia.scm.group.GroupPermissions; import sonia.scm.plugin.Extension; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryDAO; +import sonia.scm.repository.RepositoryPermissions; import sonia.scm.user.User; import sonia.scm.user.UserPermissions; import sonia.scm.util.Util; @@ -256,6 +258,9 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector collectGlobalPermissions(builder, user, groups); collectRepositoryPermissions(builder, user, groups); builder.add(canReadOwnUser(user)); + builder.add(getUserAutocompletePermission()); + builder.add(getGroupAutocompletePermission()); + builder.add(getRepoAutocompletePermission()); permissions = builder.build(); } @@ -264,6 +269,18 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector return info; } + private String getRepoAutocompletePermission() { + return RepositoryPermissions.autocomplete().asShiroString(); + } + + private String getGroupAutocompletePermission() { + return GroupPermissions.autocomplete().asShiroString(); + } + + private String getUserAutocompletePermission() { + return UserPermissions.autocomplete().asShiroString(); + } + private String canReadOwnUser(User user) { return UserPermissions.read(user.getName()).asShiroString(); } 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 876b0f094c..7757e0382a 100644 --- a/scm-webapp/src/main/java/sonia/scm/user/DefaultUserManager.java +++ b/scm-webapp/src/main/java/sonia/scm/user/DefaultUserManager.java @@ -300,6 +300,12 @@ public class DefaultUserManager extends AbstractUserManager return getAll(null); } + @Override + public Collection getFiltered(String filter, int limit) { + UserPermissions.autocomplete().check(); + return userDAO.getFiltered(filter, limit); + } + /** * Method description * diff --git a/scm-webapp/src/test/java/sonia/scm/api/rest/resources/AbstractManagerResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/rest/resources/AbstractManagerResourceTest.java index 41bcac3c6a..c2d7eef852 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/rest/resources/AbstractManagerResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/rest/resources/AbstractManagerResourceTest.java @@ -105,6 +105,11 @@ public class AbstractManagerResourceTest { return id; } + @Override + public String getDisplayName() { + return id; + } + @Override public void setLastModified(Long timestamp) { diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/AutoCompleteResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/AutoCompleteResourceTest.java new file mode 100644 index 0000000000..a6fd5f1f8a --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/AutoCompleteResourceTest.java @@ -0,0 +1,368 @@ +package sonia.scm.api.v2.resources; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.shiro.subject.Subject; +import org.apache.shiro.subject.support.SubjectThreadState; +import org.apache.shiro.util.ThreadContext; +import org.apache.shiro.util.ThreadState; +import org.assertj.core.util.Lists; +import org.jboss.resteasy.core.Dispatcher; +import org.jboss.resteasy.mock.MockHttpRequest; +import org.jboss.resteasy.mock.MockHttpResponse; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; +import sonia.scm.SCMContextProvider; +import sonia.scm.config.ScmConfiguration; +import sonia.scm.group.DefaultGroupManager; +import sonia.scm.group.Group; +import sonia.scm.group.GroupManager; +import sonia.scm.group.xml.XmlGroupDAO; +import sonia.scm.repository.DefaultRepositoryManager; +import sonia.scm.repository.NamespaceStrategy; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryManager; +import sonia.scm.repository.xml.XmlRepositoryDAO; +import sonia.scm.security.KeyGenerator; +import sonia.scm.store.ConfigurationStore; +import sonia.scm.store.ConfigurationStoreFactory; +import sonia.scm.user.DefaultUserManager; +import sonia.scm.user.User; +import sonia.scm.user.UserManager; +import sonia.scm.user.xml.XmlUserDAO; +import sonia.scm.web.VndMediaType; +import sonia.scm.xml.XmlDatabase; + +import javax.servlet.http.HttpServletResponse; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.initMocks; +import static sonia.scm.api.v2.resources.DispatcherMock.createDispatcher; + +@RunWith(MockitoJUnitRunner.Silent.class) +public class AutoCompleteResourceTest { + + public static final String URL = "/" + AutoCompleteResource.PATH; + private final Integer defaultLimit = Integer.valueOf(AutoCompleteResource.DEFAULT_LIMIT); + private Dispatcher dispatcher; + + private final Subject subject = mock(Subject.class); + private final ThreadState subjectThreadState = new SubjectThreadState(subject); + + + private XmlUserDAO userDaoMock; + + private XmlGroupDAO groupDaoMock; + + private XmlRepositoryDAO repoDaoMock; + private XmlDatabase xmlDB; + private ObjectMapper jsonObjectMapper = new ObjectMapper(); + + + @Before + public void prepareEnvironment() { + initMocks(this); + ConfigurationStoreFactory storeFactory = mock(ConfigurationStoreFactory.class); + ConfigurationStore storeConfig = mock(ConfigurationStore.class); + xmlDB = mock(XmlDatabase.class); + when(storeConfig.get()).thenReturn(xmlDB); + when(storeFactory.getStore(any(), any())).thenReturn(storeConfig); + XmlUserDAO userDao = new XmlUserDAO(storeFactory); + userDaoMock = spy(userDao); + XmlGroupDAO groupDAO = new XmlGroupDAO(storeFactory); + groupDaoMock = spy(groupDAO); + XmlRepositoryDAO repoDao = new XmlRepositoryDAO(storeFactory); + repoDaoMock = spy(repoDao); + ReducedObjectModelToDtoMapperImpl mapper = new ReducedObjectModelToDtoMapperImpl(); + UserManager userManager = new DefaultUserManager(userDaoMock); + GroupManager groupManager = new DefaultGroupManager(groupDaoMock); + RepositoryManager repositoryManager = new DefaultRepositoryManager(mock(ScmConfiguration.class), mock(SCMContextProvider.class), mock(KeyGenerator.class), repoDaoMock, new HashSet<>(), mock(NamespaceStrategy.class)); + AutoCompleteResource autoCompleteResource = new AutoCompleteResource(mapper, userManager, groupManager, repositoryManager); + dispatcher = createDispatcher(autoCompleteResource); + subjectThreadState.bind(); + ThreadContext.bind(subject); + when(subject.isPermitted(any(String.class))).thenReturn(true); + } + + @After + public void cleanupContext() { + ThreadContext.unbindSubject(); + } + + @Test + public void shouldGet400OnFailedParameterForUserSearch() throws Exception { + MockHttpRequest request = MockHttpRequest + .get("/" + AutoCompleteResource.PATH + "user") + .contentType(VndMediaType.AUTOCOMPLETE) + .accept(VndMediaType.AUTOCOMPLETE); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(400, response.getStatus()); + } + + @Test + public void shouldGet400IfParameterLengthLessThan2CharsForUserSearch() throws Exception { + MockHttpRequest request = MockHttpRequest + .get("/" + AutoCompleteResource.PATH + "user?filter=a") + .contentType(VndMediaType.AUTOCOMPLETE) + .accept(VndMediaType.AUTOCOMPLETE); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(400, response.getStatus()); + } + + @Test + public void shouldSearchUsers() throws Exception { + ArrayList users = Lists.newArrayList(createMockUser("YuCantFindMe", "ha ha"), createMockUser("user1", "User 1"), createMockUser("user2", "User 2")); + String searched = "user"; + when(xmlDB.values()).thenReturn(users); + MockHttpRequest request = MockHttpRequest + .get("/" + AutoCompleteResource.PATH + "user?filter=" + searched) + .contentType(VndMediaType.AUTOCOMPLETE) + .accept(VndMediaType.AUTOCOMPLETE); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + + assertResultSize(response, 2); + assertTrue(response.getContentAsString().contains("\"id\":\"user1\"")); + assertTrue(response.getContentAsString().contains("\"displayName\":\"User 1\"")); + assertTrue(response.getContentAsString().contains("\"id\":\"user2\"")); + assertTrue(response.getContentAsString().contains("\"displayName\":\"User 2\"")); + } + + @Test + public void shouldSearchUsersWithLimitLength() throws Exception { + List users = IntStream.range(0, 10).boxed().map(i -> createMockUser("user" + i, "User " + i)).collect(Collectors.toList()); + when(xmlDB.values()).thenReturn(users); + MockHttpRequest request = MockHttpRequest + .get("/" + AutoCompleteResource.PATH + "user?filter=user&limit=1") + .contentType(VndMediaType.AUTOCOMPLETE) + .accept(VndMediaType.AUTOCOMPLETE); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + verify(userDaoMock).getFiltered(eq("user"), eq(1)); + assertResultSize(response, 1); + } + + @Test + public void shouldSearchUsersWithDefaultLimitLength() throws Exception { + List userList = IntStream.range(0, 10).boxed().map(i -> createMockUser("user" + i, "User " + i)).collect(Collectors.toList()); + when(xmlDB.values()).thenReturn(userList); + MockHttpRequest request = MockHttpRequest + .get("/" + AutoCompleteResource.PATH + "user?filter=user") + .contentType(VndMediaType.AUTOCOMPLETE) + .accept(VndMediaType.AUTOCOMPLETE); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + verify(userDaoMock).getFiltered(eq("user"), eq(defaultLimit)); + assertResultSize(response, defaultLimit); + } + + @Test + public void shouldGet400OnFailedParameterForGroupSearch() throws Exception { + MockHttpRequest request = MockHttpRequest + .get("/" + AutoCompleteResource.PATH + "group") + .contentType(VndMediaType.AUTOCOMPLETE) + .accept(VndMediaType.AUTOCOMPLETE); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(400, response.getStatus()); + } + + @Test + public void shouldGet400IfParameterLengthLessThan2CharsForGroupSearch() throws Exception { + MockHttpRequest request = MockHttpRequest + .get("/" + AutoCompleteResource.PATH + "group?filter=a") + .contentType(VndMediaType.AUTOCOMPLETE) + .accept(VndMediaType.AUTOCOMPLETE); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(400, response.getStatus()); + } + + @Test + public void shouldSearchGroups() throws Exception { + ArrayList groups = Lists.newArrayList(createMockGroup("YuCantFindMe"), createMockGroup("group_1"), createMockGroup("group_2")); + String searched = "group"; + when(xmlDB.values()).thenReturn(groups); + MockHttpRequest request = MockHttpRequest + .get("/" + AutoCompleteResource.PATH + "group?filter=" + searched) + .contentType(VndMediaType.AUTOCOMPLETE) + .accept(VndMediaType.AUTOCOMPLETE); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + assertResultSize(response, 2); + assertTrue(response.getContentAsString().contains("\"id\":\"group_1\"")); + assertTrue(response.getContentAsString().contains("\"displayName\":\"group_1\"")); + assertTrue(response.getContentAsString().contains("\"id\":\"group_2\"")); + assertTrue(response.getContentAsString().contains("\"displayName\":\"group_2\"")); + } + + @Test + public void shouldSearchGroupsWithLimitLength() throws Exception { + List groups = IntStream.range(0, 10).boxed().map(i -> createMockGroup("group_" + i)).collect(Collectors.toList()); + when(xmlDB.values()).thenReturn(groups); + MockHttpRequest request = MockHttpRequest + .get("/" + AutoCompleteResource.PATH + "group?filter=group&limit=1") + .contentType(VndMediaType.AUTOCOMPLETE) + .accept(VndMediaType.AUTOCOMPLETE); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + verify(groupDaoMock).getFiltered(eq("group"), eq(1)); + assertResultSize(response, 1); + } + + @Test + public void shouldSearchGroupsWithDefaultLimitLength() throws Exception { + List groups = IntStream.range(0, 10).boxed().map(i -> createMockGroup("group_" + i)).collect(Collectors.toList()); + when(xmlDB.values()).thenReturn(groups); + MockHttpRequest request = MockHttpRequest + .get("/" + AutoCompleteResource.PATH + "group?filter=group") + .contentType(VndMediaType.AUTOCOMPLETE) + .accept(VndMediaType.AUTOCOMPLETE); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + verify(groupDaoMock).getFiltered(eq("group"), eq(defaultLimit)); + assertResultSize(response, defaultLimit); + } + + + @Test + public void shouldGet400OnFailedParameterForRepoSearch() throws Exception { + MockHttpRequest request = MockHttpRequest + .get("/" + AutoCompleteResource.PATH + "repository") + .contentType(VndMediaType.AUTOCOMPLETE) + .accept(VndMediaType.AUTOCOMPLETE); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(400, response.getStatus()); + } + + @Test + public void shouldGet400IfParameterLengthLessThan2CharsForRepoSearch() throws Exception { + MockHttpRequest request = MockHttpRequest + .get("/" + AutoCompleteResource.PATH + "repository?filter=a") + .contentType(VndMediaType.AUTOCOMPLETE) + .accept(VndMediaType.AUTOCOMPLETE); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(400, response.getStatus()); + } + + @Test + public void shouldSearchRepos() throws Exception { + List repos = Lists.newArrayList(createMockRepo("YCannotFindMe"), createMockRepo("repo1"), createMockRepo("repo2")); + when(xmlDB.values()).thenReturn(repos); + MockHttpRequest request = MockHttpRequest + .get("/" + AutoCompleteResource.PATH + "repository?filter=repo") + .contentType(VndMediaType.AUTOCOMPLETE) + .accept(VndMediaType.AUTOCOMPLETE); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + assertResultSize(response, 2); + assertTrue(response.getContentAsString().contains("\"displayName\":\"space/repo1\"")); + assertTrue(response.getContentAsString().contains("\"displayName\":\"space/repo2\"")); + } + + @Test + public void shouldSearchReposWithLimitLength() throws Exception { + List repositories = IntStream.range(0, 10).boxed().map(i -> createMockRepo("repo" + i)).collect(Collectors.toList()); + when(xmlDB.values()).thenReturn(repositories); + MockHttpRequest request = MockHttpRequest + .get("/" + AutoCompleteResource.PATH + "repository?filter=repo&limit=1") + .contentType(VndMediaType.AUTOCOMPLETE) + .accept(VndMediaType.AUTOCOMPLETE); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + verify(repoDaoMock).getFiltered(eq("repo"), eq(1)); + assertResultSize(response, 1); + } + + @Test + public void shouldSearchReposWithDefaultLimitLength() throws Exception { + List repositories = IntStream.range(0, 10).boxed().map(i -> createMockRepo("repo" + i)).collect(Collectors.toList()); + when(xmlDB.values()).thenReturn(repositories); + MockHttpRequest request = MockHttpRequest + .get("/" + AutoCompleteResource.PATH + "repository?filter=repo") + .contentType(VndMediaType.AUTOCOMPLETE) + .accept(VndMediaType.AUTOCOMPLETE); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + verify(repoDaoMock).getFiltered(eq("repo"), eq(defaultLimit)); + assertResultSize(response, defaultLimit); + } + + + private User createMockUser(String id, String name) { + return new User(id, name, "em@l.de"); + } + + + private Group createMockGroup(String name) { + Group group = new Group("type", name); + group.setDescription(name); + return group; + } + + private Repository createMockRepo(String repository) { + return new Repository("id", "git", "space", repository); + } + + private void assertResultSize(MockHttpResponse response, int size) throws java.io.IOException { + ReducedObjectModelDto[] reducedObjectModelDtos = jsonObjectMapper.readValue(response.getContentAsString(), ReducedObjectModelDto[].class); + assertTrue(reducedObjectModelDtos.length == size); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DispatcherMock.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DispatcherMock.java index a1abdb6ff4..d28e96d8a4 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DispatcherMock.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DispatcherMock.java @@ -17,6 +17,7 @@ public class DispatcherMock { dispatcher.getProviderFactory().registerProvider(InternalRepositoryExceptionMapper.class); dispatcher.getProviderFactory().registerProvider(ChangePasswordNotAllowedExceptionMapper.class); dispatcher.getProviderFactory().registerProvider(InvalidPasswordExceptionMapper.class); + dispatcher.getProviderFactory().registerProvider(AutoCompleteBadParamExceptionMapper.class); return dispatcher; } } 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 6ab1dc6aeb..3d33fa0eb6 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 @@ -23,7 +23,6 @@ import javax.servlet.http.HttpServletResponse; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; -import java.text.MessageFormat; import static java.util.Collections.singletonList; import static org.junit.Assert.assertEquals; diff --git a/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java b/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java index 5e7963ef1d..291369e7b8 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java @@ -161,8 +161,8 @@ public class DefaultAuthorizationCollectorTest { AuthorizationInfo authInfo = collector.collect(); assertThat(authInfo.getRoles(), Matchers.contains(Role.USER)); - assertThat(authInfo.getStringPermissions(), hasSize(1)); - assertThat(authInfo.getStringPermissions(), contains("user:read:trillian")); + assertThat(authInfo.getStringPermissions(), hasSize(4)); + assertThat(authInfo.getStringPermissions(), containsInAnyOrder("user:autocomplete", "group:autocomplete", "repository:autocomplete", "user:read:trillian")); assertThat(authInfo.getObjectPermissions(), nullValue()); } @@ -209,7 +209,7 @@ public class DefaultAuthorizationCollectorTest { AuthorizationInfo authInfo = collector.collect(); assertThat(authInfo.getRoles(), Matchers.containsInAnyOrder(Role.USER)); assertThat(authInfo.getObjectPermissions(), nullValue()); - assertThat(authInfo.getStringPermissions(), containsInAnyOrder("repository:read,pull:one", "repository:read,pull,push:two", "user:read:trillian")); + assertThat(authInfo.getStringPermissions(), containsInAnyOrder("user:autocomplete", "group:autocomplete","repository:autocomplete", "repository:read,pull:one", "repository:read,pull,push:two", "user:read:trillian")); } /** @@ -230,7 +230,7 @@ public class DefaultAuthorizationCollectorTest { AuthorizationInfo authInfo = collector.collect(); assertThat(authInfo.getRoles(), Matchers.containsInAnyOrder(Role.USER)); assertThat(authInfo.getObjectPermissions(), nullValue()); - assertThat(authInfo.getStringPermissions(), containsInAnyOrder("one:one", "two:two", "user:read:trillian")); + assertThat(authInfo.getStringPermissions(), containsInAnyOrder("one:one", "two:two", "user:read:trillian", "user:autocomplete" , "group:autocomplete", "repository:autocomplete")); } private void authenticate(User user, String group, String... groups) { From 8908bf0646eefb6452ae1210a3af538519b7535d Mon Sep 17 00:00:00 2001 From: Mohamed Karray Date: Mon, 8 Oct 2018 14:33:48 +0200 Subject: [PATCH 2/9] try lerna exec with --bail=false --- scm-ui-components/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scm-ui-components/package.json b/scm-ui-components/package.json index 73c20625bd..b9447e45f9 100644 --- a/scm-ui-components/package.json +++ b/scm-ui-components/package.json @@ -5,7 +5,7 @@ "scripts": { "bootstrap": "lerna bootstrap", "link": "lerna exec -- yarn link", - "unlink": "lerna exec --no-bail -- yarn unlink" + "unlink": "lerna exec --bail=false -- yarn unlink" }, "devDependencies": { "lerna": "^3.2.1" From 9f02102f42324586595520664f6f4f9802ae3df5 Mon Sep 17 00:00:00 2001 From: Mohamed Karray Date: Mon, 8 Oct 2018 15:51:29 +0200 Subject: [PATCH 3/9] skip yarn unlink --- scm-ui-components/lerna.json | 2 +- scm-ui-components/pom.xml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/scm-ui-components/lerna.json b/scm-ui-components/lerna.json index 02962cf634..37166b8655 100644 --- a/scm-ui-components/lerna.json +++ b/scm-ui-components/lerna.json @@ -2,6 +2,6 @@ "packages": [ "packages/*" ], - "version": "0.0.0", + "version": "3.2.1", "npmClient": "yarn" } diff --git a/scm-ui-components/pom.xml b/scm-ui-components/pom.xml index f5544a9e24..830945a8a3 100644 --- a/scm-ui-components/pom.xml +++ b/scm-ui-components/pom.xml @@ -55,6 +55,7 @@ run + true From 41b47b13dcfbcbabf2afe5319bdcff4e7ec4836d Mon Sep 17 00:00:00 2001 From: Mohamed Karray Date: Tue, 9 Oct 2018 08:55:57 +0200 Subject: [PATCH 4/9] use javax validation --- .../java/sonia/scm/repository/Repository.java | 2 +- .../java/sonia/scm/xml/AbstractXmlDAO.java | 3 +- .../AutoCompleteBadParamException.java | 11 ------ .../AutoCompleteBadParamExceptionMapper.java | 16 --------- .../v2/resources/AutoCompleteResource.java | 35 +++++++------------ .../scm/api/v2/resources/DispatcherMock.java | 1 - 6 files changed, 16 insertions(+), 52 deletions(-) delete mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/AutoCompleteBadParamException.java delete mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/AutoCompleteBadParamExceptionMapper.java diff --git a/scm-core/src/main/java/sonia/scm/repository/Repository.java b/scm-core/src/main/java/sonia/scm/repository/Repository.java index 7f2aab6865..97915eeaa5 100644 --- a/scm-core/src/main/java/sonia/scm/repository/Repository.java +++ b/scm-core/src/main/java/sonia/scm/repository/Repository.java @@ -186,7 +186,7 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per @Override public String getDisplayName() { - return getNamespaceAndName().toString(); + return getNamespace() + "/" + getName(); } @Override diff --git a/scm-dao-xml/src/main/java/sonia/scm/xml/AbstractXmlDAO.java b/scm-dao-xml/src/main/java/sonia/scm/xml/AbstractXmlDAO.java index f0907538cf..ee1affe252 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/xml/AbstractXmlDAO.java +++ b/scm-dao-xml/src/main/java/sonia/scm/xml/AbstractXmlDAO.java @@ -35,6 +35,7 @@ package sonia.scm.xml; //~--- non-JDK imports -------------------------------------------------------- import com.google.common.collect.ImmutableList; +import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -241,7 +242,7 @@ public abstract class AbstractXmlDAO item.getId().contains(searched) || (item.getDisplayName() != null && item.getDisplayName().contains(searched))) + .filter(item -> StringUtils.containsIgnoreCase(item.getId(), searched) || (item.getDisplayName() != null && StringUtils.containsIgnoreCase(item.getDisplayName() , searched))) .limit(limit <= 0 ? size : limit) .collect(Collectors.toList())); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AutoCompleteBadParamException.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AutoCompleteBadParamException.java deleted file mode 100644 index 1f3f7177bd..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AutoCompleteBadParamException.java +++ /dev/null @@ -1,11 +0,0 @@ -package sonia.scm.api.v2.resources; - -public class AutoCompleteBadParamException extends Exception { - - public static final String PARAMETER_IS_REQUIRED = "The parameter is required."; - public static final String INVALID_PARAMETER_LENGTH = "Invalid parameter length."; - - public AutoCompleteBadParamException(String message) { - super(message); - } -} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AutoCompleteBadParamExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AutoCompleteBadParamExceptionMapper.java deleted file mode 100644 index d2b2eeaed3..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AutoCompleteBadParamExceptionMapper.java +++ /dev/null @@ -1,16 +0,0 @@ -package sonia.scm.api.v2.resources; - -import javax.ws.rs.core.Response; -import javax.ws.rs.ext.ExceptionMapper; -import javax.ws.rs.ext.Provider; - -@Provider -public class AutoCompleteBadParamExceptionMapper implements ExceptionMapper { - - @Override - public Response toResponse(AutoCompleteBadParamException exception) { - return Response.status(Response.Status.BAD_REQUEST) - .entity(exception.getMessage()) - .build(); - } -} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AutoCompleteResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AutoCompleteResource.java index 5dfa78a762..eebec9baa4 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AutoCompleteResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AutoCompleteResource.java @@ -2,14 +2,14 @@ package sonia.scm.api.v2.resources; import com.webcohesion.enunciate.metadata.rs.ResponseCode; import com.webcohesion.enunciate.metadata.rs.StatusCodes; -import org.apache.commons.lang.StringUtils; +import org.hibernate.validator.constraints.NotEmpty; import sonia.scm.group.GroupManager; import sonia.scm.repository.RepositoryManager; import sonia.scm.user.UserManager; import sonia.scm.web.VndMediaType; import javax.inject.Inject; -import javax.validation.Valid; +import javax.validation.constraints.Size; import javax.ws.rs.DefaultValue; import javax.ws.rs.GET; import javax.ws.rs.Path; @@ -18,14 +18,16 @@ import javax.ws.rs.QueryParam; import javax.ws.rs.core.Response; import java.util.stream.Collectors; -import static sonia.scm.api.v2.resources.AutoCompleteBadParamException.INVALID_PARAMETER_LENGTH; -import static sonia.scm.api.v2.resources.AutoCompleteBadParamException.PARAMETER_IS_REQUIRED; @Path(AutoCompleteResource.PATH) public class AutoCompleteResource { public static final String PATH = "v2/autocomplete/"; public static final String DEFAULT_LIMIT = "5"; - public static final int MIN_SEARCHED_CHARS = 1; + public static final int MIN_SEARCHED_CHARS = 2; + + public static final String PARAMETER_IS_REQUIRED = "The parameter is required."; + public static final String INVALID_PARAMETER_LENGTH = "Invalid parameter length."; + private ReducedObjectModelToDtoMapper mapper; @@ -51,9 +53,8 @@ public class AutoCompleteResource { @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"user:autocomplete\" privilege"), @ResponseCode(code = 500, condition = "internal server error") }) - public Response searchUser(@QueryParam("filter") String filter, - @DefaultValue(DEFAULT_LIMIT) @QueryParam("limit") Integer limit) throws AutoCompleteBadParamException { - validateParams(filter); + public Response searchUser(@NotEmpty(message = PARAMETER_IS_REQUIRED) @Size(min = MIN_SEARCHED_CHARS, message = INVALID_PARAMETER_LENGTH) @QueryParam("filter") String filter, + @DefaultValue(DEFAULT_LIMIT) @QueryParam("limit") Integer limit) { return Response.ok(userManager.getFiltered(filter, limit) .stream() .map(mapper::map) @@ -71,9 +72,8 @@ public class AutoCompleteResource { @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"group:autocomplete\" privilege"), @ResponseCode(code = 500, condition = "internal server error") }) - public Response searchGroup(@Valid @QueryParam("filter") String filter, - @DefaultValue(DEFAULT_LIMIT) @QueryParam("limit") Integer limit) throws AutoCompleteBadParamException { - validateParams(filter); + public Response searchGroup(@NotEmpty(message = PARAMETER_IS_REQUIRED) @Size(min = MIN_SEARCHED_CHARS, message = INVALID_PARAMETER_LENGTH) @QueryParam("filter") String filter, + @DefaultValue(DEFAULT_LIMIT) @QueryParam("limit") Integer limit) { return Response.ok(groupManager.getFiltered(filter, limit) .stream() .map(mapper::map) @@ -91,9 +91,8 @@ public class AutoCompleteResource { @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"repository:autocomplete\" privilege"), @ResponseCode(code = 500, condition = "internal server error") }) - public Response searchRepo(@Valid @QueryParam("filter") String filter, - @DefaultValue(DEFAULT_LIMIT) @QueryParam("limit") Integer limit) throws AutoCompleteBadParamException { - validateParams(filter); + public Response searchRepo(@NotEmpty(message = PARAMETER_IS_REQUIRED) @Size(min = MIN_SEARCHED_CHARS, message = INVALID_PARAMETER_LENGTH) @QueryParam("filter") String filter, + @DefaultValue(DEFAULT_LIMIT) @QueryParam("limit") Integer limit) { return Response.ok(repositoryManager.getFiltered(filter, limit) .stream() .map(mapper::map) @@ -101,13 +100,5 @@ public class AutoCompleteResource { .build(); } - void validateParams(String filter) throws AutoCompleteBadParamException { - if (StringUtils.isBlank(filter)) { - throw new AutoCompleteBadParamException(PARAMETER_IS_REQUIRED); - } - if (filter.length() <= MIN_SEARCHED_CHARS) { - throw new AutoCompleteBadParamException(INVALID_PARAMETER_LENGTH); - } - } } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DispatcherMock.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DispatcherMock.java index d28e96d8a4..a1abdb6ff4 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DispatcherMock.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DispatcherMock.java @@ -17,7 +17,6 @@ public class DispatcherMock { dispatcher.getProviderFactory().registerProvider(InternalRepositoryExceptionMapper.class); dispatcher.getProviderFactory().registerProvider(ChangePasswordNotAllowedExceptionMapper.class); dispatcher.getProviderFactory().registerProvider(InvalidPasswordExceptionMapper.class); - dispatcher.getProviderFactory().registerProvider(AutoCompleteBadParamExceptionMapper.class); return dispatcher; } } From de805788fc181e2e9757e73ad39fb0fe90595599 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 9 Oct 2018 15:35:54 +0200 Subject: [PATCH 5/9] Backed out changeset cee469c7213b --- scm-ui-components/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scm-ui-components/package.json b/scm-ui-components/package.json index b9447e45f9..73c20625bd 100644 --- a/scm-ui-components/package.json +++ b/scm-ui-components/package.json @@ -5,7 +5,7 @@ "scripts": { "bootstrap": "lerna bootstrap", "link": "lerna exec -- yarn link", - "unlink": "lerna exec --bail=false -- yarn unlink" + "unlink": "lerna exec --no-bail -- yarn unlink" }, "devDependencies": { "lerna": "^3.2.1" From e6b241727f5318f65cb2f3cad5fc50252032436e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 9 Oct 2018 15:37:09 +0200 Subject: [PATCH 6/9] Backed out changeset c0ce0773f13d --- scm-ui-components/pom.xml | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/scm-ui-components/pom.xml b/scm-ui-components/pom.xml index 830945a8a3..f49d2070cd 100644 --- a/scm-ui-components/pom.xml +++ b/scm-ui-components/pom.xml @@ -48,17 +48,6 @@ - - unlink - package - - run - - - true - - - link package @@ -66,7 +55,17 @@ run - + + + + + unlink + clean + + run + + + From 433cf2a02e4e8c392db902bc190c60c666118d84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 9 Oct 2018 15:42:21 +0200 Subject: [PATCH 7/9] Revert to status of v2.0.0-m3 --- scm-ui-components/lerna.json | 2 +- scm-ui-components/pom.xml | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/scm-ui-components/lerna.json b/scm-ui-components/lerna.json index 37166b8655..02962cf634 100644 --- a/scm-ui-components/lerna.json +++ b/scm-ui-components/lerna.json @@ -2,6 +2,6 @@ "packages": [ "packages/*" ], - "version": "3.2.1", + "version": "0.0.0", "npmClient": "yarn" } diff --git a/scm-ui-components/pom.xml b/scm-ui-components/pom.xml index f49d2070cd..f5544a9e24 100644 --- a/scm-ui-components/pom.xml +++ b/scm-ui-components/pom.xml @@ -49,7 +49,7 @@ - link + unlink package run @@ -59,13 +59,13 @@ - unlink - clean + link + package run - + From 30ac4be7bfbe7223429053628186ad8ed76d8a71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 9 Oct 2018 16:00:03 +0200 Subject: [PATCH 8/9] Backout ui changes --- scm-ui-components/lerna.json | 2 +- scm-ui-components/package.json | 2 +- scm-ui-components/pom.xml | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/scm-ui-components/lerna.json b/scm-ui-components/lerna.json index 37166b8655..02962cf634 100644 --- a/scm-ui-components/lerna.json +++ b/scm-ui-components/lerna.json @@ -2,6 +2,6 @@ "packages": [ "packages/*" ], - "version": "3.2.1", + "version": "0.0.0", "npmClient": "yarn" } diff --git a/scm-ui-components/package.json b/scm-ui-components/package.json index b9447e45f9..73c20625bd 100644 --- a/scm-ui-components/package.json +++ b/scm-ui-components/package.json @@ -5,7 +5,7 @@ "scripts": { "bootstrap": "lerna bootstrap", "link": "lerna exec -- yarn link", - "unlink": "lerna exec --bail=false -- yarn unlink" + "unlink": "lerna exec --no-bail -- yarn unlink" }, "devDependencies": { "lerna": "^3.2.1" diff --git a/scm-ui-components/pom.xml b/scm-ui-components/pom.xml index 830945a8a3..f5544a9e24 100644 --- a/scm-ui-components/pom.xml +++ b/scm-ui-components/pom.xml @@ -55,7 +55,6 @@ run - true From 29faaf1c032227596ffa836dd51111797df6fe5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 9 Oct 2018 15:08:33 +0000 Subject: [PATCH 9/9] Close branch feature/autocomplet_endpoint_v2