Merge with 2.0.0-m3

sonia.scm.it.RepositoryHookITCase breaks for hg.
This commit is contained in:
René Pfeuffer
2018-08-31 10:40:12 +02:00
1810 changed files with 33506 additions and 207291 deletions

View File

@@ -1,6 +1,6 @@
package sonia.scm.api.rest;
import sonia.scm.user.UserAlreadyExistsException;
import sonia.scm.AlreadyExistsException;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
@@ -8,9 +8,9 @@ import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
@Provider
public class UserAlreadyExistsExceptionMapper implements ExceptionMapper<UserAlreadyExistsException> {
public class AlreadyExistsExceptionMapper implements ExceptionMapper<AlreadyExistsException> {
@Override
public Response toResponse(UserAlreadyExistsException exception) {
public Response toResponse(AlreadyExistsException exception) {
return Response.status(Status.CONFLICT).build();
}
}

View File

@@ -0,0 +1,15 @@
package sonia.scm.api.rest;
import sonia.scm.ConcurrentModificationException;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
@Provider
public class ConcurrentModificationExceptionMapper implements ExceptionMapper<ConcurrentModificationException> {
@Override
public Response toResponse(ConcurrentModificationException exception) {
return Response.status(Response.Status.CONFLICT).build();
}
}

View File

@@ -1,16 +0,0 @@
package sonia.scm.api.rest;
import sonia.scm.group.GroupAlreadyExistsException;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
@Provider
public class GroupAlreadyExistsExceptionMapper implements ExceptionMapper<GroupAlreadyExistsException> {
@Override
public Response toResponse(GroupAlreadyExistsException exception) {
return Response.status(Status.CONFLICT).build();
}
}

View File

@@ -34,15 +34,15 @@ package sonia.scm.api.rest;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
//~--- JDK imports ------------------------------------------------------------
import java.io.Serializable;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import java.io.Serializable;
//~--- JDK imports ------------------------------------------------------------
/**
*
@@ -127,7 +127,7 @@ public class Permission implements Serializable
public String toString()
{
//J-
return Objects.toStringHelper(this)
return MoreObjects.toStringHelper(this)
.add("id", id)
.add("value", value)
.toString();

View File

@@ -1,16 +0,0 @@
package sonia.scm.api.rest;
import sonia.scm.repository.RepositoryAlreadyExistsException;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
@Provider
public class RepositoryAlreadyExistsExceptionMapper implements ExceptionMapper<RepositoryAlreadyExistsException> {
@Override
public Response toResponse(RepositoryAlreadyExistsException exception) {
return Response.status(Status.CONFLICT).build();
}
}

View File

@@ -34,15 +34,16 @@ package sonia.scm.api.rest;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import com.google.common.base.Throwables;
//~--- JDK imports ------------------------------------------------------------
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
//~--- JDK imports ------------------------------------------------------------
/**
*
* @author Sebastian Sdorra
@@ -145,7 +146,7 @@ public class RestExceptionResult
public String toString()
{
//J-
return Objects.toStringHelper(this)
return MoreObjects.toStringHelper(this)
.add("message", message)
.add("stacktrace", stacktrace)
.toString();

View File

@@ -36,11 +36,11 @@ package sonia.scm.api.rest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
//~--- JDK imports ------------------------------------------------------------
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
//~--- JDK imports ------------------------------------------------------------
/**
*
* @author Sebastian Sdorra

View File

@@ -48,8 +48,13 @@ import sonia.scm.util.AssertUtil;
import sonia.scm.util.HttpUtil;
import sonia.scm.util.Util;
import javax.ws.rs.core.*;
import javax.ws.rs.core.CacheControl;
import javax.ws.rs.core.EntityTag;
import javax.ws.rs.core.GenericEntity;
import javax.ws.rs.core.Request;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriInfo;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
@@ -61,28 +66,19 @@ import java.util.Date;
//~--- JDK imports ------------------------------------------------------------
/**
*
* @author Sebastian Sdorra
*
* @param <T>
* @param <E>
*/
public abstract class AbstractManagerResource<T extends ModelObject,
E extends Exception>
{
public abstract class AbstractManagerResource<T extends ModelObject> {
/** the logger for AbstractManagerResource */
private static final Logger logger =
LoggerFactory.getLogger(AbstractManagerResource.class);
protected final Manager<T, E> manager;
protected final Manager<T> manager;
private final Class<T> type;
protected int cacheMaxAge = 0;
protected boolean disableCache = false;
public AbstractManagerResource(Manager<T, E> manager, Class<T> type) {
public AbstractManagerResource(Manager<T> manager, Class<T> type) {
this.manager = manager;
this.type = type;
}

View File

@@ -1,44 +1,8 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.api.rest.resources;
//~--- non-JDK imports --------------------------------------------------------
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.repository.PathNotFoundException;
import sonia.scm.repository.RepositoryException;
import sonia.scm.repository.RevisionNotFoundException;
import sonia.scm.repository.api.CatCommandBuilder;
import sonia.scm.repository.api.RepositoryService;
@@ -50,97 +14,40 @@ import javax.ws.rs.core.StreamingOutput;
import java.io.IOException;
import java.io.OutputStream;
/**
*
* @author Sebastian Sdorra
*/
public class BrowserStreamingOutput implements StreamingOutput
{
public class BrowserStreamingOutput implements StreamingOutput {
/** the logger for BrowserStreamingOutput */
private static final Logger logger =
LoggerFactory.getLogger(BrowserStreamingOutput.class);
//~--- constructors ---------------------------------------------------------
private final CatCommandBuilder builder;
private final String path;
private final RepositoryService repositoryService;
/**
* Constructs ...
*
*
* @param browser
* @param revision
*
* @param repositoryService
*
* @param builder
* @param path
*/
public BrowserStreamingOutput(RepositoryService repositoryService,
CatCommandBuilder builder, String path)
{
CatCommandBuilder builder, String path) {
this.repositoryService = repositoryService;
this.builder = builder;
this.path = path;
}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param output
*
* @throws IOException
* @throws WebApplicationException
*/
@Override
public void write(OutputStream output)
throws IOException, WebApplicationException
{
try
{
public void write(OutputStream output) throws IOException {
try {
builder.retriveContent(output, path);
}
catch (PathNotFoundException ex)
{
if (logger.isWarnEnabled())
{
} catch (PathNotFoundException ex) {
if (logger.isWarnEnabled()) {
logger.warn("could not find path {}", ex.getPath());
}
throw new WebApplicationException(Response.Status.NOT_FOUND);
}
catch (RevisionNotFoundException ex)
{
if (logger.isWarnEnabled())
{
} catch (RevisionNotFoundException ex) {
if (logger.isWarnEnabled()) {
logger.warn("could not find revision {}", ex.getRevision());
}
throw new WebApplicationException(Response.Status.NOT_FOUND);
}
catch (RepositoryException ex)
{
logger.error("could not write content to page", ex);
throw new WebApplicationException(ex,
Response.Status.INTERNAL_SERVER_ERROR);
}
finally
{
} finally {
IOUtil.close(repositoryService);
}
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private final CatCommandBuilder builder;
/** Field description */
private final String path;
/** Field description */
private final RepositoryService repositoryService;
}

View File

@@ -39,26 +39,20 @@ import com.google.inject.Inject;
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import com.webcohesion.enunciate.metadata.rs.TypeHint;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.credential.PasswordService;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.ConcurrentModificationException;
import sonia.scm.NotFoundException;
import sonia.scm.api.rest.RestActionResult;
import sonia.scm.security.Role;
import sonia.scm.security.ScmSecurityException;
import sonia.scm.user.User;
import sonia.scm.user.UserException;
import sonia.scm.user.UserManager;
import sonia.scm.util.AssertUtil;
//~--- JDK imports ------------------------------------------------------------
import java.io.IOException;
import javax.ws.rs.FormParam;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
@@ -67,6 +61,8 @@ import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
//~--- JDK imports ------------------------------------------------------------
/**
* Resource to change the password of the authenticated user.
*
@@ -104,11 +100,6 @@ public class ChangePasswordResource
*
* @param oldPassword old password of the current user
* @param newPassword new password for the current user
*
* @return
*
* @throws IOException
* @throws UserException
*/
@POST
@TypeHint(RestActionResult.class)
@@ -118,10 +109,7 @@ public class ChangePasswordResource
@ResponseCode(code = 500, condition = "internal server error")
})
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public Response changePassword(@FormParam("old-password") String oldPassword,
@FormParam("new-password") String newPassword)
throws UserException, IOException
{
public Response changePassword(@FormParam("old-password") String oldPassword, @FormParam("new-password") String newPassword) throws NotFoundException, ConcurrentModificationException {
AssertUtil.assertIsNotEmpty(oldPassword);
AssertUtil.assertIsNotEmpty(newPassword);

View File

@@ -35,26 +35,20 @@ package sonia.scm.api.rest.resources;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.io.Closeables;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.repository.PathNotFoundException;
import sonia.scm.repository.RepositoryException;
import sonia.scm.repository.RevisionNotFoundException;
import sonia.scm.repository.api.DiffCommandBuilder;
import sonia.scm.repository.api.RepositoryService;
//~--- JDK imports ------------------------------------------------------------
import java.io.IOException;
import java.io.OutputStream;
import sonia.scm.util.IOUtil;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.StreamingOutput;
import sonia.scm.util.IOUtil;
import java.io.IOException;
import java.io.OutputStream;
//~--- JDK imports ------------------------------------------------------------
/**
*
@@ -96,22 +90,11 @@ public class DiffStreamingOutput implements StreamingOutput
* @throws WebApplicationException
*/
@Override
public void write(OutputStream output)
throws IOException, WebApplicationException
{
public void write(OutputStream output) throws IOException {
try
{
builder.retriveContent(output);
}
catch (PathNotFoundException ex)
{
if (logger.isWarnEnabled())
{
logger.warn("could not find path {}", ex.getPath());
}
throw new WebApplicationException(Response.Status.NOT_FOUND);
}
catch (RevisionNotFoundException ex)
{
if (logger.isWarnEnabled())
@@ -121,13 +104,13 @@ public class DiffStreamingOutput implements StreamingOutput
throw new WebApplicationException(Response.Status.NOT_FOUND);
}
catch (RepositoryException ex)
{
logger.error("could not write content to page", ex);
throw new WebApplicationException(ex,
Response.Status.INTERNAL_SERVER_ERROR);
}
// catch (RepositoryException ex)
// {
// logger.error("could not write content to page", ex);
//
// throw new WebApplicationException(ex,
// Response.Status.INTERNAL_SERVER_ERROR);
// }
finally
{
IOUtil.close(repositoryService);

View File

@@ -43,12 +43,25 @@ import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import com.webcohesion.enunciate.metadata.rs.TypeHint;
import org.apache.shiro.SecurityUtils;
import sonia.scm.group.Group;
import sonia.scm.group.GroupException;
import sonia.scm.group.GroupManager;
import sonia.scm.security.Role;
import javax.ws.rs.*;
import javax.ws.rs.core.*;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
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.Context;
import javax.ws.rs.core.GenericEntity;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Request;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import java.util.Collection;
//~--- JDK imports ------------------------------------------------------------
@@ -60,23 +73,13 @@ import java.util.Collection;
*/
@Path("groups")
@Singleton
public class GroupResource
extends AbstractManagerResource<Group, GroupException>
{
public class GroupResource extends AbstractManagerResource<Group> {
/** Field description */
public static final String PATH_PART = "groups";
//~--- constructors ---------------------------------------------------------
/**
* Constructs ...
*
*
*
* @param securityContextProvider
* @param groupManager
*/
@Inject
public GroupResource(GroupManager groupManager)
{

View File

@@ -33,9 +33,7 @@
package sonia.scm.api.rest.resources;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.base.Objects;
import com.google.common.base.MoreObjects;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.io.Files;
@@ -47,6 +45,8 @@ import com.webcohesion.enunciate.metadata.rs.TypeHint;
import org.apache.shiro.SecurityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.AlreadyExistsException;
import sonia.scm.NotFoundException;
import sonia.scm.NotSupportedFeatuerException;
import sonia.scm.Type;
import sonia.scm.api.rest.RestActionUploadResult;
@@ -75,8 +75,6 @@ import java.util.Set;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
//~--- JDK imports ------------------------------------------------------------
/**
* Rest resource for importing repositories.
*
@@ -256,10 +254,6 @@ public class RepositoryImportResource
service = serviceFactory.create(repository);
service.getPullCommand().pull(request.getUrl());
}
catch (RepositoryException ex)
{
handleImportFailure(ex, repository);
}
catch (IOException ex)
{
handleImportFailure(ex, repository);
@@ -413,11 +407,6 @@ public class RepositoryImportResource
logger.warn("exception occured durring directory import", ex);
response = Response.serverError().build();
}
catch (RepositoryException ex)
{
logger.warn("exception occured durring directory import", ex);
response = Response.serverError().build();
}
}
else
{
@@ -526,14 +515,14 @@ public class RepositoryImportResource
// repository = new Repository(null, type, name);
manager.create(repository);
}
catch (RepositoryAlreadyExistsException ex)
catch (AlreadyExistsException ex)
{
logger.warn("a {} repository with the name {} already exists", type,
name);
throw new WebApplicationException(Response.Status.CONFLICT);
}
catch (RepositoryException ex)
catch (InternalRepositoryException ex)
{
handleGenericCreationFailure(ex, type, name);
}
@@ -583,11 +572,7 @@ public class RepositoryImportResource
service = serviceFactory.create(repository);
service.getUnbundleCommand().setCompressed(compressed).unbundle(file);
}
catch (RepositoryException ex)
{
handleImportFailure(ex, repository);
}
catch (IOException ex)
catch (InternalRepositoryException ex)
{
handleImportFailure(ex, repository);
}
@@ -685,9 +670,9 @@ public class RepositoryImportResource
{
manager.delete(repository);
}
catch (RepositoryException e)
catch (InternalRepositoryException | NotFoundException e)
{
logger.error("can not delete repository", e);
logger.error("can not delete repository after import failure", e);
}
throw new WebApplicationException(ex,
@@ -741,7 +726,7 @@ public class RepositoryImportResource
{
throw new WebApplicationException(ex);
}
catch (RepositoryException ex)
catch (InternalRepositoryException ex)
{
throw new WebApplicationException(ex);
}
@@ -812,7 +797,7 @@ public class RepositoryImportResource
public String toString()
{
//J-
return Objects.toStringHelper(this)
return MoreObjects.toStringHelper(this)
.add("name", name)
.add("url", url)
.toString();

View File

@@ -46,16 +46,51 @@ import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authz.AuthorizationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.repository.*;
import sonia.scm.repository.api.*;
import sonia.scm.NotFoundException;
import sonia.scm.repository.BlameResult;
import sonia.scm.repository.Branches;
import sonia.scm.repository.BrowserResult;
import sonia.scm.repository.Changeset;
import sonia.scm.repository.ChangesetPagingResult;
import sonia.scm.repository.HealthChecker;
import sonia.scm.repository.Permission;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryIsNotArchivedException;
import sonia.scm.repository.RepositoryManager;
import sonia.scm.repository.RepositoryNotFoundException;
import sonia.scm.repository.Tags;
import sonia.scm.repository.api.BlameCommandBuilder;
import sonia.scm.repository.api.BrowseCommandBuilder;
import sonia.scm.repository.api.CatCommandBuilder;
import sonia.scm.repository.api.CommandNotSupportedException;
import sonia.scm.repository.api.DiffCommandBuilder;
import sonia.scm.repository.api.DiffFormat;
import sonia.scm.repository.api.LogCommandBuilder;
import sonia.scm.repository.api.RepositoryService;
import sonia.scm.repository.api.RepositoryServiceFactory;
import sonia.scm.util.AssertUtil;
import sonia.scm.util.HttpUtil;
import sonia.scm.util.IOUtil;
import sonia.scm.util.Util;
import javax.ws.rs.*;
import javax.ws.rs.core.*;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
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.Context;
import javax.ws.rs.core.GenericEntity;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Request;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.StreamingOutput;
import javax.ws.rs.core.UriInfo;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
@@ -69,7 +104,7 @@ import java.util.Collection;
*/
@Singleton
@Path("repositories")
public class RepositoryResource extends AbstractManagerResource<Repository, RepositoryException>
public class RepositoryResource extends AbstractManagerResource<Repository>
{
/** Field description */
@@ -169,12 +204,15 @@ public class RepositoryResource extends AbstractManagerResource<Repository, Repo
{
logger.warn("delete not allowed", ex);
response = Response.status(Response.Status.FORBIDDEN).build();
} catch (NotFoundException e) {
// there is nothing to do because delete should be idempotent
response = Response.ok().build();
}
catch (RepositoryException ex)
{
logger.error("error during delete", ex);
response = createErrorResponse(ex);
}
// catch (IOException ex)
// {
// logger.error("error during delete", ex);
// response = Response.serverError().build();
// }
}
else
{
@@ -215,10 +253,8 @@ public class RepositoryResource extends AbstractManagerResource<Repository, Repo
{
logger.warn("could not find repository ".concat(id), ex);
response = Response.status(Status.NOT_FOUND).build();
}
catch (RepositoryException | IOException ex)
{
logger.error("error occured during health check", ex);
} catch (NotFoundException e) {
logger.error("error occured during health check", e);
response = Response.serverError().build();
}
@@ -312,7 +348,6 @@ public class RepositoryResource extends AbstractManagerResource<Repository, Repo
* @return a annotate/blame view for the given path
*
* @throws IOException
* @throws RepositoryException
*/
@GET
@Path("{id}/blame")
@@ -326,7 +361,7 @@ public class RepositoryResource extends AbstractManagerResource<Repository, Repo
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public Response getBlame(@PathParam("id") String id,
@QueryParam("revision") String revision, @QueryParam("path") String path)
throws RepositoryException, IOException
throws IOException
{
Response response = null;
RepositoryService service = null;
@@ -382,7 +417,6 @@ public class RepositoryResource extends AbstractManagerResource<Repository, Repo
* @return all {@link Branches} of a repository
*
* @throws IOException
* @throws RepositoryException
*
* @since 1.18
*/
@@ -397,7 +431,7 @@ public class RepositoryResource extends AbstractManagerResource<Repository, Repo
@TypeHint(Branches.class)
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public Response getBranches(@PathParam("id") String id)
throws RepositoryException, IOException
throws IOException
{
Response response = null;
RepositoryService service = null;
@@ -446,7 +480,6 @@ public class RepositoryResource extends AbstractManagerResource<Repository, Repo
* @return a list of folders and files for the given folder
*
* @throws IOException
* @throws RepositoryException
*/
@GET
@Path("{id}/browse")
@@ -466,7 +499,7 @@ public class RepositoryResource extends AbstractManagerResource<Repository, Repo
@QueryParam("disableLastCommit") @DefaultValue("false") boolean disableLastCommit,
@QueryParam("disableSubRepositoryDetection") @DefaultValue("false") boolean disableSubRepositoryDetection,
@QueryParam("recursive") @DefaultValue("false") boolean recursive)
throws RepositoryException, IOException
throws IOException
//J+
{
Response response = null;
@@ -505,7 +538,7 @@ public class RepositoryResource extends AbstractManagerResource<Repository, Repo
response = Response.status(Response.Status.NOT_FOUND).build();
}
}
catch (RepositoryNotFoundException ex)
catch (NotFoundException ex)
{
response = Response.status(Response.Status.NOT_FOUND).build();
}
@@ -531,7 +564,6 @@ public class RepositoryResource extends AbstractManagerResource<Repository, Repo
* @return a {@link Changeset} from the given repository
*
* @throws IOException
* @throws RepositoryException
*/
@GET
@Path("{id}/changeset/{revision}")
@@ -545,7 +577,7 @@ public class RepositoryResource extends AbstractManagerResource<Repository, Repo
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public Response getChangeset(@PathParam("id") String id,
@PathParam("revision") String revision)
throws IOException, RepositoryException
throws IOException
{
Response response = null;
@@ -568,7 +600,7 @@ public class RepositoryResource extends AbstractManagerResource<Repository, Repo
response = Response.status(Status.NOT_FOUND).build();
}
}
catch (RepositoryNotFoundException ex)
catch (NotFoundException ex)
{
response = Response.status(Response.Status.NOT_FOUND).build();
}
@@ -603,7 +635,6 @@ public class RepositoryResource extends AbstractManagerResource<Repository, Repo
* @return a list of {@link Changeset} for the given repository
*
* @throws IOException
* @throws RepositoryException
*/
@GET
@Path("{id}/changesets")
@@ -623,7 +654,7 @@ public class RepositoryResource extends AbstractManagerResource<Repository, Repo
@QueryParam("branch") String branch,
@DefaultValue("0") @QueryParam("start") int start,
@DefaultValue("20") @QueryParam("limit") int limit
) throws RepositoryException, IOException
) throws IOException
//J+
{
Response response = null;
@@ -664,7 +695,7 @@ public class RepositoryResource extends AbstractManagerResource<Repository, Repo
response = Response.ok().build();
}
}
catch (RepositoryNotFoundException ex)
catch (NotFoundException ex)
{
response = Response.status(Response.Status.NOT_FOUND).build();
}
@@ -759,7 +790,6 @@ public class RepositoryResource extends AbstractManagerResource<Repository, Repo
* @return the modifications of a {@link Changeset}
*
* @throws IOException
* @throws RepositoryException
*/
@GET
@Path("{id}/diff")
@@ -774,7 +804,7 @@ public class RepositoryResource extends AbstractManagerResource<Repository, Repo
public Response getDiff(@PathParam("id") String id,
@QueryParam("revision") String revision, @QueryParam("path") String path,
@QueryParam("format") DiffFormat format)
throws RepositoryException, IOException
throws IOException
{
AssertUtil.assertIsNotEmpty(id);
AssertUtil.assertIsNotEmpty(revision);
@@ -841,7 +871,6 @@ public class RepositoryResource extends AbstractManagerResource<Repository, Repo
* @return all {@link Tags} of a repository
*
* @throws IOException
* @throws RepositoryException
*
* @since 1.18
*/
@@ -856,7 +885,7 @@ public class RepositoryResource extends AbstractManagerResource<Repository, Repo
@TypeHint(Tags.class)
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public Response getTags(@PathParam("id") String id)
throws RepositoryException, IOException
throws IOException
{
Response response = null;
RepositoryService service = null;

View File

@@ -45,13 +45,25 @@ import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.credential.PasswordService;
import sonia.scm.security.Role;
import sonia.scm.user.User;
import sonia.scm.user.UserException;
import sonia.scm.user.UserManager;
import sonia.scm.util.AssertUtil;
import sonia.scm.util.Util;
import javax.ws.rs.*;
import javax.ws.rs.core.*;
import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
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.Context;
import javax.ws.rs.core.GenericEntity;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Request;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import java.util.Collection;
//~--- JDK imports ------------------------------------------------------------
@@ -63,7 +75,7 @@ import java.util.Collection;
*/
@Singleton
@Path("users")
public class UserResource extends AbstractManagerResource<User, UserException>
public class UserResource extends AbstractManagerResource<User>
{
/** Field description */

View File

@@ -0,0 +1,58 @@
package sonia.scm.api.v2;
import lombok.Getter;
import org.jboss.resteasy.api.validation.ResteasyViolationException;
import javax.validation.ConstraintViolation;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlRootElement;
import java.util.List;
import java.util.stream.Collectors;
@Provider
public class ValidationExceptionMapper implements ExceptionMapper<ResteasyViolationException> {
@Override
public Response toResponse(ResteasyViolationException exception) {
List<ConstraintViolationBean> violations =
exception.getConstraintViolations()
.stream()
.map(ConstraintViolationBean::new)
.collect(Collectors.toList());
return Response
.status(Response.Status.BAD_REQUEST)
.type(MediaType.APPLICATION_JSON_TYPE)
.entity(new ValidationError(violations))
.build();
}
@Getter
public static class ValidationError {
@XmlElement(name = "violation")
@XmlElementWrapper(name = "violations")
private List<ConstraintViolationBean> violations;
public ValidationError(List<ConstraintViolationBean> violations) {
this.violations = violations;
}
}
@XmlRootElement(name = "violation")
@Getter
public static class ConstraintViolationBean {
private String path;
private String message;
public ConstraintViolationBean(ConstraintViolation<?> violation) {
message = violation.getMessage();
path = violation.getPropertyPath().toString();
}
}
}

View File

@@ -9,7 +9,6 @@ import sonia.scm.repository.Changeset;
import sonia.scm.repository.ChangesetPagingResult;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryException;
import sonia.scm.repository.RepositoryNotFoundException;
import sonia.scm.repository.RepositoryPermissions;
import sonia.scm.repository.api.CommandNotSupportedException;
@@ -65,7 +64,7 @@ public class BranchRootResource {
@ResponseCode(code = 404, condition = "not found, no branch with the specified name 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("branch") String branchName) throws IOException, RepositoryException {
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))) {
Branches branches = repositoryService.getBranchesCommand().getBranches();
return branches.getBranches()
@@ -136,7 +135,7 @@ public class BranchRootResource {
@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, RepositoryException {
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();

View File

@@ -29,7 +29,7 @@ public abstract class BranchToBranchDtoMapper {
.self(resourceLinks.branch().self(namespaceAndName, target.getName()))
.single(linkBuilder("history", resourceLinks.branch().history(namespaceAndName, target.getName())).build())
.single(linkBuilder("changeset", resourceLinks.changeset().changeset(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getRevision())).build())
.single(linkBuilder("source", resourceLinks.source().source(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getRevision())).build());
.single(linkBuilder("source", resourceLinks.source().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getRevision())).build());
target.add(linksBuilder.build());
}
}

View File

@@ -0,0 +1,40 @@
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.util.Iterator;
import java.util.List;
@Getter
@Setter
@NoArgsConstructor
public class BrowserResultDto extends HalRepresentation implements Iterable<FileObjectDto> {
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
protected HalRepresentation add(Links links) {
return super.add(links);
}
// REVIEW return null?
@Override
public Iterator<FileObjectDto> iterator() {
Iterator<FileObjectDto> it = null;
if (files != null)
{
it = files.iterator();
}
return it;
}
}

View File

@@ -0,0 +1,50 @@
package sonia.scm.api.v2.resources;
import de.otto.edison.hal.Links;
import sonia.scm.repository.BrowserResult;
import sonia.scm.repository.FileObject;
import sonia.scm.repository.NamespaceAndName;
import javax.inject.Inject;
import java.util.ArrayList;
import java.util.List;
public class BrowserResultToBrowserResultDtoMapper {
@Inject
private FileObjectToFileObjectDtoMapper fileObjectToFileObjectDtoMapper;
@Inject
private ResourceLinks resourceLinks;
public BrowserResultDto map(BrowserResult browserResult, NamespaceAndName namespaceAndName) {
BrowserResultDto browserResultDto = new BrowserResultDto();
browserResultDto.setTag(browserResult.getTag());
browserResultDto.setBranch(browserResult.getBranch());
browserResultDto.setRevision(browserResult.getRevision());
List<FileObjectDto> fileObjectDtoList = new ArrayList<>();
for (FileObject fileObject : browserResult.getFiles()) {
fileObjectDtoList.add(mapFileObject(fileObject, namespaceAndName, browserResult.getRevision()));
}
browserResultDto.setFiles(fileObjectDtoList);
this.addLinks(browserResult, browserResultDto, namespaceAndName);
return browserResultDto;
}
private FileObjectDto mapFileObject(FileObject fileObject, NamespaceAndName namespaceAndName, String revision) {
return fileObjectToFileObjectDtoMapper.map(fileObject, namespaceAndName, revision);
}
private void addLinks(BrowserResult browserResult, BrowserResultDto dto, NamespaceAndName namespaceAndName) {
if (browserResult.getRevision() == null) {
dto.add(Links.linkingTo().self(resourceLinks.source().selfWithoutRevision(namespaceAndName.getNamespace(), namespaceAndName.getName())).build());
} else {
dto.add(Links.linkingTo().self(resourceLinks.source().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), browserResult.getRevision())).build());
}
}
}

View File

@@ -9,8 +9,9 @@ import sonia.scm.repository.Changeset;
import sonia.scm.repository.ChangesetPagingResult;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryException;
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;
@@ -54,7 +55,7 @@ public class ChangesetRootResource {
@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, RepositoryException {
@DefaultValue("10") @QueryParam("pageSize") int pageSize) throws IOException, RevisionNotFoundException, RepositoryNotFoundException {
RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name));
Repository repository = repositoryService.getRepository();
RepositoryPermissions.read(repository).check();
@@ -81,7 +82,7 @@ public class ChangesetRootResource {
@Produces(VndMediaType.CHANGESET)
@TypeHint(ChangesetDto.class)
@Path("{id}")
public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("id") String id) throws RepositoryException, IOException {
public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("id") String id) throws IOException, RevisionNotFoundException, RepositoryNotFoundException {
RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name));
Repository repository = repositoryService.getRepository();
RepositoryPermissions.read(repository).check();

View File

@@ -1,6 +1,7 @@
package sonia.scm.api.v2.resources;
import de.otto.edison.hal.HalRepresentation;
import sonia.scm.AlreadyExistsException;
import sonia.scm.Manager;
import sonia.scm.ModelObject;
import sonia.scm.PageResult;
@@ -22,16 +23,14 @@ import static javax.ws.rs.core.Response.Status.BAD_REQUEST;
*
* @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}.
*
* @see SingleResourceManagerAdapter
*/
@SuppressWarnings("squid:S00119") // "MODEL_OBJECT" is much more meaningful than "M", right?
class CollectionResourceManagerAdapter<MODEL_OBJECT extends ModelObject,
DTO extends HalRepresentation,
EXCEPTION extends Exception> extends AbstractManagerResource<MODEL_OBJECT, EXCEPTION> {
DTO extends HalRepresentation> extends AbstractManagerResource<MODEL_OBJECT> {
CollectionResourceManagerAdapter(Manager<MODEL_OBJECT, EXCEPTION> manager, Class<MODEL_OBJECT> type) {
CollectionResourceManagerAdapter(Manager<MODEL_OBJECT> manager, Class<MODEL_OBJECT> type) {
super(manager, type);
}
@@ -48,7 +47,7 @@ class CollectionResourceManagerAdapter<MODEL_OBJECT extends ModelObject,
* Creates a model object for the given dto and returns a corresponding http response.
* This handles all corner cases, eg. no conflicts or missing privileges.
*/
public Response create(DTO dto, Supplier<MODEL_OBJECT> modelObjectSupplier, Function<MODEL_OBJECT, String> uriCreator) throws EXCEPTION {
public Response create(DTO dto, Supplier<MODEL_OBJECT> modelObjectSupplier, Function<MODEL_OBJECT, String> uriCreator) throws AlreadyExistsException {
if (dto == null) {
return Response.status(BAD_REQUEST).build();
}

View File

@@ -8,7 +8,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.PathNotFoundException;
import sonia.scm.repository.RepositoryException;
import sonia.scm.repository.RepositoryNotFoundException;
import sonia.scm.repository.RevisionNotFoundException;
import sonia.scm.repository.api.RepositoryService;
@@ -61,8 +60,8 @@ public class ContentResource {
@ResponseCode(code = 500, condition = "internal server error")
})
public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision, @PathParam("path") String path) {
StreamingOutput stream = createStreamingOutput(namespace, name, revision, path);
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
StreamingOutput stream = createStreamingOutput(namespace, name, revision, path, repositoryService);
Response.ResponseBuilder responseBuilder = Response.ok(stream);
return createContentHeader(namespace, name, revision, path, repositoryService, responseBuilder);
} catch (RepositoryNotFoundException e) {
@@ -71,17 +70,20 @@ public class ContentResource {
}
}
private StreamingOutput createStreamingOutput(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision, @PathParam("path") String path, RepositoryService repositoryService) {
private StreamingOutput createStreamingOutput(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision, @PathParam("path") String path) {
return os -> {
try {
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
repositoryService.getCatCommand().setRevision(revision).retriveContent(os, path);
os.close();
} catch (RepositoryNotFoundException e) {
LOG.debug("repository {}/{} not found", path, namespace, name, e);
throw new WebApplicationException(Status.NOT_FOUND);
} catch (PathNotFoundException e) {
LOG.debug("path '{}' not found in repository {}/{}", path, namespace, name, e);
throw new WebApplicationException(Status.NOT_FOUND);
} catch (RepositoryException e) {
LOG.info("error reading repository resource {} from {}/{}", path, namespace, name, e);
throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
} catch (RevisionNotFoundException e) {
LOG.debug("revision '{}' not found in repository {}/{}", revision, namespace, name, e);
throw new WebApplicationException(Status.NOT_FOUND);
}
};
}
@@ -124,7 +126,7 @@ public class ContentResource {
} catch (RevisionNotFoundException e) {
LOG.debug("revision '{}' not found in repository {}/{}", revision, namespace, name, e);
return Response.status(Status.NOT_FOUND).build();
} catch (IOException | RepositoryException e) {
} catch (IOException e) {
LOG.info("error reading repository resource {} from {}/{}", path, namespace, name, e);
return Response.status(Status.INTERNAL_SERVER_ERROR).entity(e.getMessage()).build();
}
@@ -137,12 +139,14 @@ public class ContentResource {
contentType.getLanguage().ifPresent(language -> responseBuilder.header("Language", language));
}
private byte[] getHead(String revision, String path, RepositoryService repositoryService) throws IOException, RepositoryException {
private byte[] getHead(String revision, String path, RepositoryService repositoryService) throws IOException, PathNotFoundException, RevisionNotFoundException {
InputStream stream = repositoryService.getCatCommand().setRevision(revision).getStream(path);
try {
byte[] buffer = new byte[HEAD_BUFFER_SIZE];
int length = stream.read(buffer);
if (length < buffer.length) {
if (length < 0) { // empty file
return new byte[]{};
} else if (length < buffer.length) {
return Arrays.copyOf(buffer, length);
} else {
return buffer;

View File

@@ -0,0 +1,28 @@
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;
@Getter
@Setter
@NoArgsConstructor
public class FileObjectDto extends HalRepresentation {
private String name;
private String path;
private boolean directory;
private String description;
private int length;
private Instant lastModified;
private SubRepositoryDto subRepository;
@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,46 @@
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.MappingTarget;
import sonia.scm.repository.FileObject;
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> {
@Inject
private ResourceLinks resourceLinks;
protected abstract FileObjectDto map(FileObject fileObject, @Context NamespaceAndName namespaceAndName, @Context String revision);
abstract SubRepositoryDto mapSubrepository(SubRepository subRepository);
@AfterMapping
void addLinks(FileObject fileObject, @MappingTarget FileObjectDto dto, @Context NamespaceAndName namespaceAndName, @Context String revision) {
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));
} else {
links.self(addPath(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,15 +1,25 @@
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.AlreadyExistsException;
import sonia.scm.group.Group;
import sonia.scm.group.GroupException;
import sonia.scm.group.GroupManager;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
import javax.ws.rs.*;
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.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response;
import java.io.IOException;
public class GroupCollectionResource {
@@ -19,7 +29,7 @@ public class GroupCollectionResource {
private final GroupCollectionToDtoMapper groupCollectionToDtoMapper;
private final ResourceLinks resourceLinks;
private final IdResourceManagerAdapter<Group, GroupDto, GroupException> adapter;
private final IdResourceManagerAdapter<Group, GroupDto> adapter;
@Inject
public GroupCollectionResource(GroupManager manager, GroupDtoToGroupMapper dtoToGroupMapper, GroupCollectionToDtoMapper groupCollectionToDtoMapper, ResourceLinks resourceLinks) {
@@ -76,7 +86,7 @@ public class GroupCollectionResource {
})
@TypeHint(TypeHint.NO_CONTENT.class)
@ResponseHeaders(@ResponseHeader(name = "Location", description = "uri to the created group"))
public Response create(GroupDto groupDto) throws IOException, GroupException {
public Response create(@Valid GroupDto groupDto) throws AlreadyExistsException {
return adapter.create(groupDto,
() -> dtoToGroupMapper.map(groupDto),
group -> resourceLinks.group().self(group.getName()));

View File

@@ -6,7 +6,9 @@ import de.otto.edison.hal.Links;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.validator.constraints.NotEmpty;
import javax.validation.constraints.Pattern;
import java.time.Instant;
import java.util.List;
import java.util.Map;
@@ -18,7 +20,9 @@ public class GroupDto extends HalRepresentation {
private String description;
@JsonInclude(JsonInclude.Include.NON_NULL)
private Instant lastModified;
@Pattern(regexp = "^[A-z0-9\\.\\-_@]|[^ ]([A-z0-9\\.\\-_@ ]*[A-z0-9\\.\\-_@]|[^ ])?$")
private String name;
@NotEmpty
private String type;
private Map<String, String> properties;
private List<String> members;

View File

@@ -4,12 +4,12 @@ import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import sonia.scm.group.Group;
import java.time.Instant;
@Mapper
public abstract class GroupDtoToGroupMapper {
public abstract class GroupDtoToGroupMapper extends BaseDtoMapper {
@Mapping(target = "creationDate", ignore = true)
@Mapping(target = "lastModified", ignore = true)
public abstract Group map(GroupDto groupDto);
}

View File

@@ -3,20 +3,28 @@ 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.ConcurrentModificationException;
import sonia.scm.NotFoundException;
import sonia.scm.group.Group;
import sonia.scm.group.GroupException;
import sonia.scm.group.GroupManager;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
import javax.ws.rs.*;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Response;
public class GroupResource {
private final GroupToGroupDtoMapper groupToGroupDtoMapper;
private final GroupDtoToGroupMapper dtoToGroupMapper;
private final IdResourceManagerAdapter<Group, GroupDto, GroupException> adapter;
private final IdResourceManagerAdapter<Group, GroupDto> adapter;
@Inject
public GroupResource(GroupManager manager, GroupToGroupDtoMapper groupToGroupDtoMapper,
@@ -45,7 +53,7 @@ public class GroupResource {
@ResponseCode(code = 404, condition = "not found, no group with the specified id/name available"),
@ResponseCode(code = 500, condition = "internal server error")
})
public Response get(@PathParam("id") String id) {
public Response get(@PathParam("id") String id) throws NotFoundException {
return adapter.get(id, groupToGroupDtoMapper::map);
}
@@ -90,7 +98,7 @@ public class GroupResource {
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(TypeHint.NO_CONTENT.class)
public Response update(@PathParam("id") String name, GroupDto groupDto) {
public Response update(@PathParam("id") String name, @Valid GroupDto groupDto) throws NotFoundException, ConcurrentModificationException {
return adapter.update(name, existing -> dtoToGroupMapper.map(groupDto));
}
}

View File

@@ -1,12 +1,14 @@
package sonia.scm.api.v2.resources;
import de.otto.edison.hal.HalRepresentation;
import sonia.scm.AlreadyExistsException;
import sonia.scm.ConcurrentModificationException;
import sonia.scm.Manager;
import sonia.scm.ModelObject;
import sonia.scm.NotFoundException;
import sonia.scm.PageResult;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Predicate;
@@ -18,25 +20,24 @@ import java.util.function.Supplier;
*/
@SuppressWarnings("squid:S00119") // "MODEL_OBJECT" is much more meaningful than "M", right?
class IdResourceManagerAdapter<MODEL_OBJECT extends ModelObject,
DTO extends HalRepresentation,
EXCEPTION extends Exception> {
DTO extends HalRepresentation> {
private final Manager<MODEL_OBJECT, EXCEPTION> manager;
private final Manager<MODEL_OBJECT> manager;
private final SingleResourceManagerAdapter<MODEL_OBJECT, DTO, EXCEPTION> singleAdapter;
private final CollectionResourceManagerAdapter<MODEL_OBJECT, DTO, EXCEPTION> collectionAdapter;
private final SingleResourceManagerAdapter<MODEL_OBJECT, DTO> singleAdapter;
private final CollectionResourceManagerAdapter<MODEL_OBJECT, DTO> collectionAdapter;
IdResourceManagerAdapter(Manager<MODEL_OBJECT, EXCEPTION> manager, Class<MODEL_OBJECT> type) {
IdResourceManagerAdapter(Manager<MODEL_OBJECT> manager, Class<MODEL_OBJECT> type) {
this.manager = manager;
singleAdapter = new SingleResourceManagerAdapter<>(manager, type);
collectionAdapter = new CollectionResourceManagerAdapter<>(manager, type);
}
Response get(String id, Function<MODEL_OBJECT, DTO> mapToDto) {
Response get(String id, Function<MODEL_OBJECT, DTO> mapToDto) throws NotFoundException {
return singleAdapter.get(loadBy(id), mapToDto);
}
public Response update(String id, Function<MODEL_OBJECT, MODEL_OBJECT> applyChanges) {
public Response update(String id, Function<MODEL_OBJECT, MODEL_OBJECT> applyChanges) throws NotFoundException, ConcurrentModificationException {
return singleAdapter.update(
loadBy(id),
applyChanges,
@@ -48,7 +49,7 @@ class IdResourceManagerAdapter<MODEL_OBJECT extends ModelObject,
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 {
public Response create(DTO dto, Supplier<MODEL_OBJECT> modelObjectSupplier, Function<MODEL_OBJECT, String> uriCreator) throws AlreadyExistsException {
return collectionAdapter.create(dto, modelObjectSupplier, uriCreator);
}

View File

@@ -33,6 +33,12 @@ public class MapperModule extends AbstractModule {
bind(TagToTagDtoMapper.class).to(Mappers.getMapper(TagToTagDtoMapper.class).getClass());
bind(FileObjectToFileObjectDtoMapper.class).to(Mappers.getMapper(FileObjectToFileObjectDtoMapper.class).getClass());
// no mapstruct required
bind(UIPluginDtoMapper.class);
bind(UIPluginDtoCollectionMapper.class);
bind(UriInfoStore.class).in(ServletScopes.REQUEST);
}
}

View File

@@ -4,8 +4,8 @@ import com.webcohesion.enunciate.metadata.rs.ResponseCode;
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import com.webcohesion.enunciate.metadata.rs.TypeHint;
import org.apache.shiro.SecurityUtils;
import sonia.scm.NotFoundException;
import sonia.scm.user.User;
import sonia.scm.user.UserException;
import sonia.scm.user.UserManager;
import sonia.scm.web.VndMediaType;
@@ -28,7 +28,7 @@ public class MeResource {
private final UserToUserDtoMapper userToDtoMapper;
private final IdResourceManagerAdapter<User, UserDto, UserException> adapter;
private final IdResourceManagerAdapter<User, UserDto> adapter;
@Inject
public MeResource(UserToUserDtoMapper userToDtoMapper, UserManager manager) {
this.userToDtoMapper = userToDtoMapper;
@@ -47,7 +47,7 @@ public class MeResource {
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
@ResponseCode(code = 500, condition = "internal server error")
})
public Response get(@Context Request request, @Context UriInfo uriInfo) {
public Response get(@Context Request request, @Context UriInfo uriInfo) throws NotFoundException {
String id = (String) SecurityUtils.getSubject().getPrincipals().getPrimaryPrincipal();
return adapter.get(id, userToDtoMapper::map);

View File

@@ -31,8 +31,8 @@
package sonia.scm.api.v2.resources;
import sonia.scm.NotFoundException;
import sonia.scm.api.rest.StatusExceptionMapper;
import sonia.scm.repository.PermissionNotFoundException;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.Provider;
@@ -41,9 +41,9 @@ import javax.ws.rs.ext.Provider;
* @since 2.0.0
*/
@Provider
public class PermissionNotFoundExceptionMapper extends StatusExceptionMapper<PermissionNotFoundException> {
public class NotFoundExceptionMapper extends StatusExceptionMapper<NotFoundException> {
public PermissionNotFoundExceptionMapper() {
super(PermissionNotFoundException.class, Response.Status.NOT_FOUND);
public NotFoundExceptionMapper() {
super(NotFoundException.class, Response.Status.NOT_FOUND);
}
}

View File

@@ -1,49 +0,0 @@
/*
Copyright (c) 2014, Sebastian Sdorra All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer. 2. Redistributions in
binary form must reproduce the above copyright notice, this list of
conditions and the following disclaimer in the documentation and/or other
materials provided with the distribution. 3. Neither the name of SCM-Manager;
nor the names of its contributors may be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
http://bitbucket.org/sdorra/scm-manager
*/
package sonia.scm.api.v2.resources;
import sonia.scm.api.rest.StatusExceptionMapper;
import sonia.scm.repository.PermissionAlreadyExistsException;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.Provider;
/**
* @since 2.0.0
*/
@Provider
public class PermissionAlreadyExistsExceptionMapper extends StatusExceptionMapper<PermissionAlreadyExistsException> {
public PermissionAlreadyExistsExceptionMapper() {
super(PermissionAlreadyExistsException.class, Response.Status.CONFLICT);
}
}

View File

@@ -6,11 +6,25 @@ 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.repository.*;
import sonia.scm.AlreadyExistsException;
import sonia.scm.NotFoundException;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Permission;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryManager;
import sonia.scm.repository.RepositoryNotFoundException;
import sonia.scm.repository.RepositoryPermissions;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
import javax.ws.rs.*;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Response;
import java.net.URI;
import java.util.Optional;
@@ -53,7 +67,7 @@ public class PermissionRootResource {
@TypeHint(TypeHint.NO_CONTENT.class)
@Consumes(VndMediaType.PERMISSION)
@Path("")
public Response create(@PathParam("namespace") String namespace, @PathParam("name") String name, PermissionDto permission) throws RepositoryException {
public Response create(@PathParam("namespace") String namespace, @PathParam("name") String name, PermissionDto permission) throws Exception {
log.info("try to add new permission: {}", permission);
Repository repository = load(namespace, name);
RepositoryPermissions.permissionWrite(repository).check();
@@ -70,7 +84,7 @@ public class PermissionRootResource {
* @param namespace the repository namespace
* @param name the repository name
* @return the http response with a list of permissionDto objects
* @throws RepositoryNotFoundException if the repository does not exists
* @throws NotFoundException if the repository does not exists
*/
@GET
@StatusCodes({
@@ -81,7 +95,7 @@ public class PermissionRootResource {
@Produces(VndMediaType.PERMISSION)
@TypeHint(PermissionDto.class)
@Path("{permission-name}")
public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("permission-name") String permissionName) throws RepositoryException {
public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("permission-name") String permissionName) throws NotFoundException {
Repository repository = load(namespace, name);
RepositoryPermissions.permissionRead(repository).check();
return Response.ok(
@@ -90,7 +104,7 @@ public class PermissionRootResource {
.filter(permission -> permissionName.equals(permission.getName()))
.map(permission -> modelToDtoMapper.map(permission, repository))
.findFirst()
.orElseThrow(() -> new PermissionNotFoundException(repository, permissionName))
.orElseThrow(NotFoundException::new)
).build();
}
@@ -101,7 +115,7 @@ public class PermissionRootResource {
* @param namespace the repository namespace
* @param name the repository name
* @return the http response with a list of permissionDto objects
* @throws RepositoryNotFoundException if the repository does not exists
* @throws NotFoundException if the repository does not exists
*/
@GET
@StatusCodes({
@@ -138,7 +152,7 @@ public class PermissionRootResource {
public Response update(@PathParam("namespace") String namespace,
@PathParam("name") String name,
@PathParam("permission-name") String permissionName,
PermissionDto permission) throws RepositoryException {
PermissionDto permission) throws NotFoundException {
log.info("try to update the permission with name: {}. the modified permission is: {}", permissionName, permission);
Repository repository = load(namespace, name);
RepositoryPermissions.permissionWrite(repository).check();
@@ -146,7 +160,7 @@ public class PermissionRootResource {
.stream()
.filter(perm -> StringUtils.isNotBlank(perm.getName()) && perm.getName().equals(permissionName))
.findFirst()
.orElseThrow(() -> new PermissionNotFoundException(repository, permissionName));
.orElseThrow(() -> new NotFoundException());
dtoToModelMapper.modify(existingPermission, permission);
manager.modify(repository);
log.info("the permission with name: {} is updated.", permissionName);
@@ -170,9 +184,9 @@ public class PermissionRootResource {
@Path("{permission-name}")
public Response delete(@PathParam("namespace") String namespace,
@PathParam("name") String name,
@PathParam("permission-name") String permissionName) throws RepositoryException {
@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()
@@ -205,14 +219,14 @@ public class PermissionRootResource {
*
* @param permission the searched permission
* @param repository the repository to be inspected
* @throws PermissionAlreadyExistsException if the permission already exists in the repository
* @throws AlreadyExistsException if the permission already exists in the repository
*/
private void checkPermissionAlreadyExists(PermissionDto permission, Repository repository) throws PermissionAlreadyExistsException {
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 PermissionAlreadyExistsException(repository, permission.getName());
throw new AlreadyExistsException();
}
}
}

View File

@@ -1,7 +1,12 @@
package sonia.scm.api.v2.resources;
import de.otto.edison.hal.Links;
import org.mapstruct.*;
import org.mapstruct.AfterMapping;
import org.mapstruct.BeforeMapping;
import org.mapstruct.Context;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.MappingTarget;
import sonia.scm.repository.Permission;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryPermissions;
@@ -43,4 +48,3 @@ public abstract class PermissionToPermissionDtoMapper {
target.add(linksBuilder.build());
}
}

View File

@@ -1,20 +1,31 @@
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.AlreadyExistsException;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryException;
import sonia.scm.repository.RepositoryManager;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
import javax.ws.rs.*;
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.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response;
public class RepositoryCollectionResource {
private static final int DEFAULT_PAGE_SIZE = 10;
private final CollectionResourceManagerAdapter<Repository, RepositoryDto, RepositoryException> adapter;
private final CollectionResourceManagerAdapter<Repository, RepositoryDto> adapter;
private final RepositoryCollectionToDtoMapper repositoryCollectionToDtoMapper;
private final RepositoryDtoToRepositoryMapper dtoToRepositoryMapper;
private final ResourceLinks resourceLinks;
@@ -76,7 +87,7 @@ public class RepositoryCollectionResource {
})
@TypeHint(TypeHint.NO_CONTENT.class)
@ResponseHeaders(@ResponseHeader(name = "Location", description = "uri to the created repository"))
public Response create(RepositoryDto repositoryDto) throws RepositoryException {
public Response create(@Valid RepositoryDto repositoryDto) throws AlreadyExistsException {
return adapter.create(repositoryDto,
() -> dtoToRepositoryMapper.map(repositoryDto, null),
repository -> resourceLinks.repository().self(repository.getNamespace(), repository.getName()));

View File

@@ -5,7 +5,10 @@ import de.otto.edison.hal.HalRepresentation;
import de.otto.edison.hal.Links;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.NotEmpty;
import javax.validation.constraints.Pattern;
import java.time.Instant;
import java.util.List;
import java.util.Map;
@@ -13,6 +16,7 @@ import java.util.Map;
@Getter @Setter
public class RepositoryDto extends HalRepresentation {
@Email
private String contact;
private Instant creationDate;
private String description;
@@ -20,8 +24,10 @@ public class RepositoryDto extends HalRepresentation {
@JsonInclude(JsonInclude.Include.NON_NULL)
private Instant lastModified;
private String namespace;
@Pattern(regexp = "(?!^\\.\\.$)(?!^\\.$)(?!.*[\\\\\\[\\]])^[A-z0-9\\.][A-z0-9\\.\\-_/]*$")
private String name;
private boolean archived = false;
@NotEmpty
private String type;
protected Map<String, String> properties;

View File

@@ -4,10 +4,9 @@ import org.mapstruct.*;
import sonia.scm.repository.Repository;
@Mapper
public abstract class RepositoryDtoToRepositoryMapper {
public abstract class RepositoryDtoToRepositoryMapper extends BaseDtoMapper {
@Mapping(target = "creationDate", ignore = true)
@Mapping(target = "lastModified", ignore = true)
@Mapping(target = "id", ignore = true)
@Mapping(target = "publicReadable", ignore = true)
@Mapping(target = "healthCheckFailures", ignore = true)

View File

@@ -1,49 +0,0 @@
/*
Copyright (c) 2014, Sebastian Sdorra All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer. 2. Redistributions in
binary form must reproduce the above copyright notice, this list of
conditions and the following disclaimer in the documentation and/or other
materials provided with the distribution. 3. Neither the name of SCM-Manager;
nor the names of its contributors may be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
http://bitbucket.org/sdorra/scm-manager
*/
package sonia.scm.api.v2.resources;
import sonia.scm.api.rest.StatusExceptionMapper;
import sonia.scm.repository.RepositoryNotFoundException;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.Provider;
/**
* @since 2.0.0
*/
@Provider
public class RepositoryNotFoundExceptionMapper extends StatusExceptionMapper<RepositoryNotFoundException> {
public RepositoryNotFoundExceptionMapper() {
super(RepositoryNotFoundException.class, Response.Status.NOT_FOUND);
}
}

View File

@@ -3,15 +3,17 @@ 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.ConcurrentModificationException;
import sonia.scm.NotFoundException;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryException;
import sonia.scm.repository.RepositoryIsNotArchivedException;
import sonia.scm.repository.RepositoryManager;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
@@ -30,7 +32,7 @@ public class RepositoryResource {
private final RepositoryDtoToRepositoryMapper dtoToRepositoryMapper;
private final RepositoryManager manager;
private final SingleResourceManagerAdapter<Repository, RepositoryDto, RepositoryException> adapter;
private final SingleResourceManagerAdapter<Repository, RepositoryDto> adapter;
private final Provider<TagRootResource> tagRootResource;
private final Provider<BranchRootResource> branchRootResource;
private final Provider<ChangesetRootResource> changesetRootResource;
@@ -82,7 +84,7 @@ public class RepositoryResource {
@ResponseCode(code = 404, condition = "not found, no repository with the specified name available in the namespace"),
@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) throws NotFoundException {
return adapter.get(loadBy(namespace, name), repositoryToDtoMapper::map);
}
@@ -129,7 +131,7 @@ public class RepositoryResource {
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(TypeHint.NO_CONTENT.class)
public Response update(@PathParam("namespace") String namespace, @PathParam("name") String name, RepositoryDto repositoryDto) {
public Response update(@PathParam("namespace") String namespace, @PathParam("name") String name, @Valid RepositoryDto repositoryDto) throws NotFoundException, ConcurrentModificationException {
return adapter.update(
loadBy(namespace, name),
existing -> processUpdate(repositoryDto, existing),

View File

@@ -47,7 +47,7 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper<Reposit
}
}
linksBuilder.single(link("changesets", resourceLinks.changeset().all(target.getNamespace(), target.getName())));
linksBuilder.single(link("sources", resourceLinks.source().self(target.getNamespace(), target.getName())));
linksBuilder.single(link("sources", resourceLinks.source().selfWithoutRevision(target.getNamespace(), target.getName())));
target.add(linksBuilder.build());
}
}

View File

@@ -318,13 +318,29 @@ class ResourceLinks {
sourceLinkBuilder = new LinkBuilder(uriInfo, RepositoryRootResource.class, RepositoryResource.class, SourceRootResource.class);
}
String self(String namespace, String name) {
return sourceLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("sources").parameters().method("getAll").parameters().href();
String self(String namespace, String name, String revision) {
return sourceLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("sources").parameters().method("getAll").parameters(revision).href();
}
String selfWithoutRevision(String namespace, String name) {
return sourceLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("sources").parameters().method("getAllWithoutRevision").parameters().href();
}
public String source(String namespace, String name, String revision) {
return sourceLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("sources").parameters().method("get").parameters(revision).href();
}
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();
}
}
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();
}
}
public PermissionLinks permission() {
return new PermissionLinks(uriInfoStore.get());
@@ -361,4 +377,37 @@ class ResourceLinks {
return permissionLinkBuilder.method("getRepositoryResource").parameters(repositoryNamespace, repositoryName).method("permissions").parameters().method(methodName).parameters(permissionName).href();
}
}
public UIPluginLinks uiPlugin() {
return new UIPluginLinks(uriInfoStore.get());
}
static class UIPluginLinks {
private final LinkBuilder uiPluginLinkBuilder;
UIPluginLinks(UriInfo uriInfo) {
uiPluginLinkBuilder = new LinkBuilder(uriInfo, UIRootResource.class, UIPluginResource.class);
}
String self(String id) {
return uiPluginLinkBuilder.method("plugins").parameters().method("getInstalledPlugin").parameters(id).href();
}
}
public UIPluginCollectionLinks uiPluginCollection() {
return new UIPluginCollectionLinks(uriInfoStore.get());
}
static class UIPluginCollectionLinks {
private final LinkBuilder uiPluginCollectionLinkBuilder;
UIPluginCollectionLinks(UriInfo uriInfo) {
uiPluginCollectionLinkBuilder = new LinkBuilder(uriInfo, UIRootResource.class, UIPluginResource.class);
}
String self() {
return uiPluginCollectionLinkBuilder.method("plugins").parameters().method("getInstalledPlugins").parameters().href();
}
}
}

View File

@@ -1,8 +1,10 @@
package sonia.scm.api.v2.resources;
import de.otto.edison.hal.HalRepresentation;
import sonia.scm.ConcurrentModificationException;
import sonia.scm.Manager;
import sonia.scm.ModelObject;
import sonia.scm.NotFoundException;
import sonia.scm.api.rest.resources.AbstractManagerResource;
import javax.ws.rs.core.GenericEntity;
@@ -22,22 +24,20 @@ import static javax.ws.rs.core.Response.Status.BAD_REQUEST;
*
* @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}.
*
* @see CollectionResourceManagerAdapter
*/
@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> {
DTO extends HalRepresentation> extends AbstractManagerResource<MODEL_OBJECT> {
private final Function<Throwable, Optional<Response>> errorHandler;
SingleResourceManagerAdapter(Manager<MODEL_OBJECT, EXCEPTION> manager, Class<MODEL_OBJECT> type) {
SingleResourceManagerAdapter(Manager<MODEL_OBJECT> manager, Class<MODEL_OBJECT> type) {
this(manager, type, e -> Optional.empty());
}
SingleResourceManagerAdapter(Manager<MODEL_OBJECT, EXCEPTION> manager, Class<MODEL_OBJECT> type, Function<Throwable, Optional<Response>> errorHandler) {
SingleResourceManagerAdapter(Manager<MODEL_OBJECT> manager, Class<MODEL_OBJECT> type, Function<Throwable, Optional<Response>> errorHandler) {
super(manager, type);
this.errorHandler = errorHandler;
}
@@ -46,28 +46,33 @@ class SingleResourceManagerAdapter<MODEL_OBJECT extends ModelObject,
* 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<Optional<MODEL_OBJECT>> reader, Function<MODEL_OBJECT, DTO> mapToDto) {
Response get(Supplier<Optional<MODEL_OBJECT>> reader, Function<MODEL_OBJECT, DTO> mapToDto) throws NotFoundException {
return reader.get()
.map(mapToDto)
.map(Response::ok)
.map(Response.ResponseBuilder::build)
.orElse(Response.status(Response.Status.NOT_FOUND).build());
.orElseThrow(NotFoundException::new);
}
/**
* 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<Optional<MODEL_OBJECT>> reader, Function<MODEL_OBJECT, MODEL_OBJECT> applyChanges, Predicate<MODEL_OBJECT> hasSameKey) {
Optional<MODEL_OBJECT> existingModelObject = reader.get();
if (!existingModelObject.isPresent()) {
return Response.status(Response.Status.NOT_FOUND).build();
}
MODEL_OBJECT changedModelObject = applyChanges.apply(existingModelObject.get());
public Response update(Supplier<Optional<MODEL_OBJECT>> reader, Function<MODEL_OBJECT, MODEL_OBJECT> applyChanges, Predicate<MODEL_OBJECT> hasSameKey) throws NotFoundException, ConcurrentModificationException {
MODEL_OBJECT existingModelObject = reader.get().orElseThrow(NotFoundException::new);
MODEL_OBJECT changedModelObject = applyChanges.apply(existingModelObject);
if (!hasSameKey.test(changedModelObject)) {
return Response.status(BAD_REQUEST).entity("illegal change of id").build();
}
return update(getId(existingModelObject.get()), changedModelObject);
else if (modelObjectWasModifiedConcurrently(existingModelObject, changedModelObject)) {
throw new ConcurrentModificationException();
}
return update(getId(existingModelObject), changedModelObject);
}
private boolean modelObjectWasModifiedConcurrently(MODEL_OBJECT existing, MODEL_OBJECT updated) {
return existing.getLastModified() != null
&& (updated.getLastModified() == null || existing.getLastModified() > updated.getLastModified());
}
public Response delete(Supplier<Optional<MODEL_OBJECT>> reader) {

View File

@@ -1,25 +1,70 @@
package sonia.scm.api.v2.resources;
import javax.ws.rs.DefaultValue;
import sonia.scm.repository.BrowserResult;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.RepositoryNotFoundException;
import sonia.scm.repository.RevisionNotFoundException;
import sonia.scm.repository.api.BrowseCommandBuilder;
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.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.core.Response;
import java.io.IOException;
public class SourceRootResource {
@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();
private final RepositoryServiceFactory serviceFactory;
private final BrowserResultToBrowserResultDtoMapper browserResultToBrowserResultDtoMapper;
@Inject
public SourceRootResource(RepositoryServiceFactory serviceFactory, BrowserResultToBrowserResultDtoMapper browserResultToBrowserResultDtoMapper) {
this.serviceFactory = serviceFactory;
this.browserResultToBrowserResultDtoMapper = browserResultToBrowserResultDtoMapper;
}
@GET
@Produces(VndMediaType.SOURCE)
@Path("")
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 get() {
throw new UnsupportedOperationException();
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) throws RevisionNotFoundException, RepositoryNotFoundException, IOException {
return getSource(namespace, name, path, 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();
browseCommand.setPath(path);
if (revision != null && !revision.isEmpty()) {
browseCommand.setRevision(revision);
}
BrowserResult browserResult = browseCommand.getBrowserResult();
if (browserResult != null) {
return Response.ok(browserResultToBrowserResultDtoMapper.map(browserResult, namespaceAndName)).build();
} else {
return Response.status(Response.Status.NOT_FOUND).build();
}
}
}
}

View File

@@ -0,0 +1,14 @@
package sonia.scm.api.v2.resources;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
public class SubRepositoryDto {
private String repositoryUrl;
private String browserUrl;
private String revision;
}

View File

@@ -0,0 +1,25 @@
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 UIPluginDto extends HalRepresentation {
private String name;
private Iterable<String> bundles;
public UIPluginDto(String name, Iterable<String> bundles) {
this.name = name;
this.bundles = bundles;
}
@Override
protected HalRepresentation add(Links links) {
return super.add(links);
}
}

View File

@@ -0,0 +1,46 @@
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.plugin.PluginWrapper;
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 UIPluginDtoCollectionMapper {
private final ResourceLinks resourceLinks;
private final UIPluginDtoMapper mapper;
@Inject
public UIPluginDtoCollectionMapper(ResourceLinks resourceLinks, UIPluginDtoMapper mapper) {
this.resourceLinks = resourceLinks;
this.mapper = mapper;
}
public HalRepresentation map(Collection<PluginWrapper> plugins) {
List<UIPluginDto> dtos = plugins.stream().map(mapper::map).collect(toList());
return new HalRepresentation(createLinks(), embedDtos(dtos));
}
private Links createLinks() {
String baseUrl = resourceLinks.uiPluginCollection().self();
Links.Builder linksBuilder = linkingTo()
.with(Links.linkingTo().self(baseUrl).build());
return linksBuilder.build();
}
private Embedded embedDtos(List<UIPluginDto> dtos) {
return embeddedBuilder()
.with("plugins", dtos)
.build();
}
}

View File

@@ -0,0 +1,61 @@
package sonia.scm.api.v2.resources;
import com.google.common.base.Strings;
import de.otto.edison.hal.Links;
import sonia.scm.plugin.PluginWrapper;
import sonia.scm.util.HttpUtil;
import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import java.util.Collections;
import java.util.Set;
import java.util.stream.Collectors;
import static de.otto.edison.hal.Links.linkingTo;
public class UIPluginDtoMapper {
private final ResourceLinks resourceLinks;
private final HttpServletRequest request;
@Inject
public UIPluginDtoMapper(ResourceLinks resourceLinks, HttpServletRequest request) {
this.resourceLinks = resourceLinks;
this.request = request;
}
public UIPluginDto map(PluginWrapper plugin) {
UIPluginDto dto = new UIPluginDto(
plugin.getPlugin().getInformation().getName(),
getScriptResources(plugin)
);
Links.Builder linksBuilder = linkingTo()
.self(resourceLinks.uiPlugin()
.self(plugin.getId()));
dto.add(linksBuilder.build());
return dto;
}
private Set<String> getScriptResources(PluginWrapper wrapper) {
Set<String> scriptResources = wrapper.getPlugin().getResources().getScriptResources();
if (scriptResources != null) {
return scriptResources.stream()
.map(this::addContextPath)
.collect(Collectors.toSet());
}
return Collections.emptySet();
}
private String addContextPath(String resource) {
String ctxPath = request.getContextPath();
if (Strings.isNullOrEmpty(ctxPath)) {
return resource;
}
return HttpUtil.append(ctxPath, resource);
}
}

View File

@@ -0,0 +1,90 @@
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.plugin.PluginLoader;
import sonia.scm.plugin.PluginWrapper;
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.core.Response;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
public class UIPluginResource {
private final PluginLoader pluginLoader;
private final UIPluginDtoCollectionMapper collectionMapper;
private final UIPluginDtoMapper mapper;
@Inject
public UIPluginResource(PluginLoader pluginLoader, UIPluginDtoCollectionMapper collectionMapper, UIPluginDtoMapper mapper) {
this.pluginLoader = pluginLoader;
this.collectionMapper = collectionMapper;
this.mapper = mapper;
}
/**
* Returns a collection of installed plugins and their ui bundles.
*
* @return collection of installed plugins.
*/
@GET
@Path("")
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(CollectionDto.class)
@Produces(VndMediaType.UI_PLUGIN_COLLECTION)
public Response getInstalledPlugins() {
List<PluginWrapper> plugins = pluginLoader.getInstalledPlugins()
.stream()
.filter(this::filter)
.collect(Collectors.toList());
return Response.ok(collectionMapper.map(plugins)).build();
}
/**
* Returns the installed plugin with the given id.
*
* @param id id of plugin
*
* @return installed plugin with specified id
*/
@GET
@Path("{id}")
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 404, condition = "not found"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(UIPluginDto.class)
@Produces(VndMediaType.UI_PLUGIN)
public Response getInstalledPlugin(@PathParam("id") String id) {
Optional<UIPluginDto> uiPluginDto = pluginLoader.getInstalledPlugins()
.stream()
.filter(this::filter)
.filter(plugin -> id.equals(plugin.getId()))
.map(mapper::map)
.findFirst();
if (uiPluginDto.isPresent()) {
return Response.ok(uiPluginDto.get()).build();
} else {
return Response.status(Response.Status.NOT_FOUND).build();
}
}
private boolean filter(PluginWrapper plugin) {
return plugin.getPlugin().getResources() != null;
}
}

View File

@@ -0,0 +1,22 @@
package sonia.scm.api.v2.resources;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.ws.rs.Path;
@Path("v2/ui")
public class UIRootResource {
private Provider<UIPluginResource> uiPluginResourceProvider;
@Inject
public UIRootResource(Provider<UIPluginResource> uiPluginResourceProvider) {
this.uiPluginResourceProvider = uiPluginResourceProvider;
}
@Path("plugins")
public UIPluginResource plugins() {
return uiPluginResourceProvider.get();
}
}

View File

@@ -1,15 +1,25 @@
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.AlreadyExistsException;
import sonia.scm.user.User;
import sonia.scm.user.UserException;
import sonia.scm.user.UserManager;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
import javax.ws.rs.*;
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.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response;
import java.io.IOException;
public class UserCollectionResource {
@@ -18,7 +28,7 @@ public class UserCollectionResource {
private final UserCollectionToDtoMapper userCollectionToDtoMapper;
private final ResourceLinks resourceLinks;
private final IdResourceManagerAdapter<User, UserDto, UserException> adapter;
private final IdResourceManagerAdapter<User, UserDto> adapter;
@Inject
public UserCollectionResource(UserManager manager, UserDtoToUserMapper dtoToUserMapper,
@@ -78,7 +88,7 @@ public class UserCollectionResource {
})
@TypeHint(TypeHint.NO_CONTENT.class)
@ResponseHeaders(@ResponseHeader(name = "Location", description = "uri to the created user"))
public Response create(UserDto userDto) throws IOException, UserException {
public Response create(@Valid UserDto userDto) throws AlreadyExistsException {
return adapter.create(userDto,
() -> dtoToUserMapper.map(userDto, ""),
user -> resourceLinks.user().self(user.getName()));

View File

@@ -6,7 +6,10 @@ import de.otto.edison.hal.Links;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.NotEmpty;
import javax.validation.constraints.Pattern;
import java.time.Instant;
import java.util.Map;
@@ -15,10 +18,13 @@ public class UserDto extends HalRepresentation {
private boolean active;
private boolean admin;
private Instant creationDate;
@NotEmpty
private String displayName;
@JsonInclude(JsonInclude.Include.NON_NULL)
private Instant lastModified;
@NotEmpty @Email
private String mail;
@Pattern(regexp = "^[A-z0-9\\.\\-_@]|[^ ]([A-z0-9\\.\\-_@ ]*[A-z0-9\\.\\-_@]|[^ ])?$")
private String name;
private String password;
private String type;

View File

@@ -9,19 +9,20 @@ import sonia.scm.user.User;
import javax.inject.Inject;
import java.time.Instant;
import static sonia.scm.api.rest.resources.UserResource.DUMMY_PASSWORT;
// Mapstruct does not support parameterized (i.e. non-default) constructors. Thus, we need to use field injection.
@SuppressWarnings("squid:S3306")
@Mapper
public abstract class UserDtoToUserMapper {
public abstract class UserDtoToUserMapper extends BaseDtoMapper {
@Inject
private PasswordService passwordService;
@Mapping(source = "password", target = "password", qualifiedByName = "encrypt")
@Mapping(target = "creationDate", ignore = true)
@Mapping(target = "lastModified", ignore = true)
public abstract User map(UserDto userDto, @Context String originalPassword);
@Named("encrypt")

View File

@@ -3,13 +3,21 @@ 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.ConcurrentModificationException;
import sonia.scm.NotFoundException;
import sonia.scm.user.User;
import sonia.scm.user.UserException;
import sonia.scm.user.UserManager;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
import javax.ws.rs.*;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Response;
public class UserResource {
@@ -17,7 +25,7 @@ public class UserResource {
private final UserDtoToUserMapper dtoToUserMapper;
private final UserToUserDtoMapper userToDtoMapper;
private final IdResourceManagerAdapter<User, UserDto, UserException> adapter;
private final IdResourceManagerAdapter<User, UserDto> adapter;
@Inject
public UserResource(UserDtoToUserMapper dtoToUserMapper, UserToUserDtoMapper userToDtoMapper, UserManager manager) {
@@ -45,7 +53,7 @@ public class UserResource {
@ResponseCode(code = 404, condition = "not found, no user with the specified id/name available"),
@ResponseCode(code = 500, condition = "internal server error")
})
public Response get(@PathParam("id") String id) {
public Response get(@PathParam("id") String id) throws NotFoundException {
return adapter.get(id, userToDtoMapper::map);
}
@@ -90,7 +98,7 @@ public class UserResource {
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(TypeHint.NO_CONTENT.class)
public Response update(@PathParam("id") String name, UserDto userDto) {
public Response update(@PathParam("id") String name, @Valid UserDto userDto) throws NotFoundException, ConcurrentModificationException {
return adapter.update(name, existing -> dtoToUserMapper.map(userDto, existing.getPassword()));
}
}