Merge with 2.0.0-m3

This commit is contained in:
René Pfeuffer
2019-04-01 14:54:42 +02:00
65 changed files with 1583 additions and 469 deletions

View File

@@ -0,0 +1,32 @@
package sonia.scm.api;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import sonia.scm.ExceptionWithContext;
import sonia.scm.api.v2.resources.ErrorDto;
import sonia.scm.web.VndMediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
@Provider
public class ContextualFallbackExceptionMapper implements ExceptionMapper<ExceptionWithContext> {
private static final Logger logger = LoggerFactory.getLogger(ContextualFallbackExceptionMapper.class);
@Override
public Response toResponse(ExceptionWithContext exception) {
logger.warn("mapping unexpected {} to status code 500", exception.getClass().getName(), exception);
ErrorDto errorDto = new ErrorDto();
errorDto.setMessage(exception.getMessage());
errorDto.setContext(exception.getContext());
errorDto.setErrorCode(exception.getCode());
errorDto.setTransactionId(MDC.get("transaction_id"));
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(errorDto)
.type(VndMediaType.ERROR_TYPE)
.build();
}
}

View File

@@ -3,9 +3,12 @@ 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.Link;
import de.otto.edison.hal.Links;
import sonia.scm.repository.Branch;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryPermissions;
import java.util.Collection;
import java.util.List;
@@ -25,22 +28,36 @@ public class BranchCollectionToDtoMapper {
this.branchToDtoMapper = branchToDtoMapper;
}
public HalRepresentation map(String namespace, String name, Collection<Branch> branches) {
return new HalRepresentation(createLinks(namespace, name), embedDtos(getBranchDtoList(namespace, name, branches)));
public HalRepresentation map(Repository repository, Collection<Branch> branches) {
return new HalRepresentation(
createLinks(repository),
embedDtos(getBranchDtoList(repository.getNamespace(), repository.getName(), 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) {
private Links createLinks(Repository repository) {
String namespace = repository.getNamespace();
String name = repository.getName();
String baseUrl = resourceLinks.branchCollection().self(namespace, name);
Links.Builder linksBuilder = linkingTo()
.with(Links.linkingTo().self(baseUrl).build());
Links.Builder linksBuilder = linkingTo().with(createSelfLink(baseUrl));
if (RepositoryPermissions.push(repository).isPermitted()) {
linksBuilder.single(createCreateLink(namespace, name));
}
return linksBuilder.build();
}
private Links createSelfLink(String baseUrl) {
return Links.linkingTo().self(baseUrl).build();
}
private Link createCreateLink(String namespace, String name) {
return Link.link("create", resourceLinks.branch().create(namespace, name));
}
private Embedded embedDtos(List<BranchDto> dtos) {
return embeddedBuilder()
.with("branches", dtos)

View File

@@ -6,12 +6,22 @@ import de.otto.edison.hal.Links;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.NotEmpty;
import javax.validation.constraints.Pattern;
@Getter @Setter @NoArgsConstructor
public class BranchDto extends HalRepresentation {
private static final String VALID_CHARACTERS_AT_START_AND_END = "\\w-,;\\]{}@&+=$#`|<>";
private static final String VALID_CHARACTERS = VALID_CHARACTERS_AT_START_AND_END + "/.";
static final String VALID_BRANCH_NAMES = "[" + VALID_CHARACTERS_AT_START_AND_END + "]([" + VALID_CHARACTERS + "]*[" + VALID_CHARACTERS_AT_START_AND_END + "])?";
@NotEmpty @Length(min = 1, max=100) @Pattern(regexp = VALID_BRANCH_NAMES)
private String name;
private String revision;
private boolean defaultBranch;
BranchDto(Links links, Embedded embedded) {
super(links, embedded);

View File

@@ -0,0 +1,19 @@
package sonia.scm.api.v2.resources;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.NotEmpty;
import javax.validation.constraints.Pattern;
import static sonia.scm.api.v2.resources.BranchDto.VALID_BRANCH_NAMES;
@Getter
@Setter
public class BranchRequestDto {
@NotEmpty @Length(min = 1, max=100) @Pattern(regexp = VALID_BRANCH_NAMES)
private String name;
private String parent;
}

View File

@@ -1,9 +1,11 @@
package sonia.scm.api.v2.resources;
import com.google.common.base.Strings;
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.NotFoundException;
import sonia.scm.PageResult;
import sonia.scm.repository.Branch;
import sonia.scm.repository.Branches;
@@ -12,21 +14,27 @@ import sonia.scm.repository.ChangesetPagingResult;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryPermissions;
import sonia.scm.repository.api.BranchCommandBuilder;
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.validation.Valid;
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.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.net.URI;
import static sonia.scm.AlreadyExistsException.alreadyExists;
import static sonia.scm.ContextEntry.ContextBuilder.entity;
import static sonia.scm.NotFoundException.notFound;
@@ -38,12 +46,15 @@ public class BranchRootResource {
private final BranchChangesetCollectionToDtoMapper branchChangesetCollectionToDtoMapper;
private final ResourceLinks resourceLinks;
@Inject
public BranchRootResource(RepositoryServiceFactory serviceFactory, BranchToBranchDtoMapper branchToDtoMapper, BranchCollectionToDtoMapper branchCollectionToDtoMapper, BranchChangesetCollectionToDtoMapper changesetCollectionToDtoMapper) {
public BranchRootResource(RepositoryServiceFactory serviceFactory, BranchToBranchDtoMapper branchToDtoMapper, BranchCollectionToDtoMapper branchCollectionToDtoMapper, BranchChangesetCollectionToDtoMapper changesetCollectionToDtoMapper, ResourceLinks resourceLinks) {
this.serviceFactory = serviceFactory;
this.branchToDtoMapper = branchToDtoMapper;
this.branchCollectionToDtoMapper = branchCollectionToDtoMapper;
this.branchChangesetCollectionToDtoMapper = changesetCollectionToDtoMapper;
this.resourceLinks = resourceLinks;
}
/**
@@ -100,12 +111,7 @@ public class BranchRootResource {
@DefaultValue("0") @QueryParam("page") int page,
@DefaultValue("10") @QueryParam("pageSize") int pageSize) throws IOException {
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
boolean branchExists = repositoryService.getBranchesCommand()
.getBranches()
.getBranches()
.stream()
.anyMatch(branch -> branchName.equals(branch.getName()));
if (!branchExists){
if (!branchExists(branchName, repositoryService)){
throw notFound(entity(Branch.class, branchName).in(Repository.class, namespace + "/" + name));
}
Repository repository = repositoryService.getRepository();
@@ -125,6 +131,58 @@ public class BranchRootResource {
}
}
/**
* Creates a new branch.
*
* @param namespace the namespace of the repository
* @param name the name of the repository
* @param branchRequest the request giving the name of the new branch and an optional parent branch
* @return A response with the link to the new branch (if created successfully).
*/
@POST
@Path("")
@Consumes(VndMediaType.BRANCH_REQUEST)
@StatusCodes({
@ResponseCode(code = 201, condition = "create success"),
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
@ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"push\" privilege"),
@ResponseCode(code = 409, condition = "conflict, a user with this name already exists"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(TypeHint.NO_CONTENT.class)
@ResponseHeaders(@ResponseHeader(name = "Location", description = "uri to the created branch"))
public Response create(@PathParam("namespace") String namespace,
@PathParam("name") String name,
@Valid BranchRequestDto branchRequest) throws IOException {
NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, name);
String branchName = branchRequest.getName();
String parentName = branchRequest.getParent();
try (RepositoryService repositoryService = serviceFactory.create(namespaceAndName)) {
if (branchExists(branchName, repositoryService)) {
throw alreadyExists(entity(Branch.class, branchName).in(Repository.class, namespace + "/" + name));
}
Repository repository = repositoryService.getRepository();
RepositoryPermissions.push(repository).check();
BranchCommandBuilder branchCommand = repositoryService.getBranchCommand();
if (!Strings.isNullOrEmpty(parentName)) {
if (!branchExists(parentName, repositoryService)) {
throw notFound(entity(Branch.class, parentName).in(Repository.class, namespace + "/" + name));
}
branchCommand.from(parentName);
}
Branch newBranch = branchCommand.branch(branchName);
return Response.created(URI.create(resourceLinks.branch().self(namespaceAndName, newBranch.getName()))).build();
}
}
private boolean branchExists(String branchName, RepositoryService repositoryService) throws IOException {
return repositoryService.getBranchesCommand()
.getBranches()
.getBranches()
.stream()
.anyMatch(branch -> branchName.equals(branch.getName()));
}
/**
* Returns the branches for a repository.
*
@@ -141,14 +199,14 @@ public class BranchRootResource {
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 400, condition = "branches not supported for given repository"),
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
@ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"group\" privilege"),
@ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"read repository\" privilege"),
@ResponseCode(code = 404, condition = "not found, no repository found for the given namespace and name"),
@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))) {
Branches branches = repositoryService.getBranchesCommand().getBranches();
return Response.ok(branchCollectionToDtoMapper.map(namespace, name, branches.getBranches())).build();
return Response.ok(branchCollectionToDtoMapper.map(repositoryService.getRepository(), branches.getBranches())).build();
} catch (CommandNotSupportedException ex) {
return Response.status(Response.Status.BAD_REQUEST).build();
}

View File

@@ -60,7 +60,7 @@ public abstract class DefaultChangesetToChangesetDtoMapper extends HalAppenderMa
}
if (repositoryService.isSupported(Command.BRANCHES)) {
embeddedBuilder.with("branches", branchCollectionToDtoMapper.getBranchDtoList(namespace, name,
getListOfObjects(source.getBranches(), branchName -> new Branch(branchName, source.getId()))));
getListOfObjects(source.getBranches(), branchName -> Branch.normalBranch(branchName, source.getId()))));
}
}
embeddedBuilder.with("parents", getListOfObjects(source.getParents(), parent -> changesetToParentDtoMapper.map(new Changeset(parent, 0L, null), repository)));

View File

@@ -386,6 +386,9 @@ class ResourceLinks {
return branchLinkBuilder.method("getRepositoryResource").parameters(namespaceAndName.getNamespace(), namespaceAndName.getName()).method("branches").parameters().method("history").parameters(branch).href();
}
public String create(String namespace, String name) {
return branchLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("branches").parameters().method("create").parameters().href();
}
}
public IncomingLinks incoming() {