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 8bffc22260..e581de99af 100644 --- a/scm-core/src/main/java/sonia/scm/group/Group.java +++ b/scm-core/src/main/java/sonia/scm/group/Group.java @@ -66,7 +66,7 @@ import javax.xml.bind.annotation.XmlRootElement; @XmlRootElement(name = "groups") @XmlAccessorType(XmlAccessType.FIELD) public class Group extends BasicPropertiesAware - implements ModelObject, Iterable, PermissionObject + implements ModelObject, PermissionObject { /** Field description */ @@ -245,7 +245,6 @@ public class Group extends BasicPropertiesAware * * @return a {@link java.util.Iterator} for the members of this {@link Group} */ - @Override public Iterator iterator() { return getMembers().iterator(); 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 d81986cd6e..ebed9f02f6 100644 --- a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java +++ b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java @@ -13,6 +13,7 @@ public class VndMediaType { private static final String SUFFIX = "+json;v=" + VERSION; public static final String USER = PREFIX + "user" + SUFFIX; + public static final String GROUP = PREFIX + "group" + SUFFIX; public static final String USER_COLLECTION = PREFIX + "userCollection" + SUFFIX; private VndMediaType() { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/Group2GroupDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/Group2GroupDtoMapper.java new file mode 100644 index 0000000000..42f061448c --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/Group2GroupDtoMapper.java @@ -0,0 +1,91 @@ +package sonia.scm.api.v2.resources; + +import com.google.inject.Inject; +import de.otto.edison.hal.Link; +import de.otto.edison.hal.Links; +import org.mapstruct.AfterMapping; +import org.mapstruct.Context; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; +import org.mapstruct.Qualifier; +import sonia.scm.PageResult; +import sonia.scm.group.Group; +import sonia.scm.group.GroupPermissions; +import sonia.scm.user.User; +import sonia.scm.util.AssertUtil; + +import javax.ws.rs.core.UriInfo; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.time.Instant; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import static de.otto.edison.hal.Link.link; +import static de.otto.edison.hal.Links.linkingTo; +import static java.util.Arrays.asList; + +@Mapper +public abstract class Group2GroupDtoMapper { + + public abstract GroupDto groupToGroupDto(Group group, @Context UriInfo uriInfo); + + @Inject + private User2UserDtoMapper user2UserDtoMapper; + + @AfterMapping + void appendLinks(Group group, @MappingTarget GroupDto target, @Context UriInfo uriInfo) { + LinkBuilder groupLinkBuilder = new LinkBuilder(uriInfo, GroupV2Resource.class, GroupSubResource.class); + + Links.Builder linksBuilder = linkingTo() + .self(groupLinkBuilder.method("getGroupSubResource").parameters(target.getName()).method("get").parameters().href()); + if (GroupPermissions.delete(group).isPermitted()) { + linksBuilder + .single(link("delete", groupLinkBuilder.method("getGroupSubResource").parameters(target.getName()).method("delete").parameters().href())); + } + if (GroupPermissions.modify(group).isPermitted()) { + linksBuilder + .single(link("update", groupLinkBuilder.method("getGroupSubResource").parameters(target.getName()).method("update").parameters().href())); + } + target.add(linksBuilder.build()); + } + + @AfterMapping + void appendUserLinks(Group group, @MappingTarget GroupDto target, @Context UriInfo uriInfo) { + Links.Builder linksBuilder = linkingTo(); + LinkBuilder userLinkBuilder = new LinkBuilder(uriInfo, UserV2Resource.class, UserSubResource.class); + group.getMembers().forEach(name -> linksBuilder.array(Link.link("users", userLinkBuilder.method("getUserSubResource").parameters(name).method("get").parameters().href()))); + + target.add(linksBuilder.build()); + } + + @AfterMapping + void embedUsers(Group group, @MappingTarget GroupDto target, @Context UriInfo uriInfo) { + List users = group.getMembers().stream().map(this::createUser).map(u -> user2UserDtoMapper.userToUserDto(u, uriInfo)).collect(Collectors.toList()); + target.withEmbedded("users", users); + } + + private User createUser(String ich) { + User user = new User(ich); + user.setCreationDate(0L); + return user; + } + + @Mapping(target = "creationDate") + Instant mapTime(Long epochMilli) { + AssertUtil.assertIsNotNull(epochMilli); + return Instant.ofEpochMilli(epochMilli); + } + + @Mapping(target = "lastModified") + Optional mapOptionalTime(Long epochMilli) { + return Optional + .ofNullable(epochMilli) + .map(Instant::ofEpochMilli); + } +} 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 new file mode 100644 index 0000000000..99245e1d89 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupCollectionResource.java @@ -0,0 +1,4 @@ +package sonia.scm.api.v2.resources; + +public class GroupCollectionResource { +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupDto.java new file mode 100644 index 0000000000..0e412a9f92 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupDto.java @@ -0,0 +1,31 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.HalRepresentation; +import de.otto.edison.hal.Links; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.time.Instant; +import java.util.List; +import java.util.Optional; + +@Data @Accessors @NoArgsConstructor +public class GroupDto extends HalRepresentation { + + private Instant creationDate; + private String description; + private Optional lastModified; + private String name; + private String type; + + @Override + protected HalRepresentation add(Links links) { + return super.add(links); + } + + @Override + protected HalRepresentation withEmbedded(String rel, List embeddedItems) { + return super.withEmbedded(rel, embeddedItems); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupSubResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupSubResource.java new file mode 100644 index 0000000000..6de603ffd1 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupSubResource.java @@ -0,0 +1,52 @@ +package sonia.scm.api.v2.resources; + +import sonia.scm.group.Group; +import sonia.scm.web.VndMediaType; + +import javax.inject.Inject; +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.core.Context; +import javax.ws.rs.core.Request; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; +import java.util.stream.IntStream; +import java.util.stream.StreamSupport; + +import static java.util.stream.Collectors.toList; + +@Produces(VndMediaType.GROUP) +public class GroupSubResource { + + private final Group2GroupDtoMapper groupToGroupDtoMapper; + + @Inject + public GroupSubResource(Group2GroupDtoMapper groupToGroupDtoMapper) { + this.groupToGroupDtoMapper = groupToGroupDtoMapper; + } + + @Path("") + @GET + public Response get(@Context Request request, @Context UriInfo uriInfo, @PathParam("id") String id) { + Group group = new Group("admin", "admin"); + group.setCreationDate(System.currentTimeMillis()); + group.setMembers(IntStream.range(1, 10).mapToObj(n -> "user" + n).collect(toList())); + return Response.ok(groupToGroupDtoMapper.groupToGroupDto(group, uriInfo)).build(); + } + + @Path("") + @DELETE + public Response delete(@PathParam("id") String id) { + throw new RuntimeException(); + } + + @Path("") + @PUT + public Response update(@PathParam("id") String id) { + throw new RuntimeException(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupV2Resource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupV2Resource.java new file mode 100644 index 0000000000..a56fca3ad5 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupV2Resource.java @@ -0,0 +1,30 @@ +package sonia.scm.api.v2.resources; + +import com.google.inject.Inject; + +import javax.ws.rs.Path; + +@Path(GroupV2Resource.GROUPS_PATH_V2) +public class GroupV2Resource { + + public static final String GROUPS_PATH_V2 = "v2/groups/"; + + private final GroupCollectionResource groupCollectionResource; + private final GroupSubResource groupSubResource; + + @Inject + public GroupV2Resource(GroupCollectionResource groupCollectionResource, GroupSubResource groupSubResource) { + this.groupCollectionResource = groupCollectionResource; + this.groupSubResource = groupSubResource; + } + + @Path("") + public GroupCollectionResource getGroupCollectionResource() { + return groupCollectionResource; + } + + @Path("{id}") + public GroupSubResource getGroupSubResource() { + return groupSubResource; + } +} 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 7eddc7be8a..8d0f3e086e 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 @@ -2,11 +2,14 @@ package sonia.scm.api.v2.resources; import com.google.inject.AbstractModule; import org.mapstruct.factory.Mappers; +import sonia.scm.group.Group; public class MapperModule extends AbstractModule { @Override protected void configure() { bind(UserDto2UserMapper.class).to(Mappers.getMapper(UserDto2UserMapper.class).getClass()); bind(User2UserDtoMapper.class).to(Mappers.getMapper(User2UserDtoMapper.class).getClass()); + + bind(Group2GroupDtoMapper.class).to(Mappers.getMapper(Group2GroupDtoMapper.class).getClass()); } } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/Group2GroupDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/Group2GroupDtoMapperTest.java new file mode 100644 index 0000000000..448db3f82e --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/Group2GroupDtoMapperTest.java @@ -0,0 +1,61 @@ +package sonia.scm.api.v2.resources; + +import org.apache.shiro.subject.Subject; +import org.apache.shiro.subject.support.SubjectThreadState; +import org.apache.shiro.util.ThreadContext; +import org.apache.shiro.util.ThreadState; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mapstruct.factory.Mappers; +import sonia.scm.group.Group; + +import javax.ws.rs.core.UriInfo; +import java.net.URI; +import java.net.URISyntaxException; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class Group2GroupDtoMapperTest { + + private final Group2GroupDtoMapper mapper = Mappers.getMapper(Group2GroupDtoMapper.class); + private final UriInfo uriInfo = mock(UriInfo.class); + private final Subject subject = mock(Subject.class); + private final ThreadState subjectThreadState = new SubjectThreadState(subject); + + private URI expectedBaseUri; + + @Before + public void init() throws URISyntaxException { + URI baseUri = new URI("http://example.com/base/"); + expectedBaseUri = baseUri.resolve(GroupV2Resource.GROUPS_PATH_V2 + "/"); + when(uriInfo.getBaseUri()).thenReturn(baseUri); + subjectThreadState.bind(); + ThreadContext.bind(subject); + } + + @After + public void unbindSubject() { + ThreadContext.unbindSubject(); + } + + @Test + public void shouldMapLinks_forUpdate() { + Group group = createDefaultGroup(); + when(subject.isPermitted("user:modify:abc")).thenReturn(true); + + GroupDto groupDto = mapper.groupToGroupDto(group, uriInfo); + + assertEquals("expected self link", expectedBaseUri.resolve("abc").toString(), groupDto.getLinks().getLinkBy("self").get().getHref()); + assertEquals("expected update link", expectedBaseUri.resolve("abc").toString(), groupDto.getLinks().getLinkBy("update").get().getHref()); + } + + private Group createDefaultGroup() { + Group group = new Group(); + group.setName("abc"); + group.setCreationDate(1L); + return group; + } +}