This commit is contained in:
Mohamed Karray
2018-09-13 14:45:32 +02:00
57 changed files with 1551 additions and 343 deletions

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

@@ -6,11 +6,12 @@ import sonia.scm.repository.Repository;
import javax.inject.Inject;
import java.util.Optional;
import java.util.function.Supplier;
public class ChangesetCollectionToDtoMapper extends BasicCollectionToDtoMapper<Changeset, ChangesetDto, ChangesetToChangesetDtoMapper> {
private final ChangesetToChangesetDtoMapper changesetToChangesetDtoMapper;
private final ResourceLinks resourceLinks;
protected final ResourceLinks resourceLinks;
@Inject
public ChangesetCollectionToDtoMapper(ChangesetToChangesetDtoMapper changesetToChangesetDtoMapper, ResourceLinks resourceLinks) {
@@ -20,10 +21,14 @@ public class ChangesetCollectionToDtoMapper extends BasicCollectionToDtoMapper<C
}
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));
return this.map(pageNumber, pageSize, pageResult, repository, () -> createSelfLink(repository));
}
private String createSelfLink(Repository repository) {
public CollectionDto map(int pageNumber, int pageSize, PageResult<Changeset> pageResult, Repository repository, Supplier<String> selfLinkSupplier) {
return super.map(pageNumber, pageSize, pageResult, selfLinkSupplier.get(), Optional.empty(), changeset -> changesetToChangesetDtoMapper.map(changeset, repository));
}
protected String createSelfLink(Repository repository) {
return resourceLinks.changeset().all(repository.getNamespace(), repository.getName());
}
}

View File

@@ -1,27 +1,71 @@
package sonia.scm.api.v2.resources;
import javax.ws.rs.DefaultValue;
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.QueryParam;
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;
@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();
@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("{id}")
public Response get(String id) {
throw new UnsupportedOperationException();
@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

@@ -0,0 +1,24 @@
package sonia.scm.api.v2.resources;
import sonia.scm.PageResult;
import sonia.scm.repository.Changeset;
import sonia.scm.repository.Repository;
import javax.inject.Inject;
public class FileHistoryCollectionToDtoMapper extends ChangesetCollectionToDtoMapper {
@Inject
public FileHistoryCollectionToDtoMapper(ChangesetToChangesetDtoMapper changesetToChangesetDtoMapper, ResourceLinks resourceLinks) {
super(changesetToChangesetDtoMapper, resourceLinks);
}
public CollectionDto map(int pageNumber, int pageSize, PageResult<Changeset> pageResult, Repository repository, String revision, String path) {
return super.map(pageNumber, pageSize, pageResult, repository, () -> createSelfLink(repository, revision, path));
}
protected String createSelfLink(Repository repository, String revision, String path) {
return super.resourceLinks.fileHistory().self(repository.getNamespace(), repository.getName(), revision, path);
}
}

View File

@@ -0,0 +1,92 @@
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.InternalRepositoryException;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryNotFoundException;
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 FileHistoryRootResource {
private final RepositoryServiceFactory serviceFactory;
private final FileHistoryCollectionToDtoMapper fileHistoryCollectionToDtoMapper;
@Inject
public FileHistoryRootResource(RepositoryServiceFactory serviceFactory, FileHistoryCollectionToDtoMapper fileHistoryCollectionToDtoMapper) {
this.serviceFactory = serviceFactory;
this.fileHistoryCollectionToDtoMapper = fileHistoryCollectionToDtoMapper;
}
/**
* Get all changesets related to the given file starting with the given revision
*
* @param namespace the repository namespace
* @param name the repository name
* @param revision the revision
* @param path the path of the file
* @param page pagination
* @param pageSize pagination
* @return all changesets related to the given file starting with the given revision
* @throws IOException on io error
* @throws RevisionNotFoundException on missing revision
* @throws RepositoryNotFoundException on missing repository
*/
@GET
@Path("{revision}/{path: .*}")
@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,
@PathParam("revision") String revision,
@PathParam("path") String path,
@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))) {
log.info("Get changesets of the file {} and revision {}", path, revision);
Repository repository = repositoryService.getRepository();
ChangesetPagingResult changesets = repositoryService.getLogCommand()
.setPagingStart(page)
.setPagingLimit(pageSize)
.setPath(path)
.setStartChangeset(revision)
.getChangesets();
if (changesets != null && changesets.getChangesets() != null) {
PageResult<Changeset> pageResult = new PageResult<>(changesets.getChangesets(), changesets.getTotal());
return Response.ok(fileHistoryCollectionToDtoMapper.map(page, pageSize, pageResult, repository, revision, path)).build();
} else {
String message = String.format("for the revision %s and the file %s there is no changesets", revision, path);
log.error(message);
throw new InternalRepositoryException(message);
}
}
}
}

View File

@@ -11,6 +11,8 @@ import sonia.scm.repository.SubRepository;
import javax.inject.Inject;
import static de.otto.edison.hal.Link.link;
@Mapper
public abstract class FileObjectToFileObjectDtoMapper extends BaseMapper<FileObject, FileObjectDto> {
@@ -29,6 +31,7 @@ public abstract class FileObjectToFileObjectDtoMapper extends BaseMapper<FileObj
links.self(resourceLinks.source().sourceWithPath(namespaceAndName.getNamespace(), namespaceAndName.getName(), revision, path));
} else {
links.self(resourceLinks.source().content(namespaceAndName.getNamespace(), namespaceAndName.getName(), revision, path));
links.single(link("history", resourceLinks.fileHistory().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), revision, path)));
}
dto.add(links.build());

View File

@@ -41,6 +41,7 @@ public class RepositoryResource {
private final Provider<PermissionRootResource> permissionRootResource;
private final Provider<DiffRootResource> diffRootResource;
private final Provider<ModificationsRootResource> modificationsRootResource;
private final Provider<FileHistoryRootResource> fileHistoryRootResource;
@Inject
public RepositoryResource(
@@ -51,7 +52,10 @@ public class RepositoryResource {
Provider<ChangesetRootResource> changesetRootResource,
Provider<SourceRootResource> sourceRootResource, Provider<ContentResource> contentResource,
Provider<PermissionRootResource> permissionRootResource,
Provider<DiffRootResource> diffRootResource, Provider<ModificationsRootResource> modificationsRootResource) {
Provider<DiffRootResource> diffRootResource,
Provider<ModificationsRootResource> modificationsRootResource,
Provider<FileHistoryRootResource> fileHistoryRootResource
) {
this.dtoToRepositoryMapper = dtoToRepositoryMapper;
this.manager = manager;
this.repositoryToDtoMapper = repositoryToDtoMapper;
@@ -64,6 +68,7 @@ public class RepositoryResource {
this.permissionRootResource = permissionRootResource;
this.diffRootResource = diffRootResource;
this.modificationsRootResource = modificationsRootResource;
this.fileHistoryRootResource = fileHistoryRootResource;
}
/**
@@ -167,6 +172,11 @@ public class RepositoryResource {
return changesetRootResource.get();
}
@Path("history/")
public FileHistoryRootResource history() {
return fileHistoryRootResource.get();
}
@Path("sources/")
public SourceRootResource sources() {
return sourceRootResource.get();

View File

@@ -15,6 +15,10 @@ class ResourceLinks {
this.uriInfoStore = uriInfoStore;
}
// we have to add the file path using URI, so that path separators (aka '/') will not be encoded as '%2F'
private static String addPath(String sourceWithPath, String path) {
return URI.create(sourceWithPath).resolve(path).toASCIIString();
}
GroupLinks group() {
return new GroupLinks(uriInfoStore.get());
@@ -322,6 +326,23 @@ class ResourceLinks {
}
}
public FileHistoryLinks fileHistory() {
return new FileHistoryLinks(uriInfoStore.get());
}
static class FileHistoryLinks {
private final LinkBuilder fileHistoryLinkBuilder;
FileHistoryLinks(UriInfo uriInfo) {
fileHistoryLinkBuilder = new LinkBuilder(uriInfo, RepositoryRootResource.class, RepositoryResource.class, FileHistoryRootResource.class);
}
String self(String namespace, String name, String changesetId, String path) {
return addPath(fileHistoryLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("history").parameters().method("getAll").parameters(changesetId, "").href(), path);
}
}
public SourceLinks source() {
return new SourceLinks(uriInfoStore.get());
}
@@ -353,10 +374,7 @@ class ResourceLinks {
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() {
return new PermissionLinks(uriInfoStore.get());