merge with branch 2.0.0-m3

This commit is contained in:
Sebastian Sdorra
2018-09-10 14:53:59 +02:00
71 changed files with 2004 additions and 542 deletions

View File

@@ -11,6 +11,8 @@ import javax.ws.rs.ext.Provider;
public class AlreadyExistsExceptionMapper implements ExceptionMapper<AlreadyExistsException> {
@Override
public Response toResponse(AlreadyExistsException exception) {
return Response.status(Status.CONFLICT).build();
return Response.status(Status.CONFLICT)
.entity(exception.getMessage())
.build();
}
}

View File

@@ -33,6 +33,8 @@ package sonia.scm.api.rest;
//~--- JDK imports ------------------------------------------------------------
import lombok.extern.slf4j.Slf4j;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.ext.ExceptionMapper;
@@ -44,6 +46,7 @@ import javax.ws.rs.ext.Provider;
* @since 1.36
*/
@Provider
@Slf4j
public class IllegalArgumentExceptionMapper
implements ExceptionMapper<IllegalArgumentException>
{
@@ -59,6 +62,7 @@ public class IllegalArgumentExceptionMapper
@Override
public Response toResponse(IllegalArgumentException exception)
{
log.info("caught IllegalArgumentException -- mapping to bad request", exception);
return Response.status(Status.BAD_REQUEST).build();
}
}

View File

@@ -90,6 +90,8 @@ public class StatusExceptionMapper<E extends Throwable>
logger.debug(msg.toString());
}
return Response.status(status).build();
return Response.status(status)
.entity(exception.getMessage())
.build();
}
}

View File

@@ -11,6 +11,8 @@ import sonia.scm.PageResult;
import javax.inject.Inject;
import java.util.EnumSet;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import static com.damnhandy.uri.template.UriTemplate.fromTemplate;
import static de.otto.edison.hal.Embedded.embeddedBuilder;
@@ -19,25 +21,28 @@ import static de.otto.edison.hal.Links.linkingTo;
import static de.otto.edison.hal.paging.NumberedPaging.zeroBasedNumberedPaging;
import static java.util.stream.Collectors.toList;
abstract class BasicCollectionToDtoMapper<E extends ModelObject, D extends HalRepresentation> {
abstract class BasicCollectionToDtoMapper<E extends ModelObject, D extends HalRepresentation, M extends BaseMapper<E, D>> {
private final String collectionName;
private final BaseMapper<E, D> entityToDtoMapper;
private final M entityToDtoMapper;
@Inject
public BasicCollectionToDtoMapper(String collectionName, BaseMapper<E, D> entityToDtoMapper) {
public BasicCollectionToDtoMapper(String collectionName, M entityToDtoMapper) {
this.collectionName = collectionName;
this.entityToDtoMapper = entityToDtoMapper;
}
public CollectionDto map(int pageNumber, int pageSize, PageResult<E> pageResult) {
NumberedPaging paging = zeroBasedNumberedPaging(pageNumber, pageSize, pageResult.getOverallCount());
List<D> dtos = pageResult.getEntities().stream().map(entityToDtoMapper::map).collect(toList());
CollectionDto map(int pageNumber, int pageSize, PageResult<E> pageResult, String selfLink, Optional<String> createLink) {
return map(pageNumber, pageSize, pageResult, selfLink, createLink, entityToDtoMapper::map);
}
CollectionDto map(int pageNumber, int pageSize, PageResult<E> pageResult, String selfLink, Optional<String> createLink, Function<E, ? extends HalRepresentation> mapper) {
NumberedPaging paging = zeroBasedNumberedPaging(pageNumber, pageSize, pageResult.getOverallCount());
List<HalRepresentation> dtos = pageResult.getEntities().stream().map(mapper).collect(toList());
CollectionDto collectionDto = new CollectionDto(
createLinks(paging),
embedDtos(dtos)
);
createLinks(paging, selfLink, createLink),
embedDtos(dtos));
collectionDto.setPage(pageNumber);
collectionDto.setPageTotal(computePageTotal(pageSize, pageResult));
return collectionDto;
@@ -51,26 +56,16 @@ abstract class BasicCollectionToDtoMapper<E extends ModelObject, D extends HalRe
}
}
private Links createLinks(NumberedPaging page) {
String baseUrl = createSelfLink();
private Links createLinks(NumberedPaging page, String selfLink, Optional<String> createLink) {
Links.Builder linksBuilder = linkingTo()
.with(page.links(
fromTemplate(baseUrl + "{?page,pageSize}"),
fromTemplate(selfLink + "{?page,pageSize}"),
EnumSet.allOf(PagingRel.class)));
if (isCreatePermitted()) {
linksBuilder.single(link("create", createCreateLink()));
}
createLink.ifPresent(link -> linksBuilder.single(link("create", link)));
return linksBuilder.build();
}
abstract boolean isCreatePermitted();
abstract String createCreateLink();
abstract String createSelfLink();
private Embedded embedDtos(List<D> dtos) {
private Embedded embedDtos(List<HalRepresentation> dtos) {
return embeddedBuilder()
.with(collectionName, dtos)
.build();

View File

@@ -26,8 +26,11 @@ public class BranchCollectionToDtoMapper {
}
public HalRepresentation map(String namespace, String name, Collection<Branch> branches) {
List<BranchDto> dtos = branches.stream().map(branch -> branchToDtoMapper.map(branch, new NamespaceAndName(namespace, name))).collect(toList());
return new HalRepresentation(createLinks(namespace, name), embedDtos(dtos));
return new HalRepresentation(createLinks(namespace, name), embedDtos(getBranchDtoList(namespace, name, branches)));
}
public List<BranchDto> getBranchDtoList(String namespace, String name, Collection<Branch> branches) {
return branches.stream().map(branch -> branchToDtoMapper.map(branch, new NamespaceAndName(namespace, name))).collect(toList());
}
private Links createLinks(String namespace, String name) {

View File

@@ -3,33 +3,43 @@ 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.PageResult;
import sonia.scm.repository.Branches;
import sonia.scm.repository.Changeset;
import sonia.scm.repository.ChangesetPagingResult;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryNotFoundException;
import sonia.scm.repository.RepositoryPermissions;
import sonia.scm.repository.api.CommandNotSupportedException;
import sonia.scm.repository.api.RepositoryService;
import sonia.scm.repository.api.RepositoryServiceFactory;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
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.Response;
import java.io.IOException;
public class BranchRootResource {
private final RepositoryServiceFactory servicefactory;
private final RepositoryServiceFactory serviceFactory;
private final BranchToBranchDtoMapper branchToDtoMapper;
private final BranchCollectionToDtoMapper branchCollectionToDtoMapper;
private final ChangesetCollectionToDtoMapper changesetCollectionToDtoMapper;
@Inject
public BranchRootResource(RepositoryServiceFactory servicefactory, BranchToBranchDtoMapper branchToDtoMapper, BranchCollectionToDtoMapper branchCollectionToDtoMapper) {
this.servicefactory = servicefactory;
public BranchRootResource(RepositoryServiceFactory serviceFactory, BranchToBranchDtoMapper branchToDtoMapper, BranchCollectionToDtoMapper branchCollectionToDtoMapper, ChangesetCollectionToDtoMapper changesetCollectionToDtoMapper) {
this.serviceFactory = serviceFactory;
this.branchToDtoMapper = branchToDtoMapper;
this.branchCollectionToDtoMapper = branchCollectionToDtoMapper;
this.changesetCollectionToDtoMapper = changesetCollectionToDtoMapper;
}
/**
@@ -37,10 +47,9 @@ public class BranchRootResource {
*
* <strong>Note:</strong> This method requires "repository" privilege.
*
* @param namespace the namespace of the repository
* @param name the name of the repository
* @param namespace the namespace of the repository
* @param name the name of the repository
* @param branchName the name of the branch
*
*/
@GET
@Path("{branch}")
@@ -55,7 +64,7 @@ public class BranchRootResource {
@ResponseCode(code = 500, condition = "internal server error")
})
public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("branch") String branchName) throws IOException {
try (RepositoryService repositoryService = servicefactory.create(new NamespaceAndName(namespace, name))) {
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
Branches branches = repositoryService.getBranchesCommand().getBranches();
return branches.getBranches()
.stream()
@@ -74,8 +83,35 @@ public class BranchRootResource {
@Path("{branch}/changesets/")
@GET
public Response history(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("branch") String branchName) {
throw new UnsupportedOperationException();
@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 changeset"),
@ResponseCode(code = 404, condition = "not found, no changesets available in the repository"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Produces(VndMediaType.CHANGESET_COLLECTION)
@TypeHint(CollectionDto.class)
public Response history(@PathParam("namespace") String namespace,
@PathParam("name") String name,
@PathParam("branch") String branchName,
@DefaultValue("0") @QueryParam("page") int page,
@DefaultValue("10") @QueryParam("pageSize") int pageSize) throws Exception {
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
Repository repository = repositoryService.getRepository();
RepositoryPermissions.read(repository).check();
ChangesetPagingResult changesets = repositoryService.getLogCommand()
.setPagingStart(page)
.setPagingLimit(pageSize)
.setBranch(branchName)
.getChangesets();
if (changesets != null && changesets.getChangesets() != null) {
PageResult<Changeset> pageResult = new PageResult<>(changesets.getChangesets(), changesets.getTotal());
return Response.ok(changesetCollectionToDtoMapper.map(page, pageSize, pageResult, repository)).build();
} else {
return Response.ok().build();
}
}
}
/**
@@ -84,8 +120,7 @@ public class BranchRootResource {
* <strong>Note:</strong> This method requires "repository" privilege.
*
* @param namespace the namespace of the repository
* @param name the name of the repository
*
* @param name the name of the repository
*/
@GET
@Path("")
@@ -100,7 +135,7 @@ public class BranchRootResource {
@ResponseCode(code = 500, condition = "internal server error")
})
public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name) throws IOException {
try (RepositoryService repositoryService = servicefactory.create(new NamespaceAndName(namespace, name))) {
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
Branches branches = repositoryService.getBranchesCommand().getBranches();
return Response.ok(branchCollectionToDtoMapper.map(namespace, name, branches.getBranches())).build();
} catch (CommandNotSupportedException ex) {

View File

@@ -6,18 +6,13 @@ import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.Iterator;
import java.util.List;
@Getter
@Setter
@NoArgsConstructor
public class BrowserResultDto extends HalRepresentation implements Iterable<FileObjectDto> {
public class BrowserResultDto extends HalRepresentation {
private String revision;
private String tag;
private String branch;
// REVIEW files nicht embedded?
private List<FileObjectDto> files;
@Override
@SuppressWarnings("squid:S1185") // We want to have this method available in this package
@@ -25,16 +20,7 @@ public class BrowserResultDto extends HalRepresentation implements Iterable<File
return super.add(links);
}
// REVIEW return null?
@Override
public Iterator<FileObjectDto> iterator() {
Iterator<FileObjectDto> it = null;
if (files != null)
{
it = files.iterator();
}
return it;
public void setFiles(List<FileObjectDto> files) {
this.withEmbedded("files", files);
}
}

View File

@@ -17,11 +17,9 @@ public class BrowserResultToBrowserResultDtoMapper {
@Inject
private ResourceLinks resourceLinks;
public BrowserResultDto map(BrowserResult browserResult, NamespaceAndName namespaceAndName) {
public BrowserResultDto map(BrowserResult browserResult, NamespaceAndName namespaceAndName, String path) {
BrowserResultDto browserResultDto = new BrowserResultDto();
browserResultDto.setTag(browserResult.getTag());
browserResultDto.setBranch(browserResult.getBranch());
browserResultDto.setRevision(browserResult.getRevision());
List<FileObjectDto> fileObjectDtoList = new ArrayList<>();
@@ -30,7 +28,7 @@ public class BrowserResultToBrowserResultDtoMapper {
}
browserResultDto.setFiles(fileObjectDtoList);
this.addLinks(browserResult, browserResultDto, namespaceAndName);
this.addLinks(browserResult, browserResultDto, namespaceAndName, path);
return browserResultDto;
}
@@ -38,13 +36,14 @@ public class BrowserResultToBrowserResultDtoMapper {
return fileObjectToFileObjectDtoMapper.map(fileObject, namespaceAndName, revision);
}
private void addLinks(BrowserResult browserResult, BrowserResultDto dto, NamespaceAndName namespaceAndName) {
private void addLinks(BrowserResult browserResult, BrowserResultDto dto, NamespaceAndName namespaceAndName, String path) {
if (path.equals("/")) {
path = "";
}
if (browserResult.getRevision() == null) {
dto.add(Links.linkingTo().self(resourceLinks.source().selfWithoutRevision(namespaceAndName.getNamespace(), namespaceAndName.getName())).build());
throw new IllegalStateException("missing revision in browser result for repository " + namespaceAndName + " and path " + path);
} else {
dto.add(Links.linkingTo().self(resourceLinks.source().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), browserResult.getRevision())).build());
dto.add(Links.linkingTo().self(resourceLinks.source().sourceWithPath(namespaceAndName.getNamespace(), namespaceAndName.getName(), browserResult.getRevision(), path)).build());
}
}
}

View File

@@ -0,0 +1,13 @@
package sonia.scm.api.v2.resources;
import sonia.scm.api.rest.StatusExceptionMapper;
import sonia.scm.util.CRLFInjectionException;
import javax.ws.rs.core.Response;
public class CRLFInjectionExceptionMapper extends StatusExceptionMapper<CRLFInjectionException> {
public CRLFInjectionExceptionMapper() {
super(CRLFInjectionException.class, Response.Status.BAD_REQUEST);
}
}

View File

@@ -0,0 +1,29 @@
package sonia.scm.api.v2.resources;
import sonia.scm.PageResult;
import sonia.scm.repository.Changeset;
import sonia.scm.repository.Repository;
import javax.inject.Inject;
import java.util.Optional;
public class ChangesetCollectionToDtoMapper extends BasicCollectionToDtoMapper<Changeset, ChangesetDto, ChangesetToChangesetDtoMapper> {
private final ChangesetToChangesetDtoMapper changesetToChangesetDtoMapper;
private final ResourceLinks resourceLinks;
@Inject
public ChangesetCollectionToDtoMapper(ChangesetToChangesetDtoMapper changesetToChangesetDtoMapper, ResourceLinks resourceLinks) {
super("changesets", changesetToChangesetDtoMapper);
this.changesetToChangesetDtoMapper = changesetToChangesetDtoMapper;
this.resourceLinks = resourceLinks;
}
public CollectionDto map(int pageNumber, int pageSize, PageResult<Changeset> pageResult, Repository repository) {
return super.map(pageNumber, pageSize, pageResult, createSelfLink(repository), Optional.empty(), changeset -> changesetToChangesetDtoMapper.map(changeset, repository));
}
private String createSelfLink(Repository repository) {
return resourceLinks.changeset().all(repository.getNamespace(), repository.getName());
}
}

View File

@@ -0,0 +1,49 @@
package sonia.scm.api.v2.resources;
import de.otto.edison.hal.HalRepresentation;
import de.otto.edison.hal.Links;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.time.Instant;
import java.util.List;
@Getter
@Setter
@NoArgsConstructor
public class ChangesetDto extends HalRepresentation {
/**
* The changeset identification string
*/
private String id;
/**
* The author of the changeset
*/
private PersonDto author;
/**
* The date when the changeset was committed
*/
private Instant date;
/**
* The text of the changeset description
*/
private String description;
@Override
@SuppressWarnings("squid:S1185") // We want to have this method available in this package
protected HalRepresentation add(Links links) {
return super.add(links);
}
@SuppressWarnings("squid:S1185") // We want to have this method available in this package
protected HalRepresentation withEmbedded(String rel, List<? extends HalRepresentation> halRepresentations) {
return super.withEmbedded(rel, halRepresentations);
}
}

View File

@@ -1,25 +1,101 @@
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 lombok.extern.slf4j.Slf4j;
import sonia.scm.PageResult;
import sonia.scm.repository.Changeset;
import sonia.scm.repository.ChangesetPagingResult;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryNotFoundException;
import sonia.scm.repository.RepositoryPermissions;
import sonia.scm.repository.RevisionNotFoundException;
import sonia.scm.repository.api.RepositoryService;
import sonia.scm.repository.api.RepositoryServiceFactory;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
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.Response;
import java.io.IOException;
@Slf4j
public class ChangesetRootResource {
private final RepositoryServiceFactory serviceFactory;
private final ChangesetCollectionToDtoMapper changesetCollectionToDtoMapper;
private final ChangesetToChangesetDtoMapper changesetToChangesetDtoMapper;
@Inject
public ChangesetRootResource(RepositoryServiceFactory serviceFactory, ChangesetCollectionToDtoMapper changesetCollectionToDtoMapper, ChangesetToChangesetDtoMapper changesetToChangesetDtoMapper) {
this.serviceFactory = serviceFactory;
this.changesetCollectionToDtoMapper = changesetCollectionToDtoMapper;
this.changesetToChangesetDtoMapper = changesetToChangesetDtoMapper;
}
@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();
@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 changeset"),
@ResponseCode(code = 404, condition = "not found, no changesets available in the repository"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Produces(VndMediaType.CHANGESET_COLLECTION)
@TypeHint(CollectionDto.class)
public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name, @DefaultValue("0") @QueryParam("page") int page,
@DefaultValue("10") @QueryParam("pageSize") int pageSize) throws IOException, RevisionNotFoundException, RepositoryNotFoundException {
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
Repository repository = repositoryService.getRepository();
RepositoryPermissions.read(repository).check();
ChangesetPagingResult changesets = repositoryService.getLogCommand()
.setPagingStart(page)
.setPagingLimit(pageSize)
.getChangesets();
if (changesets != null && changesets.getChangesets() != null) {
PageResult<Changeset> pageResult = new PageResult<>(changesets.getChangesets(), changesets.getTotal());
return Response.ok(changesetCollectionToDtoMapper.map(page, pageSize, pageResult, repository)).build();
} else {
return Response.ok().build();
}
}
}
@GET
@Path("{revision}")
public Response get() {
throw new UnsupportedOperationException();
@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 changeset"),
@ResponseCode(code = 404, condition = "not found, no changeset with the specified id is available in the repository"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Produces(VndMediaType.CHANGESET)
@TypeHint(ChangesetDto.class)
@Path("{id}")
public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("id") String id) throws IOException, RevisionNotFoundException, RepositoryNotFoundException {
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
Repository repository = repositoryService.getRepository();
RepositoryPermissions.read(repository).check();
ChangesetPagingResult changesets = repositoryService.getLogCommand()
.setStartChangeset(id)
.setEndChangeset(id)
.getChangesets();
if (changesets != null && changesets.getChangesets() != null && changesets.getChangesets().size() == 1) {
return Response.ok(changesetToChangesetDtoMapper.map(changesets.getChangesets().get(0), repository)).build();
} else {
return Response.status(Response.Status.NOT_FOUND).build();
}
}
}
}

View File

@@ -0,0 +1,78 @@
package sonia.scm.api.v2.resources;
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 sonia.scm.repository.Branch;
import sonia.scm.repository.Changeset;
import sonia.scm.repository.Repository;
import sonia.scm.repository.Tag;
import sonia.scm.repository.api.Command;
import sonia.scm.repository.api.RepositoryService;
import sonia.scm.repository.api.RepositoryServiceFactory;
import javax.inject.Inject;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
import static de.otto.edison.hal.Link.link;
import static de.otto.edison.hal.Links.linkingTo;
@Mapper
public abstract class ChangesetToChangesetDtoMapper extends BaseMapper<Changeset, ChangesetDto> {
@Inject
private RepositoryServiceFactory serviceFactory;
@Inject
private ResourceLinks resourceLinks;
@Inject
private BranchCollectionToDtoMapper branchCollectionToDtoMapper;
@Inject
private ChangesetToParentDtoMapper changesetToParentDtoMapper;
@Inject
private TagCollectionToDtoMapper tagCollectionToDtoMapper;
@Mapping(target = "attributes", ignore = true) // We do not map HAL attributes
public abstract ChangesetDto map(Changeset changeset, @Context Repository repository);
@AfterMapping
void appendLinks(Changeset source, @MappingTarget ChangesetDto target, @Context Repository repository) {
String namespace = repository.getNamespace();
String name = repository.getName();
try (RepositoryService repositoryService = serviceFactory.create(repository)) {
if (repositoryService.isSupported(Command.TAGS)) {
target.withEmbedded("tags", tagCollectionToDtoMapper.getTagDtoList(namespace, name,
getListOfObjects(source.getTags(), tagName -> new Tag(tagName, source.getId()))));
}
if (repositoryService.isSupported(Command.BRANCHES)) {
target.withEmbedded("branches", branchCollectionToDtoMapper.getBranchDtoList(namespace, name,
getListOfObjects(source.getBranches(), branchName -> new Branch(branchName, source.getId()))));
}
}
target.withEmbedded("parents", getListOfObjects(source.getParents(), parent -> changesetToParentDtoMapper.map(new Changeset(parent, 0L, null), repository)));
Links.Builder linksBuilder = linkingTo()
.self(resourceLinks.changeset().self(repository.getNamespace(), repository.getName(), target.getId()))
.single(link("diff", resourceLinks.diff().self(namespace, name, target.getId())));
target.add(linksBuilder.build());
}
private <T> List<T> getListOfObjects(List<String> list, Function<String, T> mapFunction) {
return list
.stream()
.map(mapFunction)
.collect(Collectors.toList());
}
}

View File

@@ -0,0 +1,38 @@
package sonia.scm.api.v2.resources;
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 sonia.scm.repository.Changeset;
import sonia.scm.repository.Repository;
import javax.inject.Inject;
import static de.otto.edison.hal.Link.link;
import static de.otto.edison.hal.Links.linkingTo;
@Mapper
public abstract class ChangesetToParentDtoMapper extends BaseMapper<Changeset, ParentChangesetDto> {
@Inject
private ResourceLinks resourceLinks;
@Mapping(target = "attributes", ignore = true) // We do not map HAL attributes
public abstract ParentChangesetDto map(Changeset changeset, @Context Repository repository);
@AfterMapping
void appendLinks(@MappingTarget ParentChangesetDto target, @Context Repository repository) {
String namespace = repository.getNamespace();
String name = repository.getName();
Links.Builder linksBuilder = linkingTo()
.self(resourceLinks.changeset().self(repository.getNamespace(), repository.getName(), target.getId()))
.single(link("diff", resourceLinks.diff().self(namespace, name, target.getId())));
target.add(linksBuilder.build());
}
}

View File

@@ -1,32 +0,0 @@
package sonia.scm.api.v2.resources;
import de.otto.edison.hal.HalRepresentation;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import static de.otto.edison.hal.Embedded.embeddedBuilder;
import static de.otto.edison.hal.Links.linkingTo;
abstract class CollectionToDtoMapper<E, D extends HalRepresentation> {
private final String collectionName;
private final BaseMapper<E, D> mapper;
protected CollectionToDtoMapper(String collectionName, BaseMapper<E, D> mapper) {
this.collectionName = collectionName;
this.mapper = mapper;
}
public HalRepresentation map(Collection<E> collection) {
List<D> dtos = collection.stream().map(mapper::map).collect(Collectors.toList());
return new HalRepresentation(
linkingTo().self(createSelfLink()).build(),
embeddedBuilder().with(collectionName, dtos).build()
);
}
protected abstract String createSelfLink();
}

View File

@@ -0,0 +1,71 @@
package sonia.scm.api.v2.resources;
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import sonia.scm.NotFoundException;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.RevisionNotFoundException;
import sonia.scm.repository.api.RepositoryService;
import sonia.scm.repository.api.RepositoryServiceFactory;
import sonia.scm.util.HttpUtil;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.StreamingOutput;
public class DiffRootResource {
public static final String HEADER_CONTENT_DISPOSITION = "Content-Disposition";
private final RepositoryServiceFactory serviceFactory;
@Inject
public DiffRootResource(RepositoryServiceFactory serviceFactory) {
this.serviceFactory = serviceFactory;
}
/**
* Get the repository diff of a revision
*
* @param namespace repository namespace
* @param name repository name
* @param revision the revision
* @return the dif of the revision
* @throws NotFoundException if the repository is not found
*/
@GET
@Path("{revision}")
@Produces(VndMediaType.DIFF)
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 400, condition = "Bad Request"),
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
@ResponseCode(code = 403, condition = "not authorized, the current user has no privileges to read the diff"),
@ResponseCode(code = 404, condition = "not found, no revision with the specified param for the repository available or repository not found"),
@ResponseCode(code = 500, condition = "internal server error")
})
public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision) throws NotFoundException {
HttpUtil.checkForCRLFInjection(revision);
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
StreamingOutput responseEntry = output -> {
try {
repositoryService.getDiffCommand()
.setRevision(revision)
.retriveContent(output);
} catch (RevisionNotFoundException e) {
throw new WebApplicationException(Response.Status.NOT_FOUND);
}
};
return Response.ok(responseEntry)
.header(HEADER_CONTENT_DISPOSITION, HttpUtil.createContentDispositionAttachmentHeader(String.format("%s-%s.diff", name, revision)))
.build();
}
}
}

View File

@@ -10,7 +10,6 @@ import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.SubRepository;
import javax.inject.Inject;
import java.net.URI;
@Mapper
public abstract class FileObjectToFileObjectDtoMapper extends BaseMapper<FileObject, FileObjectDto> {
@@ -27,19 +26,14 @@ public abstract class FileObjectToFileObjectDtoMapper extends BaseMapper<FileObj
String path = removeFirstSlash(fileObject.getPath());
Links.Builder links = Links.linkingTo();
if (dto.isDirectory()) {
links.self(addPath(resourceLinks.source().sourceWithPath(namespaceAndName.getNamespace(), namespaceAndName.getName(), revision, ""), path));
links.self(resourceLinks.source().sourceWithPath(namespaceAndName.getNamespace(), namespaceAndName.getName(), revision, path));
} else {
links.self(addPath(resourceLinks.source().content(namespaceAndName.getNamespace(), namespaceAndName.getName(), revision, ""), path));
links.self(resourceLinks.source().content(namespaceAndName.getNamespace(), namespaceAndName.getName(), revision, path));
}
dto.add(links.build());
}
// we have to add the file path using URI, so that path separators (aka '/') will not be encoded as '%2F'
private String addPath(String sourceWithPath, String path) {
return URI.create(sourceWithPath).resolve(path).toASCIIString();
}
private String removeFirstSlash(String source) {
return source.startsWith("/") ? source.substring(1) : source;
}

View File

@@ -1,11 +1,16 @@
package sonia.scm.api.v2.resources;
import sonia.scm.PageResult;
import sonia.scm.group.Group;
import sonia.scm.group.GroupPermissions;
import javax.inject.Inject;
import java.util.Optional;
public class GroupCollectionToDtoMapper extends BasicCollectionToDtoMapper<Group, GroupDto> {
import static java.util.Optional.empty;
import static java.util.Optional.of;
public class GroupCollectionToDtoMapper extends BasicCollectionToDtoMapper<Group, GroupDto, GroupToGroupDtoMapper> {
private final ResourceLinks resourceLinks;
@@ -15,18 +20,15 @@ public class GroupCollectionToDtoMapper extends BasicCollectionToDtoMapper<Group
this.resourceLinks = resourceLinks;
}
@Override
String createCreateLink() {
return resourceLinks.groupCollection().create();
public CollectionDto map(int pageNumber, int pageSize, PageResult<Group> pageResult) {
return map(pageNumber, pageSize, pageResult, this.createSelfLink(), this.createCreateLink());
}
@Override
String createSelfLink() {
private Optional<String> createCreateLink() {
return GroupPermissions.create().isPermitted() ? of(resourceLinks.groupCollection().create()): empty();
}
private String createSelfLink() {
return resourceLinks.groupCollection().self();
}
@Override
boolean isCreatePermitted() {
return GroupPermissions.create().isPermitted();
}
}

View File

@@ -28,6 +28,11 @@ public class MapperModule extends AbstractModule {
bind(PermissionDtoToPermissionMapper.class).to(Mappers.getMapper(PermissionDtoToPermissionMapper.class).getClass());
bind(PermissionToPermissionDtoMapper.class).to(Mappers.getMapper(PermissionToPermissionDtoMapper.class).getClass());
bind(ChangesetToChangesetDtoMapper.class).to(Mappers.getMapper(ChangesetToChangesetDtoMapper.class).getClass());
bind(ChangesetToParentDtoMapper.class).to(Mappers.getMapper(ChangesetToParentDtoMapper.class).getClass());
bind(TagToTagDtoMapper.class).to(Mappers.getMapper(TagToTagDtoMapper.class).getClass());
bind(FileObjectToFileObjectDtoMapper.class).to(Mappers.getMapper(FileObjectToFileObjectDtoMapper.class).getClass());
// no mapstruct required

View File

@@ -0,0 +1,27 @@
package sonia.scm.api.v2.resources;
import de.otto.edison.hal.HalRepresentation;
import de.otto.edison.hal.Links;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString
@NoArgsConstructor
public class ParentChangesetDto extends HalRepresentation {
/**
* the id of the parent changeset
*/
private String id;
@Override
@SuppressWarnings("squid:S1185") // We want to have this method available in this package
protected HalRepresentation add(Links links) {
return super.add(links);
}
}

View File

@@ -10,6 +10,8 @@ import lombok.ToString;
@Getter @Setter @ToString
public class PermissionDto extends HalRepresentation {
public static final String GROUP_PREFIX = "@";
@JsonInclude(JsonInclude.Include.NON_NULL)
private String name;
@@ -21,11 +23,19 @@ public class PermissionDto extends HalRepresentation {
*
**/
@JsonInclude(JsonInclude.Include.NON_NULL)
private String type ;
private String type;
private boolean groupPermission = false;
public PermissionDto() {
}
public PermissionDto(String permissionName, boolean groupPermission) {
name = permissionName;
this.groupPermission = groupPermission;
}
@Override
@SuppressWarnings("squid:S1185") // We want to have this method available in this package

View File

@@ -5,7 +5,6 @@ import com.webcohesion.enunciate.metadata.rs.ResponseHeader;
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import com.webcohesion.enunciate.metadata.rs.TypeHint;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import sonia.scm.AlreadyExistsException;
import sonia.scm.NotFoundException;
import sonia.scm.repository.NamespaceAndName;
@@ -28,10 +27,14 @@ import javax.ws.rs.Produces;
import javax.ws.rs.core.Response;
import java.net.URI;
import java.util.Optional;
import java.util.function.Predicate;
import static sonia.scm.api.v2.resources.PermissionDto.GROUP_PREFIX;
@Slf4j
public class PermissionRootResource {
private PermissionDtoToPermissionMapper dtoToModelMapper;
private PermissionToPermissionDtoMapper modelToDtoMapper;
private PermissionCollectionToDtoMapper permissionCollectionToDtoMapper;
@@ -101,7 +104,7 @@ public class PermissionRootResource {
return Response.ok(
repository.getPermissions()
.stream()
.filter(permission -> permissionName.equals(permission.getName()))
.filter(filterPermission(permissionName))
.map(permission -> modelToDtoMapper.map(permission, repository))
.findFirst()
.orElseThrow(NotFoundException::new)
@@ -135,6 +138,7 @@ public class PermissionRootResource {
/**
* Update a permission to the user or group managed by the repository
* ignore the user input for groupPermission and take it from the path parameter (if the group prefix (@) exists it is a group permission)
*
* @param permission permission to modify
* @param permissionName permission to modify
@@ -152,15 +156,23 @@ public class PermissionRootResource {
public Response update(@PathParam("namespace") String namespace,
@PathParam("name") String name,
@PathParam("permission-name") String permissionName,
PermissionDto permission) throws NotFoundException {
PermissionDto permission) throws NotFoundException, AlreadyExistsException {
log.info("try to update the permission with name: {}. the modified permission is: {}", permissionName, permission);
Repository repository = load(namespace, name);
RepositoryPermissions.permissionWrite(repository).check();
String extractedPermissionName = getPermissionName(permissionName);
if (!isPermissionExist(new PermissionDto(extractedPermissionName, isGroupPermission(permissionName)), repository)) {
throw new NotFoundException("the permission " + extractedPermissionName + " does not exist");
}
permission.setGroupPermission(isGroupPermission(permissionName));
if (!extractedPermissionName.equals(permission.getName())) {
checkPermissionAlreadyExists(permission, repository, "target permission " + permission.getName() + " already exists");
}
Permission existingPermission = repository.getPermissions()
.stream()
.filter(perm -> StringUtils.isNotBlank(perm.getName()) && perm.getName().equals(permissionName))
.filter(filterPermission(permissionName))
.findFirst()
.orElseThrow(() -> new NotFoundException());
.orElseThrow(NotFoundException::new);
dtoToModelMapper.modify(existingPermission, permission);
manager.modify(repository);
log.info("the permission with name: {} is updated.", permissionName);
@@ -186,11 +198,11 @@ public class PermissionRootResource {
@PathParam("name") String name,
@PathParam("permission-name") String permissionName) throws NotFoundException {
log.info("try to delete the permission with name: {}.", permissionName);
Repository repository = load(namespace, name);
Repository repository = load(namespace, name);
RepositoryPermissions.modify(repository).check();
repository.getPermissions()
.stream()
.filter(perm -> StringUtils.isNotBlank(perm.getName()) && perm.getName().equals(permissionName))
.filter(filterPermission(permissionName))
.findFirst()
.ifPresent(p -> repository.getPermissions().remove(p))
;
@@ -199,6 +211,22 @@ public class PermissionRootResource {
return Response.noContent().build();
}
Predicate<Permission> filterPermission(String permissionName) {
return permission -> getPermissionName(permissionName).equals(permission.getName())
&&
permission.isGroupPermission() == isGroupPermission(permissionName);
}
private String getPermissionName(String permissionName) {
return Optional.of(permissionName)
.filter(p -> !isGroupPermission(permissionName))
.orElse(permissionName.substring(1));
}
private boolean isGroupPermission(String permissionName) {
return permissionName.startsWith(GROUP_PREFIX);
}
/**
* check if the actual user is permitted to manage the repository permissions
@@ -219,16 +247,24 @@ public class PermissionRootResource {
*
* @param permission the searched permission
* @param repository the repository to be inspected
* @param errorMessage error message
* @throws AlreadyExistsException if the permission already exists in the repository
*/
private void checkPermissionAlreadyExists(PermissionDto permission, Repository repository) throws AlreadyExistsException {
boolean isPermissionAlreadyExist = repository.getPermissions()
.stream()
.anyMatch(p -> p.getName().equals(permission.getName()));
if (isPermissionAlreadyExist) {
throw new AlreadyExistsException();
private void checkPermissionAlreadyExists(PermissionDto permission, Repository repository, String errorMessage) throws AlreadyExistsException {
if (isPermissionExist(permission, repository)) {
throw new AlreadyExistsException(errorMessage);
}
}
private boolean isPermissionExist(PermissionDto permission, Repository repository) {
return repository.getPermissions()
.stream()
.anyMatch(p -> p.getName().equals(permission.getName()) && p.isGroupPermission() == permission.isGroupPermission());
}
private void checkPermissionAlreadyExists(PermissionDto permission, Repository repository) throws AlreadyExistsException {
checkPermissionAlreadyExists(permission, repository, "the permission " + permission.getName() + " already exist.");
}
}

View File

@@ -12,9 +12,11 @@ import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryPermissions;
import javax.inject.Inject;
import java.util.Optional;
import static de.otto.edison.hal.Link.link;
import static de.otto.edison.hal.Links.linkingTo;
import static sonia.scm.api.v2.resources.PermissionDto.GROUP_PREFIX;
@Mapper
public abstract class PermissionToPermissionDtoMapper {
@@ -39,11 +41,14 @@ public abstract class PermissionToPermissionDtoMapper {
*/
@AfterMapping
void appendLinks(@MappingTarget PermissionDto target, @Context Repository repository) {
String permissionName = Optional.of(target.getName())
.filter(p -> !target.isGroupPermission())
.orElse(GROUP_PREFIX + target.getName());
Links.Builder linksBuilder = linkingTo()
.self(resourceLinks.permission().self(repository.getNamespace(), repository.getName(), target.getName()));
.self(resourceLinks.permission().self(repository.getNamespace(), repository.getName(), permissionName));
if (RepositoryPermissions.permissionWrite(repository).isPermitted()) {
linksBuilder.single(link("update", resourceLinks.permission().update(repository.getNamespace(), repository.getName(), target.getName())));
linksBuilder.single(link("delete", resourceLinks.permission().delete(repository.getNamespace(), repository.getName(), target.getName())));
linksBuilder.single(link("update", resourceLinks.permission().update(repository.getNamespace(), repository.getName(), permissionName)));
linksBuilder.single(link("delete", resourceLinks.permission().delete(repository.getNamespace(), repository.getName(), permissionName)));
}
target.add(linksBuilder.build());
}

View File

@@ -0,0 +1,22 @@
package sonia.scm.api.v2.resources;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
public class PersonDto {
/**
* mail address of the person
*/
private String mail;
/**
* name of the person
*/
private String name;
}

View File

@@ -1,13 +1,18 @@
package sonia.scm.api.v2.resources;
import sonia.scm.PageResult;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryPermissions;
import javax.inject.Inject;
import java.util.Optional;
import static java.util.Optional.empty;
import static java.util.Optional.of;
// 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<Repository, RepositoryDto> {
public class RepositoryCollectionToDtoMapper extends BasicCollectionToDtoMapper<Repository, RepositoryDto, RepositoryToRepositoryDtoMapper> {
private final ResourceLinks resourceLinks;
@@ -17,18 +22,15 @@ public class RepositoryCollectionToDtoMapper extends BasicCollectionToDtoMapper<
this.resourceLinks = resourceLinks;
}
@Override
String createCreateLink() {
return resourceLinks.repositoryCollection().create();
public CollectionDto map(int pageNumber, int pageSize, PageResult<Repository> pageResult) {
return map(pageNumber, pageSize, pageResult, this.createSelfLink(), this.createCreateLink());
}
Optional<String> createCreateLink() {
return RepositoryPermissions.create().isPermitted() ? of(resourceLinks.repositoryCollection().create()): empty();
}
@Override
String createSelfLink() {
return resourceLinks.repositoryCollection().self();
}
@Override
boolean isCreatePermitted() {
return RepositoryPermissions.create().isPermitted();
}
}

View File

@@ -39,6 +39,7 @@ public class RepositoryResource {
private final Provider<SourceRootResource> sourceRootResource;
private final Provider<ContentResource> contentResource;
private final Provider<PermissionRootResource> permissionRootResource;
private final Provider<DiffRootResource> diffRootResource;
@Inject
public RepositoryResource(
@@ -47,7 +48,9 @@ public class RepositoryResource {
Provider<TagRootResource> tagRootResource,
Provider<BranchRootResource> branchRootResource,
Provider<ChangesetRootResource> changesetRootResource,
Provider<SourceRootResource> sourceRootResource, Provider<ContentResource> contentResource, Provider<PermissionRootResource> permissionRootResource) {
Provider<SourceRootResource> sourceRootResource, Provider<ContentResource> contentResource,
Provider<PermissionRootResource> permissionRootResource,
Provider<DiffRootResource> diffRootResource) {
this.dtoToRepositoryMapper = dtoToRepositoryMapper;
this.manager = manager;
this.repositoryToDtoMapper = repositoryToDtoMapper;
@@ -58,6 +61,7 @@ public class RepositoryResource {
this.sourceRootResource = sourceRootResource;
this.contentResource = contentResource;
this.permissionRootResource = permissionRootResource;
this.diffRootResource = diffRootResource;
}
/**
@@ -146,6 +150,11 @@ public class RepositoryResource {
return tagRootResource.get();
}
@Path("diff/")
public DiffRootResource diff() {
return diffRootResource.get();
}
@Path("branches/")
public BranchRootResource branches(@PathParam("namespace") String namespace, @PathParam("name") String name) {
return branchRootResource.get();

View File

@@ -40,13 +40,13 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper<Reposit
}
try (RepositoryService repositoryService = serviceFactory.create(repository)) {
if (repositoryService.isSupported(Command.TAGS)) {
linksBuilder.single(link("tags", resourceLinks.tagCollection().self(target.getNamespace(), target.getName())));
linksBuilder.single(link("tags", resourceLinks.tag().all(target.getNamespace(), target.getName())));
}
if (repositoryService.isSupported(Command.BRANCHES)) {
linksBuilder.single(link("branches", resourceLinks.branchCollection().self(target.getNamespace(), target.getName())));
}
}
linksBuilder.single(link("changesets", resourceLinks.changeset().self(target.getNamespace(), target.getName())));
linksBuilder.single(link("changesets", resourceLinks.changeset().all(target.getNamespace(), target.getName())));
linksBuilder.single(link("sources", resourceLinks.source().selfWithoutRevision(target.getNamespace(), target.getName())));
target.add(linksBuilder.build());
}

View File

@@ -207,7 +207,7 @@ class ResourceLinks {
}
public TagCollectionLinks tagCollection() {
public TagCollectionLinks tag() {
return new TagCollectionLinks(uriInfoStore.get());
}
@@ -215,11 +215,35 @@ class ResourceLinks {
private final LinkBuilder tagLinkBuilder;
TagCollectionLinks(UriInfo uriInfo) {
tagLinkBuilder = new LinkBuilder(uriInfo, RepositoryRootResource.class, RepositoryResource.class, TagRootResource.class, TagCollectionResource.class);
tagLinkBuilder = new LinkBuilder(uriInfo, RepositoryRootResource.class, RepositoryResource.class, TagRootResource.class);
}
String self(String namespace, String name) {
return tagLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("tags").parameters().method("getTagCollectionResource").parameters().method("getAll").parameters().href();
String self(String namespace, String name, String tagName) {
return tagLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("tags").parameters().method("get").parameters(tagName).href();
}
String all(String namespace, String name) {
return tagLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("tags").parameters().method("getAll").parameters().href();
}
}
public DiffLinks diff() {
return new DiffLinks(uriInfoStore.get());
}
static class DiffLinks {
private final LinkBuilder diffLinkBuilder;
DiffLinks(UriInfo uriInfo) {
diffLinkBuilder = new LinkBuilder(uriInfo, RepositoryRootResource.class, RepositoryResource.class, DiffRootResource.class);
}
String self(String namespace, String name, String id) {
return diffLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("diff").parameters().method("get").parameters(id).href();
}
String all(String namespace, String name) {
return diffLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("diff").parameters().method("getAll").parameters().href();
}
}
@@ -270,7 +294,11 @@ class ResourceLinks {
changesetLinkBuilder = new LinkBuilder(uriInfo, RepositoryRootResource.class, RepositoryResource.class, ChangesetRootResource.class);
}
String self(String namespace, String name) {
String self(String namespace, String name, String changesetId) {
return changesetLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("changesets").parameters().method("get").parameters(changesetId).href();
}
String all(String namespace, String name) {
return changesetLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("changesets").parameters().method("getAll").parameters().href();
}
@@ -303,15 +331,16 @@ class ResourceLinks {
}
public String sourceWithPath(String namespace, String name, String revision, String path) {
if (revision == null) {
return sourceLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("sources").parameters().method("get").parameters(null, path).href();
} else {
return sourceLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("sources").parameters().method("get").parameters(revision, path).href();
}
return addPath(sourceLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("sources").parameters().method("get").parameters(revision, "").href(), path);
}
public String content(String namespace, String name, String revision, String path) {
return sourceLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("content").parameters().method("get").parameters(revision, path).href();
return addPath(sourceLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("content").parameters().method("get").parameters(revision, "").href(), path);
}
// we have to add the file path using URI, so that path separators (aka '/') will not be encoded as '%2F'
private String addPath(String sourceWithPath, String path) {
return URI.create(sourceWithPath).resolve(path).toASCIIString();
}
}
public PermissionLinks permission() {

View File

@@ -1,5 +1,6 @@
package sonia.scm.api.v2.resources;
import sonia.scm.NotFoundException;
import sonia.scm.repository.BrowserResult;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.RepositoryNotFoundException;
@@ -32,25 +33,25 @@ public class SourceRootResource {
@GET
@Produces(VndMediaType.SOURCE)
@Path("")
public Response getAllWithoutRevision(@PathParam("namespace") String namespace, @PathParam("name") String name) {
public Response getAllWithoutRevision(@PathParam("namespace") String namespace, @PathParam("name") String name) throws RevisionNotFoundException, RepositoryNotFoundException, IOException {
return getSource(namespace, name, "/", null);
}
@GET
@Produces(VndMediaType.SOURCE)
@Path("{revision}")
public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision) {
public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision) throws RevisionNotFoundException, RepositoryNotFoundException, IOException {
return getSource(namespace, name, "/", revision);
}
@GET
@Produces(VndMediaType.SOURCE)
@Path("{revision}/{path: .*}")
public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision, @PathParam("path") String path) {
public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision, @PathParam("path") String path) throws NotFoundException, IOException {
return getSource(namespace, name, path, revision);
}
private Response getSource(String namespace, String repoName, String path, String revision) {
private Response getSource(String namespace, String repoName, String path, String revision) throws IOException, RevisionNotFoundException, RepositoryNotFoundException {
NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, repoName);
try (RepositoryService repositoryService = serviceFactory.create(namespaceAndName)) {
BrowseCommandBuilder browseCommand = repositoryService.getBrowseCommand();
@@ -61,15 +62,10 @@ public class SourceRootResource {
BrowserResult browserResult = browseCommand.getBrowserResult();
if (browserResult != null) {
return Response.ok(browserResultToBrowserResultDtoMapper.map(browserResult, namespaceAndName)).build();
return Response.ok(browserResultToBrowserResultDtoMapper.map(browserResult, namespaceAndName, path)).build();
} else {
return Response.status(Response.Status.NOT_FOUND).build();
}
} catch (RepositoryNotFoundException | RevisionNotFoundException e) {
return Response.status(Response.Status.NOT_FOUND).build();
} catch (IOException e) {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
}
}
}

View File

@@ -1,18 +0,0 @@
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();
}
}

View File

@@ -0,0 +1,49 @@
package sonia.scm.api.v2.resources;
import com.google.inject.Inject;
import de.otto.edison.hal.Embedded;
import de.otto.edison.hal.HalRepresentation;
import de.otto.edison.hal.Links;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Tag;
import java.util.Collection;
import java.util.List;
import static de.otto.edison.hal.Embedded.embeddedBuilder;
import static de.otto.edison.hal.Links.linkingTo;
import static java.util.stream.Collectors.toList;
public class TagCollectionToDtoMapper {
private final ResourceLinks resourceLinks;
private final TagToTagDtoMapper tagToTagDtoMapper;
@Inject
public TagCollectionToDtoMapper(ResourceLinks resourceLinks, TagToTagDtoMapper tagToTagDtoMapper) {
this.resourceLinks = resourceLinks;
this.tagToTagDtoMapper = tagToTagDtoMapper;
}
public HalRepresentation map(String namespace, String name, Collection<Tag> tags) {
return new HalRepresentation(createLinks(namespace, name), embedDtos(getTagDtoList(namespace, name, tags)));
}
public List<TagDto> getTagDtoList(String namespace, String name, Collection<Tag> tags) {
return tags.stream().map(tag -> tagToTagDtoMapper.map(tag, new NamespaceAndName(namespace, name))).collect(toList());
}
private Links createLinks(String namespace, String name) {
return
linkingTo()
.self(resourceLinks.tag().all(namespace, name))
.build();
}
private Embedded embedDtos(List<TagDto> dtos) {
return embeddedBuilder()
.with("tags", dtos)
.build();
}
}

View File

@@ -0,0 +1,24 @@
package sonia.scm.api.v2.resources;
import de.otto.edison.hal.HalRepresentation;
import de.otto.edison.hal.Links;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
public class TagDto extends HalRepresentation {
private String name;
private String revision;
@Override
@SuppressWarnings("squid:S1185") // We want to have this method available in this package
protected HalRepresentation add(Links links) {
return super.add(links);
}
}

View File

@@ -0,0 +1,7 @@
package sonia.scm.api.v2.resources;
import sonia.scm.NotFoundException;
public class TagNotFoundException extends NotFoundException {
}

View File

@@ -1,20 +1,100 @@
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.NamespaceAndName;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryNotFoundException;
import sonia.scm.repository.RepositoryPermissions;
import sonia.scm.repository.Tag;
import sonia.scm.repository.Tags;
import sonia.scm.repository.api.RepositoryService;
import sonia.scm.repository.api.RepositoryServiceFactory;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Response;
import java.io.IOException;
public class TagRootResource {
private final Provider<TagCollectionResource> tagCollectionResource;
private final RepositoryServiceFactory serviceFactory;
private final TagCollectionToDtoMapper tagCollectionToDtoMapper;
private final TagToTagDtoMapper tagToTagDtoMapper;
@Inject
public TagRootResource(Provider<TagCollectionResource> tagCollectionResource) {
this.tagCollectionResource = tagCollectionResource;
public TagRootResource(RepositoryServiceFactory serviceFactory,
TagCollectionToDtoMapper tagCollectionToDtoMapper,
TagToTagDtoMapper tagToTagDtoMapper) {
this.serviceFactory = serviceFactory;
this.tagCollectionToDtoMapper = tagCollectionToDtoMapper;
this.tagToTagDtoMapper = tagToTagDtoMapper;
}
@GET
@Path("")
public TagCollectionResource getTagCollectionResource() {
return tagCollectionResource.get();
@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 tags"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Produces(VndMediaType.TAG_COLLECTION)
@TypeHint(CollectionDto.class)
public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name) throws IOException, RepositoryNotFoundException {
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
Tags tags = getTags(repositoryService);
if (tags != null && tags.getTags() != null) {
return Response.ok(tagCollectionToDtoMapper.map(namespace, name, tags.getTags())).build();
} else {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Error on getting tag from repository.")
.build();
}
}
}
@GET
@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 tags"),
@ResponseCode(code = 404, condition = "not found, no tag available in the repository"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Produces(VndMediaType.TAG)
@TypeHint(TagDto.class)
@Path("{tagName}")
public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("tagName") String tagName) throws IOException, RepositoryNotFoundException, TagNotFoundException {
NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, name);
try (RepositoryService repositoryService = serviceFactory.create(namespaceAndName)) {
Tags tags = getTags(repositoryService);
if (tags != null && tags.getTags() != null) {
Tag tag = tags.getTags().stream()
.filter(t -> tagName.equals(t.getName()))
.findFirst()
.orElseThrow(TagNotFoundException::new);
return Response.ok(tagToTagDtoMapper.map(tag, namespaceAndName)).build();
} else {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Error on getting tag from repository.")
.build();
}
}
}
private Tags getTags(RepositoryService repositoryService) throws IOException {
Repository repository = repositoryService.getRepository();
RepositoryPermissions.read(repository).check();
return repositoryService.getTagsCommand().getTags();
}
}

View File

@@ -0,0 +1,34 @@
package sonia.scm.api.v2.resources;
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 sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Tag;
import javax.inject.Inject;
import static de.otto.edison.hal.Link.link;
import static de.otto.edison.hal.Links.linkingTo;
@Mapper
public abstract class TagToTagDtoMapper {
@Inject
private ResourceLinks resourceLinks;
@Mapping(target = "attributes", ignore = true) // We do not map HAL attributes
public abstract TagDto map(Tag tag, @Context NamespaceAndName namespaceAndName);
@AfterMapping
void appendLinks(@MappingTarget TagDto target, @Context NamespaceAndName namespaceAndName) {
Links.Builder linksBuilder = linkingTo()
.self(resourceLinks.tag().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getName()))
.single(link("sources", resourceLinks.source().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getRevision())))
.single(link("changesets", resourceLinks.changeset().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getRevision())));
target.add(linksBuilder.build());
}
}

View File

@@ -1,13 +1,18 @@
package sonia.scm.api.v2.resources;
import sonia.scm.PageResult;
import sonia.scm.user.User;
import sonia.scm.user.UserPermissions;
import javax.inject.Inject;
import java.util.Optional;
import static java.util.Optional.empty;
import static java.util.Optional.of;
// Mapstruct does not support parameterized (i.e. non-default) constructors. Thus, we need to use field injection.
@SuppressWarnings("squid:S3306")
public class UserCollectionToDtoMapper extends BasicCollectionToDtoMapper<User, UserDto> {
public class UserCollectionToDtoMapper extends BasicCollectionToDtoMapper<User, UserDto, UserToUserDtoMapper> {
private final ResourceLinks resourceLinks;
@@ -17,18 +22,15 @@ public class UserCollectionToDtoMapper extends BasicCollectionToDtoMapper<User,
this.resourceLinks = resourceLinks;
}
@Override
String createCreateLink() {
return resourceLinks.userCollection().create();
public CollectionDto map(int pageNumber, int pageSize, PageResult<User> pageResult) {
return map(pageNumber, pageSize, pageResult, this.createSelfLink(), this.createCreateLink());
}
Optional<String> createCreateLink() {
return UserPermissions.create().isPermitted() ? of(resourceLinks.userCollection().create()): empty();
}
@Override
String createSelfLink() {
return resourceLinks.userCollection().self();
}
@Override
boolean isCreatePermitted() {
return UserPermissions.create().isPermitted();
}
}

View File

@@ -39,34 +39,27 @@ import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.common.hash.Hashing;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.plugin.ExplodedSmp.PathTransformer;
//~--- JDK imports ------------------------------------------------------------
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URL;
import java.nio.file.DirectoryStream;
import java.nio.file.DirectoryStream.Filter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Set;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
//~--- JDK imports ------------------------------------------------------------
/**
*
@@ -370,9 +363,10 @@ public final class PluginProcessor
if (Files.exists(libDir))
{
for (Path f : Files.newDirectoryStream(libDir, GLOB_JAR))
{
urls.add(f.toUri().toURL());
try (DirectoryStream<Path> pathDirectoryStream = Files.newDirectoryStream(libDir, GLOB_JAR)) {
for (Path f : pathDirectoryStream) {
urls.add(f.toUri().toURL());
}
}
}
@@ -656,7 +650,7 @@ public final class PluginProcessor
break;
}
}
logger.debug("move installed archive to {}", installed);
Files.move(archive, installed);