Support reading object by other identifiers than id

Therefore split adapter class for single entity and collection handling.
This commit is contained in:
René Pfeuffer
2018-07-03 13:11:18 +02:00
parent 0768b638ed
commit 1bcc35d48b
10 changed files with 254 additions and 50 deletions

View File

@@ -38,13 +38,11 @@ package sonia.scm.repository;
import sonia.scm.Type; import sonia.scm.Type;
import sonia.scm.TypeManager; import sonia.scm.TypeManager;
//~--- JDK imports ------------------------------------------------------------ import javax.servlet.http.HttpServletRequest;
import java.io.IOException; import java.io.IOException;
import java.util.Collection; import java.util.Collection;
import javax.servlet.http.HttpServletRequest; //~--- JDK imports ------------------------------------------------------------
/** /**
* The central class for managing {@link Repository} objects. * The central class for managing {@link Repository} objects.
@@ -149,4 +147,12 @@ public interface RepositoryManager
*/ */
@Override @Override
public RepositoryHandler getHandler(String type); public RepositoryHandler getHandler(String type);
default Repository getByNamespace(String namespace, String name) {
return getAll()
.stream()
.filter(r -> r.getName().equals(name) && r.getNamespace().equals(namespace))
.findFirst()
.orElse(null);
}
} }

View File

@@ -26,43 +26,14 @@ import static javax.ws.rs.core.Response.Status.BAD_REQUEST;
* @param <EXCEPTION> The exception type for the model object, eg. {@link sonia.scm.user.UserException}. * @param <EXCEPTION> The exception type for the model object, eg. {@link sonia.scm.user.UserException}.
*/ */
@SuppressWarnings("squid:S00119") // "MODEL_OBJECT" is much more meaningful than "M", right? @SuppressWarnings("squid:S00119") // "MODEL_OBJECT" is much more meaningful than "M", right?
class ResourceManagerAdapter<MODEL_OBJECT extends ModelObject, class CollectionResourceManagerAdapter<MODEL_OBJECT extends ModelObject,
DTO extends HalRepresentation, DTO extends HalRepresentation,
EXCEPTION extends Exception> extends AbstractManagerResource<MODEL_OBJECT, EXCEPTION> { EXCEPTION extends Exception> extends AbstractManagerResource<MODEL_OBJECT, EXCEPTION> {
ResourceManagerAdapter(Manager<MODEL_OBJECT, EXCEPTION> manager) { CollectionResourceManagerAdapter(Manager<MODEL_OBJECT, EXCEPTION> manager) {
super(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<MODEL_OBJECT, DTO> 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<MODEL_OBJECT, MODEL_OBJECT> applyChanges) {
MODEL_OBJECT existingModelObject = manager.get(id);
if (existingModelObject == null) {
return Response.status(Response.Status.NOT_FOUND).build();
}
MODEL_OBJECT changedModelObject = applyChanges.apply(existingModelObject);
if (!id.equals(changedModelObject.getId())) {
return Response.status(BAD_REQUEST).entity("illegal change of id").build();
}
return update(id, changedModelObject);
}
/** /**
* Reads all model objects in a paged way, maps them using the given function and returns a corresponding http response. * Reads all model objects in a paged way, maps them using the given function and returns a corresponding http response.
* This handles all corner cases, eg. missing privileges. * This handles all corner cases, eg. missing privileges.

View File

@@ -1,13 +1,23 @@
package sonia.scm.api.v2.resources; package sonia.scm.api.v2.resources;
import com.webcohesion.enunciate.metadata.rs.*; import com.webcohesion.enunciate.metadata.rs.ResponseCode;
import com.webcohesion.enunciate.metadata.rs.ResponseHeader;
import com.webcohesion.enunciate.metadata.rs.ResponseHeaders;
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import com.webcohesion.enunciate.metadata.rs.TypeHint;
import sonia.scm.group.Group; import sonia.scm.group.Group;
import sonia.scm.group.GroupException; import sonia.scm.group.GroupException;
import sonia.scm.group.GroupManager; import sonia.scm.group.GroupManager;
import sonia.scm.web.VndMediaType; import sonia.scm.web.VndMediaType;
import javax.inject.Inject; import javax.inject.Inject;
import javax.ws.rs.*; import javax.ws.rs.Consumes;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.io.IOException; import java.io.IOException;
@@ -19,14 +29,14 @@ public class GroupCollectionResource {
private final GroupCollectionToDtoMapper groupCollectionToDtoMapper; private final GroupCollectionToDtoMapper groupCollectionToDtoMapper;
private final ResourceLinks resourceLinks; private final ResourceLinks resourceLinks;
private final ResourceManagerAdapter<Group, GroupDto, GroupException> adapter; private final IdResourceManagerAdapter<Group, GroupDto, GroupException> adapter;
@Inject @Inject
public GroupCollectionResource(GroupManager manager, GroupDtoToGroupMapper dtoToGroupMapper, GroupCollectionToDtoMapper groupCollectionToDtoMapper, ResourceLinks resourceLinks) { public GroupCollectionResource(GroupManager manager, GroupDtoToGroupMapper dtoToGroupMapper, GroupCollectionToDtoMapper groupCollectionToDtoMapper, ResourceLinks resourceLinks) {
this.dtoToGroupMapper = dtoToGroupMapper; this.dtoToGroupMapper = dtoToGroupMapper;
this.groupCollectionToDtoMapper = groupCollectionToDtoMapper; this.groupCollectionToDtoMapper = groupCollectionToDtoMapper;
this.resourceLinks = resourceLinks; this.resourceLinks = resourceLinks;
this.adapter = new ResourceManagerAdapter<>(manager); this.adapter = new IdResourceManagerAdapter<>(manager);
} }
/** /**

View File

@@ -22,14 +22,14 @@ public class GroupResource {
private final GroupToGroupDtoMapper groupToGroupDtoMapper; private final GroupToGroupDtoMapper groupToGroupDtoMapper;
private final GroupDtoToGroupMapper dtoToGroupMapper; private final GroupDtoToGroupMapper dtoToGroupMapper;
private final ResourceManagerAdapter<Group, GroupDto, GroupException> adapter; private final IdResourceManagerAdapter<Group, GroupDto, GroupException> adapter;
@Inject @Inject
public GroupResource(GroupManager manager, GroupToGroupDtoMapper groupToGroupDtoMapper, public GroupResource(GroupManager manager, GroupToGroupDtoMapper groupToGroupDtoMapper,
GroupDtoToGroupMapper groupDtoToGroupMapper) { GroupDtoToGroupMapper groupDtoToGroupMapper) {
this.groupToGroupDtoMapper = groupToGroupDtoMapper; this.groupToGroupDtoMapper = groupToGroupDtoMapper;
this.dtoToGroupMapper = groupDtoToGroupMapper; this.dtoToGroupMapper = groupDtoToGroupMapper;
this.adapter = new ResourceManagerAdapter<>(manager); this.adapter = new IdResourceManagerAdapter<>(manager);
} }
/** /**

View File

@@ -0,0 +1,51 @@
package sonia.scm.api.v2.resources;
import de.otto.edison.hal.HalRepresentation;
import sonia.scm.Manager;
import sonia.scm.ModelObject;
import sonia.scm.PageResult;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.util.function.Function;
import java.util.function.Supplier;
/**
* Facade for {@link SingleResourceManagerAdapter} and {@link CollectionResourceManagerAdapter}.
*/
@SuppressWarnings("squid:S00119") // "MODEL_OBJECT" is much more meaningful than "M", right?
class IdResourceManagerAdapter<MODEL_OBJECT extends ModelObject,
DTO extends HalRepresentation,
EXCEPTION extends Exception> {
private final Manager<MODEL_OBJECT, EXCEPTION> manager;
private final SingleResourceManagerAdapter<MODEL_OBJECT, DTO, EXCEPTION> singleAdapter;
private final CollectionResourceManagerAdapter<MODEL_OBJECT, DTO, EXCEPTION> collectionAdapter;
IdResourceManagerAdapter(Manager<MODEL_OBJECT, EXCEPTION> manager) {
this.manager = manager;
singleAdapter = new SingleResourceManagerAdapter<>(manager);
collectionAdapter = new CollectionResourceManagerAdapter<>(manager);
}
Response get(String id, Function<MODEL_OBJECT, DTO> mapToDto) {
return singleAdapter.get(() -> manager.get(id), mapToDto);
}
public Response update(String id, Function<MODEL_OBJECT, MODEL_OBJECT> applyChanges) {
return singleAdapter.update(() -> manager.get(id), applyChanges);
}
public Response getAll(int page, int pageSize, String sortBy, boolean desc, Function<PageResult<MODEL_OBJECT>, CollectionDto> mapToDto) {
return collectionAdapter.getAll(page, pageSize, sortBy, desc, mapToDto);
}
public Response create(DTO dto, Supplier<MODEL_OBJECT> modelObjectSupplier, Function<MODEL_OBJECT, String> uriCreator) throws IOException, EXCEPTION {
return collectionAdapter.create(dto, modelObjectSupplier, uriCreator);
}
public Response delete(String id) {
return singleAdapter.delete(id);
}
}

View File

@@ -19,12 +19,14 @@ public class RepositoryResource {
private final RepositoryToRepositoryDtoMapper repositoryToDtoMapper; private final RepositoryToRepositoryDtoMapper repositoryToDtoMapper;
private final ResourceManagerAdapter<Repository, RepositoryDto, RepositoryException> adapter; private final RepositoryManager manager;
private final SingleResourceManagerAdapter<Repository, RepositoryDto, RepositoryException> adapter;
@Inject @Inject
public RepositoryResource(RepositoryToRepositoryDtoMapper repositoryToDtoMapper, RepositoryManager manager) { public RepositoryResource(RepositoryToRepositoryDtoMapper repositoryToDtoMapper, RepositoryManager manager) {
this.manager = manager;
this.repositoryToDtoMapper = repositoryToDtoMapper; this.repositoryToDtoMapper = repositoryToDtoMapper;
this.adapter = new ResourceManagerAdapter<>(manager); this.adapter = new SingleResourceManagerAdapter<>(manager);
} }
@GET @GET
@@ -39,6 +41,6 @@ public class RepositoryResource {
@ResponseCode(code = 500, condition = "internal server error") @ResponseCode(code = 500, condition = "internal server error")
}) })
public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name) { public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name) {
return adapter.get("31QwjAKOK2", repositoryToDtoMapper::map); return adapter.get(() -> manager.getByNamespace(namespace, name), repositoryToDtoMapper::map);
} }
} }

View File

@@ -0,0 +1,77 @@
package sonia.scm.api.v2.resources;
import de.otto.edison.hal.HalRepresentation;
import sonia.scm.Manager;
import sonia.scm.ModelObject;
import sonia.scm.api.rest.resources.AbstractManagerResource;
import javax.ws.rs.core.GenericEntity;
import javax.ws.rs.core.Response;
import java.util.Collection;
import java.util.function.Function;
import java.util.function.Supplier;
import static javax.ws.rs.core.Response.Status.BAD_REQUEST;
/**
* Adapter from resource http endpoints to managers.
*
* Provides common CRUD operations and DTO to Model Object mapping to keep Resources more DRY.
*
* @param <MODEL_OBJECT> The type of the model object, eg. {@link sonia.scm.user.User}.
* @param <DTO> The corresponding transport object, eg. {@link UserDto}.
* @param <EXCEPTION> The exception type for the model object, eg. {@link sonia.scm.user.UserException}.
*/
@SuppressWarnings("squid:S00119") // "MODEL_OBJECT" is much more meaningful than "M", right?
class SingleResourceManagerAdapter<MODEL_OBJECT extends ModelObject,
DTO extends HalRepresentation,
EXCEPTION extends Exception> extends AbstractManagerResource<MODEL_OBJECT, EXCEPTION> {
SingleResourceManagerAdapter(Manager<MODEL_OBJECT, EXCEPTION> manager) {
super(manager);
}
/**
* Reads the model object for the given id, transforms it to a dto and returns a corresponding http response.
* This handles all corner cases, eg. no matching object for the id or missing privileges.
*/
Response get(Supplier<MODEL_OBJECT> reader, Function<MODEL_OBJECT, DTO> mapToDto) {
MODEL_OBJECT modelObject = reader.get();
if (modelObject == null) {
return Response.status(Response.Status.NOT_FOUND).build();
}
DTO dto = mapToDto.apply(modelObject);
return Response.ok(dto).build();
}
/**
* Update the model object for the given id according to the given function and returns a corresponding http response.
* This handles all corner cases, eg. no matching object for the id or missing privileges.
*/
public Response update(Supplier<MODEL_OBJECT> reader, Function<MODEL_OBJECT, MODEL_OBJECT> applyChanges) {
MODEL_OBJECT existingModelObject = reader.get();
if (existingModelObject == null) {
return Response.status(Response.Status.NOT_FOUND).build();
}
MODEL_OBJECT changedModelObject = applyChanges.apply(existingModelObject);
if (!getId(existingModelObject).equals(getId(changedModelObject))) {
return Response.status(BAD_REQUEST).entity("illegal change of id").build();
}
return update(getId(existingModelObject), changedModelObject);
}
@Override
protected GenericEntity<Collection<MODEL_OBJECT>> createGenericEntity(Collection<MODEL_OBJECT> modelObjects) {
throw new UnsupportedOperationException();
}
@Override
protected String getId(MODEL_OBJECT item) {
return item.getId();
}
@Override
protected String getPathPart() {
throw new UnsupportedOperationException();
}
}

View File

@@ -1,13 +1,23 @@
package sonia.scm.api.v2.resources; package sonia.scm.api.v2.resources;
import com.webcohesion.enunciate.metadata.rs.*; import com.webcohesion.enunciate.metadata.rs.ResponseCode;
import com.webcohesion.enunciate.metadata.rs.ResponseHeader;
import com.webcohesion.enunciate.metadata.rs.ResponseHeaders;
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import com.webcohesion.enunciate.metadata.rs.TypeHint;
import sonia.scm.user.User; import sonia.scm.user.User;
import sonia.scm.user.UserException; import sonia.scm.user.UserException;
import sonia.scm.user.UserManager; import sonia.scm.user.UserManager;
import sonia.scm.web.VndMediaType; import sonia.scm.web.VndMediaType;
import javax.inject.Inject; import javax.inject.Inject;
import javax.ws.rs.*; import javax.ws.rs.Consumes;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.io.IOException; import java.io.IOException;
@@ -18,14 +28,14 @@ public class UserCollectionResource {
private final UserCollectionToDtoMapper userCollectionToDtoMapper; private final UserCollectionToDtoMapper userCollectionToDtoMapper;
private final ResourceLinks resourceLinks; private final ResourceLinks resourceLinks;
private final ResourceManagerAdapter<User, UserDto, UserException> adapter; private final IdResourceManagerAdapter<User, UserDto, UserException> adapter;
@Inject @Inject
public UserCollectionResource(UserManager manager, UserDtoToUserMapper dtoToUserMapper, public UserCollectionResource(UserManager manager, UserDtoToUserMapper dtoToUserMapper,
UserCollectionToDtoMapper userCollectionToDtoMapper, ResourceLinks resourceLinks) { UserCollectionToDtoMapper userCollectionToDtoMapper, ResourceLinks resourceLinks) {
this.dtoToUserMapper = dtoToUserMapper; this.dtoToUserMapper = dtoToUserMapper;
this.userCollectionToDtoMapper = userCollectionToDtoMapper; this.userCollectionToDtoMapper = userCollectionToDtoMapper;
this.adapter = new ResourceManagerAdapter<>(manager); this.adapter = new IdResourceManagerAdapter<>(manager);
this.resourceLinks = resourceLinks; this.resourceLinks = resourceLinks;
} }

View File

@@ -23,13 +23,13 @@ public class UserResource {
private final UserDtoToUserMapper dtoToUserMapper; private final UserDtoToUserMapper dtoToUserMapper;
private final UserToUserDtoMapper userToDtoMapper; private final UserToUserDtoMapper userToDtoMapper;
private final ResourceManagerAdapter<User, UserDto, UserException> adapter; private final IdResourceManagerAdapter<User, UserDto, UserException> adapter;
@Inject @Inject
public UserResource(UserDtoToUserMapper dtoToUserMapper, UserToUserDtoMapper userToDtoMapper, UserManager manager) { public UserResource(UserDtoToUserMapper dtoToUserMapper, UserToUserDtoMapper userToDtoMapper, UserManager manager) {
this.dtoToUserMapper = dtoToUserMapper; this.dtoToUserMapper = dtoToUserMapper;
this.userToDtoMapper = userToDtoMapper; this.userToDtoMapper = userToDtoMapper;
this.adapter = new ResourceManagerAdapter<>(manager); this.adapter = new IdResourceManagerAdapter<>(manager);
} }
/** /**

View File

@@ -0,0 +1,77 @@
package sonia.scm.api.v2.resources;
import org.jboss.resteasy.core.Dispatcher;
import org.jboss.resteasy.mock.MockDispatcherFactory;
import org.jboss.resteasy.mock.MockHttpRequest;
import org.jboss.resteasy.mock.MockHttpResponse;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Answers;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryManager;
import java.net.URI;
import java.net.URISyntaxException;
import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
import static javax.servlet.http.HttpServletResponse.SC_OK;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;
public class RepositoryRootResourceTest {
private final Dispatcher dispatcher = MockDispatcherFactory.createDispatcher();
@Mock
private RepositoryManager repositoryManager;
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private ResourceLinks resourceLinks;
@InjectMocks
private RepositoryToRepositoryDtoMapperImpl repositoryToDtoMapper;
@Before
public void prepareEnvironment() {
initMocks(this);
ResourceLinksMock.initMock(resourceLinks, URI.create("/"));
RepositoryResource repositoryResource = new RepositoryResource(repositoryToDtoMapper, repositoryManager);
RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider.of(repositoryResource));
dispatcher.getRegistry().addSingletonResource(repositoryRootResource);
}
@Test
public void shouldFailForNotExistingRepository() throws URISyntaxException {
mockRepository("space", "repo");
MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/other");
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
assertEquals(SC_NOT_FOUND, response.getStatus());
}
@Test
public void shouldFindExistingRepository() throws URISyntaxException {
mockRepository("space", "repo");
MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo");
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
assertEquals(SC_OK, response.getStatus());
assertTrue(response.getContentAsString().contains("\"name\":\"repo\""));
}
private Repository mockRepository(String namespace, String name) {
Repository repository = new Repository();
repository.setNamespace(namespace);
repository.setName(name);
when(repositoryManager.getByNamespace(namespace, name)).thenReturn(repository);
return repository;
}
}