This commit is contained in:
René Pfeuffer
2018-11-06 09:26:41 +01:00
119 changed files with 8479 additions and 5472 deletions

View File

@@ -0,0 +1,20 @@
package sonia.scm;
import com.google.common.collect.ImmutableMap;
import com.google.inject.servlet.ServletModule;
import org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher;
import org.jboss.resteasy.plugins.server.servlet.ResteasyContextParameters;
import javax.inject.Singleton;
import java.util.Map;
public class ResteasyModule extends ServletModule {
@Override
protected void configureServlets() {
bind(HttpServletDispatcher.class).in(Singleton.class);
Map<String, String> initParams = ImmutableMap.of(ResteasyContextParameters.RESTEASY_SERVLET_MAPPING_PREFIX, "/api");
serve("/api/*").with(HttpServletDispatcher.class, initParams);
}
}

View File

@@ -126,6 +126,7 @@ public class ScmContextListener extends GuiceResteasyBootstrapServletContextList
ClassOverrides overrides = ClassOverrides.findOverrides(pluginLoader.getUberClassLoader());
List<Module> moduleList = Lists.newArrayList();
moduleList.add(new ResteasyModule());
moduleList.add(new ScmInitializerModule());
moduleList.add(new ScmEventBusModule());
moduleList.add(new EagerSingletonModule());

View File

@@ -57,5 +57,9 @@ public class TemplatingPushStateDispatcher implements PushStateDispatcher {
return request.getContextPath();
}
public String getLiveReloadURL() {
return System.getProperty("livereload.url");
}
}
}

View File

@@ -0,0 +1,43 @@
package sonia.scm.api;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import sonia.scm.api.v2.resources.ErrorDto;
import sonia.scm.api.v2.resources.ExceptionWithContextToErrorDtoMapper;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
import java.util.Collections;
@Provider
public class FallbackExceptionMapper implements ExceptionMapper<Exception> {
private static final Logger logger = LoggerFactory.getLogger(FallbackExceptionMapper.class);
private static final String ERROR_CODE = "CmR8GCJb31";
private final ExceptionWithContextToErrorDtoMapper mapper;
@Inject
public FallbackExceptionMapper(ExceptionWithContextToErrorDtoMapper mapper) {
this.mapper = mapper;
}
@Override
public Response toResponse(Exception exception) {
logger.debug("map {} to status code 500", exception);
ErrorDto errorDto = new ErrorDto();
errorDto.setMessage("internal server error");
errorDto.setContext(Collections.emptyList());
errorDto.setErrorCode(ERROR_CODE);
errorDto.setTransactionId(MDC.get("transaction_id"));
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(errorDto)
.type(VndMediaType.ERROR_TYPE)
.build();
}
}

View File

@@ -13,7 +13,7 @@ public class ContextualExceptionMapper<E extends ExceptionWithContext> implement
private static final Logger logger = LoggerFactory.getLogger(ContextualExceptionMapper.class);
private ExceptionWithContextToErrorDtoMapper mapper;
private final ExceptionWithContextToErrorDtoMapper mapper;
private final Response.Status status;
private final Class<E> type;

View File

@@ -0,0 +1,17 @@
package sonia.scm.api.v2;
import sonia.scm.NotSupportedFeatureException;
import sonia.scm.api.rest.ContextualExceptionMapper;
import sonia.scm.api.v2.resources.ExceptionWithContextToErrorDtoMapper;
import javax.inject.Inject;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.Provider;
@Provider
public class NotSupportedFeatureExceptionMapper extends ContextualExceptionMapper<NotSupportedFeatureException> {
@Inject
public NotSupportedFeatureExceptionMapper(ExceptionWithContextToErrorDtoMapper mapper) {
super(NotSupportedFeatureException.class, Response.Status.BAD_REQUEST, mapper);
}
}

View File

@@ -23,6 +23,7 @@ import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.util.Optional;
@Slf4j
@@ -94,8 +95,12 @@ public class ChangesetRootResource {
.setStartChangeset(id)
.setEndChangeset(id)
.getChangesets();
if (changesets != null && changesets.getChangesets() != null && changesets.getChangesets().size() == 1) {
return Response.ok(changesetToChangesetDtoMapper.map(changesets.getChangesets().get(0), repository)).build();
if (changesets != null && changesets.getChangesets() != null && !changesets.getChangesets().isEmpty()) {
Optional<Changeset> changeset = changesets.getChangesets().stream().filter(ch -> ch.getId().equals(id)).findFirst();
if (!changeset.isPresent()) {
return Response.status(Response.Status.NOT_FOUND).build();
}
return Response.ok(changesetToChangesetDtoMapper.map(changeset.get(), repository)).build();
} else {
return Response.status(Response.Status.NOT_FOUND).build();
}

View File

@@ -122,7 +122,7 @@ public class ContentResource {
private void appendContentHeader(String path, byte[] head, Response.ResponseBuilder responseBuilder) {
ContentType contentType = ContentTypes.detect(path, head);
responseBuilder.header("Content-Type", contentType.getRaw());
contentType.getLanguage().ifPresent(language -> responseBuilder.header("Language", language));
contentType.getLanguage().ifPresent(language -> responseBuilder.header("X-Programming-Language", language));
}
private byte[] getHead(String revision, String path, RepositoryService repositoryService) throws IOException {

View File

@@ -4,22 +4,29 @@ import com.webcohesion.enunciate.metadata.rs.ResponseCode;
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import sonia.scm.NotFoundException;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.api.DiffFormat;
import sonia.scm.repository.api.RepositoryService;
import sonia.scm.repository.api.RepositoryServiceFactory;
import sonia.scm.util.HttpUtil;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
import javax.validation.constraints.Pattern;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.StreamingOutput;
public class DiffRootResource {
public static final String HEADER_CONTENT_DISPOSITION = "Content-Disposition";
private static final String DIFF_FORMAT_VALUES_REGEX = "NATIVE|GIT|UNIFIED";
private final RepositoryServiceFactory serviceFactory;
@Inject
@@ -48,12 +55,14 @@ public class DiffRootResource {
@ResponseCode(code = 404, condition = "not found, no revision with the specified param for the repository available or repository not found"),
@ResponseCode(code = 500, condition = "internal server error")
})
public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision){
public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision , @Pattern(regexp = DIFF_FORMAT_VALUES_REGEX) @DefaultValue("NATIVE") @QueryParam("format") String format ){
HttpUtil.checkForCRLFInjection(revision);
DiffFormat diffFormat = DiffFormat.valueOf(format);
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
StreamingOutput responseEntry = output -> {
repositoryService.getDiffCommand()
.setRevision(revision)
.setFormat(diffFormat)
.retriveContent(output);
};
return Response.ok(responseEntry)

View File

@@ -17,7 +17,7 @@ public class ErrorDto {
private List<ContextEntry> context;
private String message;
@XmlElement(name = "violation")
@JsonInclude(JsonInclude.Include.NON_NULL)
@XmlElementWrapper(name = "violations")
private List<ConstraintViolationDto> violations;

View File

@@ -34,22 +34,19 @@ package sonia.scm.boot;
//~--- non-JDK imports --------------------------------------------------------
import com.github.legman.Subscribe;
import com.google.inject.servlet.GuiceFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.SCMContext;
import sonia.scm.Stage;
import sonia.scm.event.ScmEventBus;
//~--- JDK imports ------------------------------------------------------------
import javax.servlet.FilterConfig;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletException;
//~--- JDK imports ------------------------------------------------------------
/**
*
* @author Sebastian Sdorra
@@ -65,6 +62,8 @@ public class BootstrapContextFilter extends GuiceFilter
//~--- methods --------------------------------------------------------------
private final BootstrapContextListener listener = new BootstrapContextListener();
/**
* Restart the whole webapp context.
*
@@ -85,29 +84,20 @@ public class BootstrapContextFilter extends GuiceFilter
}
else
{
logger.warn(
"destroy filter pipeline, because of a received restart event");
logger.warn("destroy filter pipeline, because of a received restart event");
destroy();
logger.warn(
"reinitialize filter pipeline, because of a received restart event");
super.init(filterConfig);
logger.warn("reinitialize filter pipeline, because of a received restart event");
initGuice();
}
}
/**
* Method description
*
*
* @param filterConfig
*
* @throws ServletException
*/
@Override
public void init(FilterConfig filterConfig) throws ServletException
{
this.filterConfig = filterConfig;
super.init(filterConfig);
initGuice();
if (SCMContext.getContext().getStage() == Stage.DEVELOPMENT)
{
@@ -116,6 +106,19 @@ public class BootstrapContextFilter extends GuiceFilter
}
}
public void initGuice() throws ServletException {
super.init(filterConfig);
listener.contextInitialized(new ServletContextEvent(filterConfig.getServletContext()));
}
@Override
public void destroy() {
super.destroy();
listener.contextDestroyed(new ServletContextEvent(filterConfig.getServletContext()));
ServletContextCleaner.cleanup(filterConfig.getServletContext());
}
//~--- fields ---------------------------------------------------------------
/** Field description */

View File

@@ -148,13 +148,15 @@ public class BootstrapContextListener implements ServletContextListener
{
context = sce.getServletContext();
PluginIndex index = readCorePluginIndex(context);
File pluginDirectory = getPluginDirectory();
try
{
extractCorePlugins(context, pluginDirectory, index);
if (!isCorePluginExtractionDisabled()) {
extractCorePlugins(context, pluginDirectory);
} else {
logger.info("core plugin extraction is disabled");
}
ClassLoader cl =
ClassLoaders.getContextClassLoader(BootstrapContextListener.class);
@@ -181,31 +183,8 @@ public class BootstrapContextListener implements ServletContextListener
}
}
/**
* Restart the whole webapp context.
*
*
* @param event restart event
*/
@Subscribe
public void handleRestartEvent(RestartEvent event)
{
logger.warn("received restart event from {} with reason: {}",
event.getCause(), event.getReason());
if (context == null)
{
logger.error("context is null, scm-manager is not initialized");
}
else
{
ServletContextEvent sce = new ServletContextEvent(context);
logger.warn("destroy context, because of a received restart event");
contextDestroyed(sce);
logger.warn("reinitialize context, because of a received restart event");
contextInitialized(sce);
}
private boolean isCorePluginExtractionDisabled() {
return Boolean.getBoolean("sonia.scm.boot.disable-core-plugin-extraction");
}
/**
@@ -214,7 +193,6 @@ public class BootstrapContextListener implements ServletContextListener
*
* @param context
* @param pluginDirectory
* @param name
* @param entry
*
* @throws IOException
@@ -269,17 +247,15 @@ public class BootstrapContextListener implements ServletContextListener
*
* @param context
* @param pluginDirectory
* @param lines
* @param index
*
* @throws IOException
*/
private void extractCorePlugins(ServletContext context, File pluginDirectory,
PluginIndex index)
throws IOException
private void extractCorePlugins(ServletContext context, File pluginDirectory) throws IOException
{
IOUtil.mkdirs(pluginDirectory);
PluginIndex index = readCorePluginIndex(context);
for (PluginIndexEntry entry : index)
{
extractCorePlugin(context, pluginDirectory, entry);

View File

@@ -0,0 +1,97 @@
package sonia.scm.boot;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.Priority;
import sonia.scm.SCMContext;
import sonia.scm.Stage;
import sonia.scm.event.ScmEventBus;
import sonia.scm.filter.WebElement;
import javax.inject.Inject;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* This servlet sends a {@link RestartEvent} to the {@link ScmEventBus} which causes scm-manager to restart the context.
* The {@link RestartServlet} can be used for reloading java code or for installing plugins without a complete restart.
* At the moment the Servlet accepts only request, if scm-manager was started in the {@link Stage#DEVELOPMENT} stage.
*
* @since 2.0.0
*/
@Priority(0)
@WebElement("/restart")
public class RestartServlet extends HttpServlet {
private static final Logger LOG = LoggerFactory.getLogger(RestartServlet.class);
private final ObjectMapper objectMapper = new ObjectMapper();
private final AtomicBoolean restarting = new AtomicBoolean();
private final ScmEventBus eventBus;
private final Stage stage;
@Inject
public RestartServlet() {
this(ScmEventBus.getInstance(), SCMContext.getContext().getStage());
}
RestartServlet(ScmEventBus eventBus, Stage stage) {
this.eventBus = eventBus;
this.stage = stage;
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
LOG.info("received sendRestartEvent request");
if (isRestartAllowed()) {
try (InputStream requestInput = req.getInputStream()) {
Reason reason = objectMapper.readValue(requestInput, Reason.class);
sendRestartEvent(resp, reason);
} catch (IOException ex) {
LOG.warn("failed to trigger sendRestartEvent event", ex);
resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
} else {
LOG.debug("received restart event in non development stage");
resp.setStatus(HttpServletResponse.SC_NOT_ACCEPTABLE);
}
}
private boolean isRestartAllowed() {
return stage == Stage.DEVELOPMENT;
}
private void sendRestartEvent(HttpServletResponse response, Reason reason) {
if ( restarting.compareAndSet(false, true) ) {
LOG.info("trigger sendRestartEvent, because of {}", reason.getMessage());
eventBus.post(new RestartEvent(RestartServlet.class, reason.getMessage()));
response.setStatus(HttpServletResponse.SC_ACCEPTED);
} else {
LOG.warn("scm-manager restarts already");
response.setStatus(HttpServletResponse.SC_CONFLICT);
}
}
public static class Reason {
private String message;
public void setMessage(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
}
}

View File

@@ -0,0 +1,59 @@
package sonia.scm.boot;
import com.google.common.collect.ImmutableSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.ServletContext;
import java.util.Enumeration;
import java.util.Set;
/**
* Remove cached resources from {@link ServletContext} to allow a clean restart of scm-manager without stale or
* duplicated data.
*
* @since 2.0.0
*/
final class ServletContextCleaner {
private static final Logger LOG = LoggerFactory.getLogger(ServletContextCleaner.class);
private static final Set<String> REMOVE_PREFIX = ImmutableSet.of(
"org.jboss.resteasy",
"resteasy",
"org.apache.shiro",
"sonia.scm"
);
private ServletContextCleaner() {
}
/**
* Remove cached attributes from {@link ServletContext}.
*
* @param servletContext servlet context
*/
static void cleanup(ServletContext servletContext) {
LOG.info("remove cached attributes from context");
Enumeration<String> attributeNames = servletContext.getAttributeNames();
while( attributeNames.hasMoreElements()) {
String name = attributeNames.nextElement();
if (shouldRemove(name)) {
LOG.info("remove attribute {} from servlet context", name);
servletContext.removeAttribute(name);
} else {
LOG.info("keep attribute {} in servlet context", name);
}
}
}
private static boolean shouldRemove(String name) {
for (String prefix : REMOVE_PREFIX) {
if (name.startsWith(prefix)) {
return true;
}
}
return false;
}
}

View File

@@ -41,6 +41,8 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableList.Builder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.SCMContext;
import sonia.scm.Stage;
import javax.servlet.ServletContext;
import java.net.MalformedURLException;
@@ -69,19 +71,21 @@ public class DefaultUberWebResourceLoader implements UberWebResourceLoader
//~--- constructors ---------------------------------------------------------
/**
* Constructs ...
*
*
* @param servletContext
* @param plugins
*/
public DefaultUberWebResourceLoader(ServletContext servletContext,
Iterable<PluginWrapper> plugins)
{
public DefaultUberWebResourceLoader(ServletContext servletContext, Iterable<PluginWrapper> plugins) {
this(servletContext, plugins, SCMContext.getContext().getStage());
}
public DefaultUberWebResourceLoader(ServletContext servletContext, Iterable<PluginWrapper> plugins, Stage stage) {
this.servletContext = servletContext;
this.plugins = plugins;
this.cache = CacheBuilder.newBuilder().build();
this.cache = createCache(stage);
}
private Cache<String, URL> createCache(Stage stage) {
if (stage == Stage.DEVELOPMENT) {
return CacheBuilder.newBuilder().maximumSize(0).build(); // Disable caching
}
return CacheBuilder.newBuilder().build();
}
//~--- get methods ----------------------------------------------------------
@@ -97,7 +101,7 @@ public class DefaultUberWebResourceLoader implements UberWebResourceLoader
@Override
public URL getResource(String path)
{
URL resource = cache.getIfPresent(path);
URL resource = getFromCache(path);
if (resource == null)
{
@@ -105,7 +109,7 @@ public class DefaultUberWebResourceLoader implements UberWebResourceLoader
if (resource != null)
{
cache.put(path, resource);
addToCache(path, resource);
}
}
else
@@ -116,6 +120,14 @@ public class DefaultUberWebResourceLoader implements UberWebResourceLoader
return resource;
}
private URL getFromCache(String path) {
return cache.getIfPresent(path);
}
private void addToCache(String path, URL url) {
cache.put(path, url);
}
/**
* Method description
*