Merge with 2.0.0-m3

This commit is contained in:
René Pfeuffer
2019-06-13 11:24:32 +02:00
85 changed files with 2644 additions and 1255 deletions

View File

@@ -58,6 +58,8 @@ import sonia.scm.util.IOUtil;
import javax.inject.Inject;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import java.io.Closeable;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Set;
@@ -73,11 +75,11 @@ public class ScmContextListener extends GuiceResteasyBootstrapServletContextList
* the logger for ScmContextListener
*/
private static final Logger LOG = LoggerFactory.getLogger(ScmContextListener.class);
private final ClassLoader parent;
private final Set<PluginWrapper> plugins;
private Injector injector;
public interface Factory {
ScmContextListener create(ClassLoader parent, Set<PluginWrapper> plugins);
}
@@ -99,23 +101,16 @@ public class ScmContextListener extends GuiceResteasyBootstrapServletContextList
super.contextInitialized(servletContextEvent);
afterInjectorCreation(servletContextEvent);
}
private void beforeInjectorCreation() {
}
private boolean hasStartupErrors() {
return SCMContext.getContext().getStartupError() != null;
}
@Override
protected List<? extends Module> getModules(ServletContext context) {
if (hasStartupErrors()) {
return getErrorModules();
}
return getDefaultModules(context);
}
private List<? extends Module> getDefaultModules(ServletContext context) {
DefaultPluginLoader pluginLoader = new DefaultPluginLoader(context, parent, plugins);
ClassOverrides overrides = ClassOverrides.findOverrides(pluginLoader.getUberClassLoader());
@@ -130,15 +125,15 @@ public class ScmContextListener extends GuiceResteasyBootstrapServletContextList
);
appendModules(pluginLoader.getExtensionProcessor(), moduleList);
moduleList.addAll(overrides.getModules());
if (SCMContext.getContext().getStage() == Stage.DEVELOPMENT){
moduleList.add(new DebugModule());
}
moduleList.add(new MapperModule());
return moduleList;
return moduleList;
}
private void appendModules(ExtensionProcessor ep, List<Module> moduleList) {
for (Class<? extends Module> module : ep.byExtensionPoint(Module.class)) {
try {
@@ -149,31 +144,27 @@ public class ScmContextListener extends GuiceResteasyBootstrapServletContextList
}
}
}
private List<? extends Module> getErrorModules() {
return Collections.singletonList(new ScmErrorModule());
}
@Override
protected void withInjector(Injector injector) {
this.injector = injector;
}
private void afterInjectorCreation(ServletContextEvent event) {
if (injector != null && !hasStartupErrors()) {
bindEagerSingletons();
initializeServletContextListeners(event);
}
}
}
private void bindEagerSingletons() {
injector.getInstance(EagerSingletonModule.class).initialize(injector);
}
private void initializeServletContextListeners(ServletContextEvent event) {
injector.getInstance(ServletContextListenerHolder.class).contextInitialized(event);
}
@Override
public void contextDestroyed(ServletContextEvent servletContextEvent)
{
@@ -183,8 +174,20 @@ public class ScmContextListener extends GuiceResteasyBootstrapServletContextList
}
super.contextDestroyed(servletContextEvent);
for (PluginWrapper plugin : getPlugins()) {
ClassLoader pcl = plugin.getClassLoader();
if (pcl instanceof Closeable) {
try {
((Closeable) pcl).close();
} catch (IOException ex) {
LOG.warn("could not close plugin classloader", ex);
}
}
}
}
private void closeCloseables() {
// close Scheduler
IOUtil.close(injector.getInstance(Scheduler.class));
@@ -205,6 +208,4 @@ public class ScmContextListener extends GuiceResteasyBootstrapServletContextList
private void destroyServletContextListeners(ServletContextEvent event) {
injector.getInstance(ServletContextListenerHolder.class).contextDestroyed(event);
}
}

View File

@@ -1,74 +0,0 @@
/**
* 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;
//~--- non-JDK imports --------------------------------------------------------
import com.google.inject.multibindings.Multibinder;
import com.google.inject.servlet.ServletModule;
import sonia.scm.template.ErrorServlet;
import sonia.scm.template.MustacheTemplateEngine;
import sonia.scm.template.TemplateEngine;
import sonia.scm.template.TemplateEngineFactory;
/**
*
* @author Sebastian Sdorra
*/
public class ScmErrorModule extends ServletModule
{
/**
* Method description
*
*/
@Override
protected void configureServlets()
{
SCMContextProvider context = SCMContext.getContext();
bind(SCMContextProvider.class).toInstance(context);
Multibinder<TemplateEngine> engineBinder =
Multibinder.newSetBinder(binder(), TemplateEngine.class);
engineBinder.addBinding().to(MustacheTemplateEngine.class);
bind(TemplateEngine.class).annotatedWith(Default.class).to(
MustacheTemplateEngine.class);
bind(TemplateEngineFactory.class);
serve(ScmServletModule.PATTERN_ALL).with(ErrorServlet.class);
}
}

View File

@@ -37,8 +37,6 @@ 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.RecreateEventBusEvent;
import sonia.scm.event.ScmEventBus;
@@ -104,11 +102,8 @@ public class BootstrapContextFilter extends GuiceFilter
initGuice();
if (SCMContext.getContext().getStage() == Stage.DEVELOPMENT)
{
logger.info("register for restart events");
ScmEventBus.getInstance().register(this);
}
logger.info("register for restart events");
ScmEventBus.getInstance().register(this);
}
public void initGuice() throws ServletException {

View File

@@ -44,6 +44,7 @@ import sonia.scm.SCMContext;
import sonia.scm.ScmContextListener;
import sonia.scm.ScmEventBusModule;
import sonia.scm.ScmInitializerModule;
import sonia.scm.migration.UpdateException;
import sonia.scm.plugin.DefaultPluginLoader;
import sonia.scm.plugin.Plugin;
import sonia.scm.plugin.PluginException;
@@ -52,6 +53,7 @@ import sonia.scm.plugin.PluginLoader;
import sonia.scm.plugin.PluginWrapper;
import sonia.scm.plugin.PluginsInternal;
import sonia.scm.plugin.SmpArchive;
import sonia.scm.update.MigrationWizardContextListener;
import sonia.scm.update.UpdateEngine;
import sonia.scm.util.ClassLoaders;
import sonia.scm.util.IOUtil;
@@ -59,13 +61,13 @@ import sonia.scm.util.IOUtil;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.http.HttpServletResponse;
import javax.xml.bind.DataBindingException;
import javax.xml.bind.JAXB;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
@@ -108,18 +110,6 @@ public class BootstrapContextListener implements ServletContextListener {
public void contextDestroyed(ServletContextEvent sce) {
contextListener.contextDestroyed(sce);
for (PluginWrapper plugin : contextListener.getPlugins()) {
ClassLoader pcl = plugin.getClassLoader();
if (pcl instanceof Closeable) {
try {
((Closeable) pcl).close();
} catch (IOException ex) {
logger.warn("could not close plugin classloader", ex);
}
}
}
context = null;
contextListener = null;
}
@@ -134,35 +124,79 @@ public class BootstrapContextListener implements ServletContextListener {
public void contextInitialized(ServletContextEvent sce) {
context = sce.getServletContext();
File pluginDirectory = getPluginDirectory();
createContextListener(pluginDirectory);
createContextListener();
contextListener.contextInitialized(sce);
}
private void createContextListener(File pluginDirectory) {
private void createContextListener() {
Throwable startupError = SCMContext.getContext().getStartupError();
if (startupError != null) {
contextListener = SingleView.error(startupError);
} else if (Versions.isTooOld()) {
contextListener = SingleView.view("/templates/too-old.mustache", HttpServletResponse.SC_CONFLICT);
} else {
createMigrationOrNormalContextListener();
Versions.writeNew();
}
}
private void createMigrationOrNormalContextListener() {
ClassLoader cl;
Set<PluginWrapper> plugins;
PluginLoader pluginLoader;
try {
File pluginDirectory = getPluginDirectory();
renameOldPluginsFolder(pluginDirectory);
if (!isCorePluginExtractionDisabled()) {
extractCorePlugins(context, pluginDirectory);
} else {
logger.info("core plugin extraction is disabled");
}
ClassLoader cl = ClassLoaders.getContextClassLoader(BootstrapContextListener.class);
cl = ClassLoaders.getContextClassLoader(BootstrapContextListener.class);
Set<PluginWrapper> plugins = PluginsInternal.collectPlugins(cl, pluginDirectory.toPath());
plugins = PluginsInternal.collectPlugins(cl, pluginDirectory.toPath());
PluginLoader pluginLoader = new DefaultPluginLoader(context, cl, plugins);
pluginLoader = new DefaultPluginLoader(context, cl, plugins);
Injector bootstrapInjector = createBootstrapInjector(pluginLoader);
processUpdates(pluginLoader, bootstrapInjector);
contextListener = bootstrapInjector.getInstance(ScmContextListener.Factory.class).create(cl, plugins);
} catch (IOException ex) {
throw new PluginLoadException("could not load plugins", ex);
}
Injector bootstrapInjector = createBootstrapInjector(pluginLoader);
startEitherMigrationOrNormalServlet(cl, plugins, pluginLoader, bootstrapInjector);
}
private void startEitherMigrationOrNormalServlet(ClassLoader cl, Set<PluginWrapper> plugins, PluginLoader pluginLoader, Injector bootstrapInjector) {
MigrationWizardContextListener wizardContextListener = prepareWizardIfNeeded(bootstrapInjector);
if (wizardContextListener.wizardNecessary()) {
contextListener = wizardContextListener;
} else {
processUpdates(pluginLoader, bootstrapInjector);
contextListener = bootstrapInjector.getInstance(ScmContextListener.Factory.class).create(cl, plugins);
}
}
private void renameOldPluginsFolder(File pluginDirectory) {
if (new File(pluginDirectory, "classpath.xml").exists()) {
File backupDirectory = new File(pluginDirectory.getParentFile(), "plugins.v1");
boolean renamed = pluginDirectory.renameTo(backupDirectory);
if (renamed) {
logger.warn("moved old plugins directory to {}", backupDirectory);
} else {
throw new UpdateException("could not rename existing v1 plugin directory");
}
}
}
private MigrationWizardContextListener prepareWizardIfNeeded(Injector bootstrapInjector) {
return new MigrationWizardContextListener(bootstrapInjector);
}
private Injector createBootstrapInjector(PluginLoader pluginLoader) {
@@ -393,7 +427,7 @@ public class BootstrapContextListener implements ServletContextListener {
private ServletContext context;
/** Field description */
private ScmContextListener contextListener;
private ServletContextListener contextListener;
private static class ScmContextListenerModule extends AbstractModule {
@Override

View File

@@ -0,0 +1,119 @@
package sonia.scm.boot;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableMap;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.multibindings.Multibinder;
import com.google.inject.servlet.GuiceServletContextListener;
import com.google.inject.servlet.ServletModule;
import sonia.scm.Default;
import sonia.scm.SCMContext;
import sonia.scm.SCMContextProvider;
import sonia.scm.template.MustacheTemplateEngine;
import sonia.scm.template.TemplateEngine;
import sonia.scm.template.TemplateEngineFactory;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextListener;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
final class SingleView {
private SingleView() {
}
static ServletContextListener error(Throwable throwable) {
String error = Throwables.getStackTraceAsString(throwable);
ViewController controller = new SimpleViewController("/templates/error.mustache", request -> {
Object model = ImmutableMap.of(
"contextPath", request.getContextPath(),
"error", error
);
return new View(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, model);
});
return new SingleViewContextListener(controller);
}
static ServletContextListener view(String template, int sc) {
ViewController controller = new SimpleViewController(template, request -> {
Object model = ImmutableMap.of(
"contextPath", request.getContextPath()
);
return new View(sc, model);
});
return new SingleViewContextListener(controller);
}
private static class SingleViewContextListener extends GuiceServletContextListener {
private final ViewController controller;
private SingleViewContextListener(ViewController controller) {
this.controller = controller;
}
@Override
protected Injector getInjector() {
return Guice.createInjector(new SingleViewModule(controller));
}
}
private static class SingleViewModule extends ServletModule {
private final ViewController viewController;
private SingleViewModule(ViewController viewController) {
this.viewController = viewController;
}
@Override
protected void configureServlets() {
SCMContextProvider context = SCMContext.getContext();
bind(SCMContextProvider.class).toInstance(context);
bind(ViewController.class).toInstance(viewController);
Multibinder<TemplateEngine> engineBinder =
Multibinder.newSetBinder(binder(), TemplateEngine.class);
engineBinder.addBinding().to(MustacheTemplateEngine.class);
bind(TemplateEngine.class).annotatedWith(Default.class).to(
MustacheTemplateEngine.class);
bind(TemplateEngineFactory.class);
bind(ServletContext.class).annotatedWith(Default.class).toInstance(getServletContext());
serve("/images/*", "/styles/*", "/favicon.ico").with(StaticResourceServlet.class);
serve("/*").with(SingleViewServlet.class);
}
}
private static class SimpleViewController implements ViewController {
private final String template;
private final SimpleViewFactory viewFactory;
private SimpleViewController(String template, SimpleViewFactory viewFactory) {
this.template = template;
this.viewFactory = viewFactory;
}
@Override
public String getTemplate() {
return template;
}
@Override
public View createView(HttpServletRequest request) {
return viewFactory.create(request);
}
}
@FunctionalInterface
interface SimpleViewFactory {
View create(HttpServletRequest request);
}
}

View File

@@ -0,0 +1,63 @@
package sonia.scm.boot;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.template.Template;
import sonia.scm.template.TemplateEngine;
import sonia.scm.template.TemplateEngineFactory;
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
@Singleton
public class SingleViewServlet extends HttpServlet {
private static final Logger LOG = LoggerFactory.getLogger(SingleViewServlet.class);
private final Template template;
private final ViewController controller;
@Inject
public SingleViewServlet(TemplateEngineFactory templateEngineFactory, ViewController controller) {
template = createTemplate(templateEngineFactory, controller.getTemplate());
this.controller = controller;
}
private Template createTemplate(TemplateEngineFactory templateEngineFactory, String template) {
TemplateEngine engine = templateEngineFactory.getEngineByExtension(template);
try {
return engine.getTemplate(template);
} catch (IOException e) {
throw new IllegalStateException("failed to parse template: " + template, e);
}
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
process(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
process(req, resp);
}
private void process(HttpServletRequest request, HttpServletResponse response) {
View view = controller.createView(request);
response.setStatus(view.getStatusCode());
response.setContentType("text/html");
response.setCharacterEncoding("UTF-8");
try (PrintWriter writer = response.getWriter()) {
template.execute(writer, view.getModel());
} catch (IOException ex) {
LOG.error("failed to write view", ex);
}
}
}

View File

@@ -0,0 +1,39 @@
package sonia.scm.boot;
import com.github.sdorra.webresources.CacheControl;
import com.github.sdorra.webresources.WebResourceSender;
import com.google.common.annotations.VisibleForTesting;
import sonia.scm.util.HttpUtil;
import javax.inject.Singleton;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
@Singleton
public class StaticResourceServlet extends HttpServlet {
private final WebResourceSender sender = WebResourceSender.create()
.withGZIP()
.withGZIPMinLength(512)
.withBufferSize(16384)
.withCacheControl(CacheControl.create().noCache());
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
URL resource = createResourceUrlFromRequest(request);
if (resource != null) {
sender.resource(resource).get(request, response);
} else {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
}
}
private URL createResourceUrlFromRequest(HttpServletRequest request) throws MalformedURLException {
String uri = HttpUtil.getStrippedURI(request);
return request.getServletContext().getResource(uri);
}
}

View File

@@ -0,0 +1,77 @@
package sonia.scm.boot;
import com.google.common.annotations.VisibleForTesting;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.SCMContext;
import sonia.scm.SCMContextProvider;
import sonia.scm.util.IOUtil;
import sonia.scm.version.Version;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Optional;
class Versions {
private static final Logger LOG = LoggerFactory.getLogger(Versions.class);
private static final Version MIN_VERSION = Version.parse("1.60");
private final SCMContextProvider contextProvider;
@VisibleForTesting
Versions(SCMContextProvider contextProvider) {
this.contextProvider = contextProvider;
}
@VisibleForTesting
boolean isPreviousVersionTooOld() {
return readVersion().map(v -> v.isOlder(MIN_VERSION)).orElse(false);
}
@VisibleForTesting
void writeNewVersion() {
Path config = contextProvider.resolve(Paths.get("config"));
IOUtil.mkdirs(config.toFile());
String version = contextProvider.getVersion();
LOG.debug("write new version {} to file", version);
Path versionFile = config.resolve("version.txt");
try {
Files.write(versionFile, version.getBytes());
} catch (IOException e) {
throw new IllegalStateException("failed to write version file", e);
}
}
private Optional<Version> readVersion() {
Path versionFile = contextProvider.resolve(Paths.get("config", "version.txt"));
if (versionFile.toFile().exists()) {
return Optional.of(readVersionFromFile(versionFile));
}
return Optional.empty();
}
private Version readVersionFromFile(Path versionFile) {
try {
String versionString = new String(Files.readAllBytes(versionFile), StandardCharsets.UTF_8).trim();
LOG.debug("read previous version {} from file", versionString);
return Version.parse(versionString);
} catch (IOException e) {
throw new IllegalStateException("failed to read version file", e);
}
}
static boolean isTooOld() {
return new Versions(SCMContext.getContext()).isPreviousVersionTooOld();
}
static void writeNew() {
new Versions(SCMContext.getContext()).writeNewVersion();
}
}

View File

@@ -0,0 +1,20 @@
package sonia.scm.boot;
class View {
private final int statusCode;
private final Object model;
View(int statusCode, Object model) {
this.statusCode = statusCode;
this.model = model;
}
int getStatusCode() {
return statusCode;
}
Object getModel() {
return model;
}
}

View File

@@ -0,0 +1,11 @@
package sonia.scm.boot;
import javax.servlet.http.HttpServletRequest;
public interface ViewController {
String getTemplate();
View createView(HttpServletRequest request);
}

View File

@@ -25,7 +25,7 @@ import java.util.Set;
import static java.util.Collections.unmodifiableCollection;
import static java.util.stream.Collectors.toList;
class SystemRepositoryPermissionProvider {
public class SystemRepositoryPermissionProvider {
private static final Logger logger = LoggerFactory.getLogger(SystemRepositoryPermissionProvider.class);
private static final String REPOSITORY_PERMISSION_DESCRIPTOR = "META-INF/scm/repository-permissions.xml";

View File

@@ -1,191 +0,0 @@
/**
* 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.template;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.base.Throwables;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.SCMContextProvider;
import sonia.scm.util.IOUtil;
import sonia.scm.util.Util;
//~--- JDK imports ------------------------------------------------------------
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
*
* @author Sebastian Sdorra
*/
@Singleton
public class ErrorServlet extends HttpServlet
{
/** Field description */
private static final String TEMPALTE = "/error.mustache";
/** Field description */
private static final long serialVersionUID = -3289076078469757874L;
/**
* the logger for ErrorServlet
*/
private static final Logger logger =
LoggerFactory.getLogger(ErrorServlet.class);
//~--- constructors ---------------------------------------------------------
/**
* Constructs ...
*
*
* @param context
* @param templateEngineFactory
*/
@Inject
public ErrorServlet(SCMContextProvider context,
TemplateEngineFactory templateEngineFactory)
{
this.context = context;
this.templateEngineFactory = templateEngineFactory;
}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param request
* @param response
*
* @throws IOException
* @throws ServletException
*/
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException
{
processRequest(request, response);
}
/**
* Method description
*
*
* @param request
* @param response
*
* @throws IOException
* @throws ServletException
*/
@Override
protected void doPost(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException
{
processRequest(request, response);
}
/**
* Method description
*
*
* @param request
* @param response
*
* @throws IOException
* @throws ServletException
*/
private void processRequest(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException
{
PrintWriter writer = null;
try
{
writer = response.getWriter();
Map<String, Object> env = new HashMap<String, Object>();
String error = Util.EMPTY_STRING;
if (context.getStartupError() != null)
{
error = Throwables.getStackTraceAsString(context.getStartupError());
}
env.put("error", error);
TemplateEngine engine = templateEngineFactory.getDefaultEngine();
Template template = engine.getTemplate(TEMPALTE);
if (template != null)
{
template.execute(writer, env);
}
else if (logger.isWarnEnabled())
{
logger.warn("could not find template {}", TEMPALTE);
}
}
finally
{
IOUtil.close(writer);
}
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private final SCMContextProvider context;
/** Field description */
private final TemplateEngineFactory templateEngineFactory;
}

View File

@@ -37,27 +37,22 @@ package sonia.scm.template;
import com.github.mustachejava.Mustache;
import com.github.mustachejava.MustacheException;
import com.google.common.base.Throwables;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.google.common.util.concurrent.UncheckedExecutionException;
import com.google.inject.Inject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.Default;
import sonia.scm.plugin.PluginLoader;
//~--- JDK imports ------------------------------------------------------------
import javax.servlet.ServletContext;
import java.io.IOException;
import java.io.Reader;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import javax.servlet.ServletContext;
//~--- JDK imports ------------------------------------------------------------
/**
*
@@ -67,6 +62,14 @@ import javax.servlet.ServletContext;
public class MustacheTemplateEngine implements TemplateEngine
{
/**
* Used to implement optional injection for the PluginLoader.
* @see <a href="https://github.com/google/guice/wiki/FrequentlyAskedQuestions#how-can-i-inject-optional-parameters-into-a-constructor">Optional Injection</a>
*/
static class PluginLoaderHolder {
@Inject(optional = true) PluginLoader pluginLoader;
}
/** Field description */
public static final TemplateType TYPE = new TemplateType("mustache",
"Mustache", "mustache");
@@ -87,13 +90,12 @@ public class MustacheTemplateEngine implements TemplateEngine
*
*
* @param context
* @param pluginLoader
* @param pluginLoaderHolder
*/
@Inject
public MustacheTemplateEngine(@Default ServletContext context,
PluginLoader pluginLoader)
public MustacheTemplateEngine(@Default ServletContext context, PluginLoaderHolder pluginLoaderHolder)
{
factory = new ServletMustacheFactory(context, pluginLoader);
factory = new ServletMustacheFactory(context, createClassLoader(pluginLoaderHolder.pluginLoader));
ThreadFactory threadFactory =
new ThreadFactoryBuilder().setNameFormat(THREAD_NAME).build();
@@ -101,6 +103,13 @@ public class MustacheTemplateEngine implements TemplateEngine
factory.setExecutorService(Executors.newCachedThreadPool(threadFactory));
}
private ClassLoader createClassLoader(PluginLoader pluginLoader) {
if (pluginLoader == null) {
return Thread.currentThread().getContextClassLoader();
}
return pluginLoader.getUberClassLoader();
}
//~--- get methods ----------------------------------------------------------
/**
@@ -112,12 +121,9 @@ public class MustacheTemplateEngine implements TemplateEngine
*
* @return
*
* @throws IOException
*/
@Override
public Template getTemplate(String templateIdentifier, Reader reader)
throws IOException
{
public Template getTemplate(String templateIdentifier, Reader reader) {
if (logger.isTraceEnabled())
{
logger.trace("try to create mustache template from reader with id {}",

View File

@@ -36,22 +36,17 @@ package sonia.scm.template;
//~--- non-JDK imports --------------------------------------------------------
import com.github.mustachejava.DefaultMustacheFactory;
import com.google.common.base.Charsets;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.plugin.PluginLoader;
//~--- JDK imports ------------------------------------------------------------
import javax.servlet.ServletContext;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import javax.servlet.ServletContext;
//~--- JDK imports ------------------------------------------------------------
/**
*
@@ -73,13 +68,12 @@ public class ServletMustacheFactory extends DefaultMustacheFactory
*
*
* @param servletContext
* @param pluginLoader
* @param classLoader
*/
public ServletMustacheFactory(ServletContext servletContext,
PluginLoader pluginLoader)
public ServletMustacheFactory(ServletContext servletContext, ClassLoader classLoader)
{
this.servletContext = servletContext;
this.pluginLoader = pluginLoader;
this.classLoader = classLoader;
}
//~--- get methods ----------------------------------------------------------
@@ -116,7 +110,7 @@ public class ServletMustacheFactory extends DefaultMustacheFactory
resourceName = resourceName.substring(1);
}
is = pluginLoader.getUberClassLoader().getResourceAsStream(resourceName);
is = classLoader.getResourceAsStream(resourceName);
}
if (is != null)
@@ -144,9 +138,8 @@ public class ServletMustacheFactory extends DefaultMustacheFactory
//~--- fields ---------------------------------------------------------------
/** Field description */
private final PluginLoader pluginLoader;
/** Field description */
private ServletContext servletContext;
private ClassLoader classLoader;
}

View File

@@ -0,0 +1,23 @@
package sonia.scm.update;
import com.google.inject.Injector;
import com.google.inject.servlet.GuiceServletContextListener;
import sonia.scm.update.repository.XmlRepositoryV1UpdateStep;
public class MigrationWizardContextListener extends GuiceServletContextListener {
private final Injector bootstrapInjector;
public MigrationWizardContextListener(Injector bootstrapInjector) {
this.bootstrapInjector = bootstrapInjector;
}
public boolean wizardNecessary() {
return !bootstrapInjector.getInstance(XmlRepositoryV1UpdateStep.class).getRepositoriesWithoutMigrationStrategies().isEmpty();
}
@Override
protected Injector getInjector() {
return bootstrapInjector.createChildInjector(new MigrationWizardModule());
}
}

View File

@@ -0,0 +1,26 @@
package sonia.scm.update;
import com.google.inject.servlet.ServletModule;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.PushStateDispatcher;
import sonia.scm.WebResourceServlet;
class MigrationWizardModule extends ServletModule {
private static final Logger LOG = LoggerFactory.getLogger(MigrationWizardModule.class);
@Override
protected void configureServlets() {
LOG.info("==========================================================");
LOG.info("= =");
LOG.info("= STARTING MIGRATION SERVLET =");
LOG.info("= =");
LOG.info("= Open SCM-Manager in a browser to start the wizard. =");
LOG.info("= =");
LOG.info("==========================================================");
bind(PushStateDispatcher.class).toInstance((request, response, uri) -> {});
serve("/images/*", "/styles/*", "/favicon.ico").with(WebResourceServlet.class);
serve("/*").with(MigrationWizardServlet.class);
}
}

View File

@@ -0,0 +1,265 @@
package sonia.scm.update;
import com.github.mustachejava.DefaultMustacheFactory;
import com.github.mustachejava.Mustache;
import com.github.mustachejava.MustacheFactory;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.boot.RestartEvent;
import sonia.scm.event.ScmEventBus;
import sonia.scm.update.repository.MigrationStrategy;
import sonia.scm.update.repository.MigrationStrategyDao;
import sonia.scm.update.repository.V1Repository;
import sonia.scm.update.repository.XmlRepositoryV1UpdateStep;
import sonia.scm.util.ValidationUtil;
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static java.util.Comparator.comparing;
@Singleton
class MigrationWizardServlet extends HttpServlet {
private static final Logger LOG = LoggerFactory.getLogger(MigrationWizardServlet.class);
private final XmlRepositoryV1UpdateStep repositoryV1UpdateStep;
private final MigrationStrategyDao migrationStrategyDao;
@Inject
MigrationWizardServlet(XmlRepositoryV1UpdateStep repositoryV1UpdateStep, MigrationStrategyDao migrationStrategyDao) {
this.repositoryV1UpdateStep = repositoryV1UpdateStep;
this.migrationStrategyDao = migrationStrategyDao;
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
List<RepositoryLineEntry> repositoryLineEntries = getRepositoryLineEntries();
doGet(req, resp, repositoryLineEntries);
}
private void doGet(HttpServletRequest req, HttpServletResponse resp, List<RepositoryLineEntry> repositoryLineEntries) {
HashMap<String, Object> model = new HashMap<>();
model.put("contextPath", req.getContextPath());
model.put("submitUrl", req.getRequestURI());
model.put("repositories", repositoryLineEntries);
model.put("strategies", getMigrationStrategies());
model.put("validationErrorsFound", repositoryLineEntries
.stream()
.anyMatch(entry -> entry.isNamespaceInvalid() || entry.isNameInvalid()));
respondWithTemplate(resp, model, "templates/repository-migration.mustache");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
List<RepositoryLineEntry> repositoryLineEntries = getRepositoryLineEntries();
boolean validationErrorFound = false;
for (RepositoryLineEntry repositoryLineEntry : repositoryLineEntries) {
String id = repositoryLineEntry.getId();
String strategy = req.getParameter("strategy-" + id);
if (!Strings.isNullOrEmpty(strategy)) {
repositoryLineEntry.setSelectedStrategy(MigrationStrategy.valueOf(strategy));
}
String namespace = req.getParameter("namespace-" + id);
repositoryLineEntry.setNamespace(namespace);
String name = req.getParameter("name-" + id);
repositoryLineEntry.setName(name);
if (!ValidationUtil.isRepositoryNameValid(namespace)) {
repositoryLineEntry.setNamespaceValid(false);
validationErrorFound = true;
}
if (!ValidationUtil.isRepositoryNameValid(name)) {
repositoryLineEntry.setNameValid(false);
validationErrorFound = true;
}
}
if (validationErrorFound) {
doGet(req, resp, repositoryLineEntries);
return;
}
repositoryLineEntries.stream()
.map(RepositoryLineEntry::getId)
.forEach(
id -> {
String strategy = req.getParameter("strategy-" + id);
String namespace = req.getParameter("namespace-" + id);
String name = req.getParameter("name-" + id);
migrationStrategyDao.set(id, MigrationStrategy.valueOf(strategy), namespace, name);
}
);
Map<String, Object> model = Collections.singletonMap("contextPath", req.getContextPath());
respondWithTemplate(resp, model, "templates/repository-migration-restart.mustache");
ScmEventBus.getInstance().post(new RestartEvent(MigrationWizardServlet.class, "wrote migration data"));
}
private List<RepositoryLineEntry> getRepositoryLineEntries() {
List<V1Repository> repositoriesWithoutMigrationStrategies =
repositoryV1UpdateStep.getRepositoriesWithoutMigrationStrategies();
return repositoriesWithoutMigrationStrategies.stream()
.map(RepositoryLineEntry::new)
.sorted(comparing(RepositoryLineEntry::getPath))
.collect(Collectors.toList());
}
private MigrationStrategy[] getMigrationStrategies() {
return MigrationStrategy.values();
}
@VisibleForTesting
void respondWithTemplate(HttpServletResponse resp, Map<String, Object> model, String templateName) {
MustacheFactory mf = new DefaultMustacheFactory();
Mustache template = mf.compile(templateName);
PrintWriter writer;
try {
writer = resp.getWriter();
} catch (IOException e) {
LOG.error("could not create writer for response", e);
resp.setStatus(500);
return;
}
template.execute(writer, model);
writer.flush();
resp.setStatus(200);
}
private static class RepositoryLineEntry {
private final String id;
private final String type;
private final String path;
private MigrationStrategy selectedStrategy;
private String namespace;
private String name;
private boolean namespaceValid = true;
private boolean nameValid = true;
public RepositoryLineEntry(V1Repository repository) {
this.id = repository.getId();
this.type = repository.getType();
this.path = repository.getType() + "/" + repository.getName();
this.selectedStrategy = MigrationStrategy.COPY;
this.namespace = computeNewNamespace(repository);
this.name = computeNewName(repository);
}
private static String computeNewNamespace(V1Repository v1Repository) {
String[] nameParts = getNameParts(v1Repository.getName());
return nameParts.length > 1 ? nameParts[0] : v1Repository.getType();
}
private static String computeNewName(V1Repository v1Repository) {
String[] nameParts = getNameParts(v1Repository.getName());
return nameParts.length == 1 ? nameParts[0] : concatPathElements(nameParts);
}
private static String[] getNameParts(String v1Name) {
return v1Name.split("/");
}
private static String concatPathElements(String[] nameParts) {
return Arrays.stream(nameParts).skip(1).collect(Collectors.joining("_"));
}
public String getId() {
return id;
}
public String getType() {
return type;
}
public String getPath() {
return path;
}
public String getNamespace() {
return namespace;
}
public String getName() {
return name;
}
public MigrationStrategy getSelectedStrategy() {
return selectedStrategy;
}
public List<RepositoryLineMigrationStrategy> getStrategies() {
return Arrays.stream(MigrationStrategy.values())
.map(s -> new RepositoryLineMigrationStrategy(s.name(), selectedStrategy == s))
.collect(Collectors.toList());
}
public void setNamespace(String namespace) {
this.namespace = namespace;
}
public void setName(String name) {
this.name = name;
}
public void setNamespaceValid(boolean namespaceValid) {
this.namespaceValid = namespaceValid;
}
public void setNameValid(boolean nameValid) {
this.nameValid = nameValid;
}
public void setSelectedStrategy(MigrationStrategy selectedStrategy) {
this.selectedStrategy = selectedStrategy;
}
public boolean isNamespaceInvalid() {
return !namespaceValid;
}
public boolean isNameInvalid() {
return !nameValid;
}
}
private static class RepositoryLineMigrationStrategy {
private final String name;
private final boolean selected;
private RepositoryLineMigrationStrategy(String name, boolean selected) {
this.name = name;
this.selected = selected;
}
public String getName() {
return name;
}
public boolean isSelected() {
return selected;
}
}
}

View File

@@ -1,5 +1,7 @@
package sonia.scm.update.repository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.SCMContextProvider;
import sonia.scm.repository.RepositoryDirectoryHandler;
import sonia.scm.repository.RepositoryLocationResolver;
@@ -7,9 +9,14 @@ import sonia.scm.repository.RepositoryLocationResolver;
import javax.inject.Inject;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;
import static java.util.Optional.of;
class CopyMigrationStrategy extends BaseMigrationStrategy {
private static final Logger LOG = LoggerFactory.getLogger(CopyMigrationStrategy.class);
private final RepositoryLocationResolver locationResolver;
@Inject
@@ -19,13 +26,14 @@ class CopyMigrationStrategy extends BaseMigrationStrategy {
}
@Override
public Path migrate(String id, String name, String type) {
Path repositoryBasePath = locationResolver.forClass(Path.class).getLocation(id);
public Optional<Path> migrate(String id, String name, String type) {
Path repositoryBasePath = locationResolver.forClass(Path.class).createLocation(id);
Path targetDataPath = repositoryBasePath
.resolve(RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY);
Path sourceDataPath = getSourceDataPath(name, type);
LOG.info("copying repository data from {} to {}", sourceDataPath, targetDataPath);
copyData(sourceDataPath, targetDataPath);
return repositoryBasePath;
return of(repositoryBasePath);
}
private void copyData(Path sourceDirectory, Path targetDirectory) {

View File

@@ -0,0 +1,32 @@
package sonia.scm.update.repository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.SCMContextProvider;
import sonia.scm.util.IOUtil;
import javax.inject.Inject;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Optional;
public class DeleteMigrationStrategy extends BaseMigrationStrategy {
private static final Logger LOG = LoggerFactory.getLogger(DeleteMigrationStrategy.class);
@Inject
DeleteMigrationStrategy(SCMContextProvider contextProvider) {
super(contextProvider);
}
@Override
public Optional<Path> migrate(String id, String name, String type) {
Path sourceDataPath = getSourceDataPath(name, type);
try {
IOUtil.delete(sourceDataPath.toFile(), true);
} catch (IOException e) {
LOG.warn("could not delete old repository path for repository {} with type {} and id {}", name, type, id);
}
return Optional.empty();
}
}

View File

@@ -0,0 +1,13 @@
package sonia.scm.update.repository;
import java.nio.file.Path;
import java.util.Optional;
import static java.util.Optional.empty;
public class IgnoreMigrationStrategy implements MigrationStrategy.Instance {
@Override
public Optional<Path> migrate(String id, String name, String type) {
return empty();
}
}

View File

@@ -1,29 +1,47 @@
package sonia.scm.update.repository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.SCMContextProvider;
import sonia.scm.repository.RepositoryDirectoryHandler;
import sonia.scm.repository.RepositoryLocationResolver;
import javax.inject.Inject;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;
import static java.util.Optional.of;
class InlineMigrationStrategy extends BaseMigrationStrategy {
private static final Logger LOG = LoggerFactory.getLogger(InlineMigrationStrategy.class);
private final RepositoryLocationResolver locationResolver;
@Inject
public InlineMigrationStrategy(SCMContextProvider contextProvider) {
public InlineMigrationStrategy(SCMContextProvider contextProvider, RepositoryLocationResolver locationResolver) {
super(contextProvider);
this.locationResolver = locationResolver;
}
@Override
public Path migrate(String id, String name, String type) {
public Optional<Path> migrate(String id, String name, String type) {
Path repositoryBasePath = getSourceDataPath(name, type);
locationResolver.forClass(Path.class).setLocation(id, repositoryBasePath);
Path targetDataPath = repositoryBasePath
.resolve(RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY);
LOG.info("moving repository data from {} to {}", repositoryBasePath, targetDataPath);
moveData(repositoryBasePath, targetDataPath);
return repositoryBasePath;
return of(repositoryBasePath);
}
private void moveData(Path sourceDirectory, Path targetDirectory) {
moveData(sourceDirectory, targetDirectory, false);
}
private void moveData(Path sourceDirectory, Path targetDirectory, boolean deleteDirectory) {
createDataDirectory(targetDirectory);
listSourceDirectory(sourceDirectory)
.filter(sourceFile -> !targetDirectory.equals(sourceFile))
@@ -31,11 +49,18 @@ class InlineMigrationStrategy extends BaseMigrationStrategy {
sourceFile -> {
Path targetFile = targetDirectory.resolve(sourceFile.getFileName());
if (Files.isDirectory(sourceFile)) {
moveData(sourceFile, targetFile);
moveData(sourceFile, targetFile, true);
} else {
moveFile(sourceFile, targetFile);
}
}
);
if (deleteDirectory) {
try {
Files.delete(sourceDirectory);
} catch (IOException e) {
LOG.warn("could not delete source repository directory {}", sourceDirectory);
}
}
}
}

View File

@@ -0,0 +1,158 @@
package sonia.scm.update.repository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.migration.UpdateException;
import sonia.scm.migration.UpdateStep;
import sonia.scm.plugin.Extension;
import sonia.scm.repository.HealthCheckFailure;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryPermission;
import sonia.scm.repository.RepositoryRole;
import sonia.scm.repository.xml.SingleRepositoryUpdateProcessor;
import sonia.scm.security.SystemRepositoryPermissionProvider;
import sonia.scm.version.Version;
import javax.inject.Inject;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlRootElement;
import java.nio.file.Path;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
@Extension
public class MigrateVerbsToPermissionRoles implements UpdateStep {
public static final Logger LOG = LoggerFactory.getLogger(MigrateVerbsToPermissionRoles.class);
private final SingleRepositoryUpdateProcessor updateProcessor;
private final SystemRepositoryPermissionProvider systemRepositoryPermissionProvider;
private final JAXBContext jaxbContextNewRepository;
private final JAXBContext jaxbContextOldRepository;
@Inject
public MigrateVerbsToPermissionRoles(SingleRepositoryUpdateProcessor updateProcessor, SystemRepositoryPermissionProvider systemRepositoryPermissionProvider) {
this.updateProcessor = updateProcessor;
this.systemRepositoryPermissionProvider = systemRepositoryPermissionProvider;
jaxbContextNewRepository = createJAXBContext(Repository.class);
jaxbContextOldRepository = createJAXBContext(OldRepository.class);
}
@Override
public void doUpdate() {
updateProcessor.doUpdate(this::update);
}
void update(String repositoryId, Path path) {
LOG.info("updating repository {}", repositoryId);
OldRepository oldRepository = readOldRepository(path);
Repository newRepository = createNewRepository(oldRepository);
writeNewRepository(path, newRepository);
}
private void writeNewRepository(Path path, Repository newRepository) {
try {
Marshaller marshaller = jaxbContextNewRepository.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
marshaller.marshal(newRepository, path.resolve("metadata.xml").toFile());
} catch (JAXBException e) {
throw new UpdateException("could not read old repository structure", e);
}
}
private OldRepository readOldRepository(Path path) {
try {
return (OldRepository) jaxbContextOldRepository.createUnmarshaller().unmarshal(path.resolve("metadata.xml").toFile());
} catch (JAXBException e) {
throw new UpdateException("could not read old repository structure", e);
}
}
private Repository createNewRepository(OldRepository oldRepository) {
Repository repository = new Repository(
oldRepository.id,
oldRepository.type,
oldRepository.namespace,
oldRepository.name,
oldRepository.contact,
oldRepository.description,
oldRepository.permissions.stream().map(this::updatePermission).toArray(RepositoryPermission[]::new)
);
repository.setCreationDate(oldRepository.creationDate);
repository.setHealthCheckFailures(oldRepository.healthCheckFailures);
repository.setLastModified(oldRepository.lastModified);
repository.setPublicReadable(oldRepository.publicReadable);
repository.setArchived(oldRepository.archived);
return repository;
}
private RepositoryPermission updatePermission(RepositoryPermission repositoryPermission) {
return findMatchingRole(repositoryPermission.getVerbs())
.map(roleName -> copyRepositoryPermissionWithRole(repositoryPermission, roleName))
.orElse(repositoryPermission);
}
private RepositoryPermission copyRepositoryPermissionWithRole(RepositoryPermission repositoryPermission, String roleName) {
return new RepositoryPermission(repositoryPermission.getName(), roleName, repositoryPermission.isGroupPermission());
}
private Optional<String> findMatchingRole(Collection<String> verbs) {
return systemRepositoryPermissionProvider.availableRoles()
.stream()
.filter(r -> roleMatchesVerbs(verbs, r))
.map(RepositoryRole::getName)
.findFirst();
}
private boolean roleMatchesVerbs(Collection<String> verbs, RepositoryRole r) {
return verbs.size() == r.getVerbs().size() && r.getVerbs().containsAll(verbs);
}
private JAXBContext createJAXBContext(Class<?> clazz) {
try {
return JAXBContext.newInstance(clazz);
} catch (JAXBException e) {
throw new UpdateException("could not create XML marshaller", e);
}
}
@Override
public Version getTargetVersion() {
return Version.parse("2.0.2");
}
@Override
public String getAffectedDataType() {
return "sonia.scm.repository.xml";
}
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "repositories")
private static class OldRepository {
private String contact;
private Long creationDate;
private String description;
@XmlElement(name = "healthCheckFailure")
@XmlElementWrapper(name = "healthCheckFailures")
private List<HealthCheckFailure> healthCheckFailures;
private String id;
private Long lastModified;
private String namespace;
private String name;
@XmlElement(name = "permission")
private final Set<RepositoryPermission> permissions = new HashSet<>();
@XmlElement(name = "public")
private boolean publicReadable = false;
private boolean archived = false;
private String type;
}
}

View File

@@ -3,17 +3,41 @@ package sonia.scm.update.repository;
import com.google.inject.Injector;
import java.nio.file.Path;
import java.util.Optional;
enum MigrationStrategy {
public enum MigrationStrategy {
COPY(CopyMigrationStrategy.class),
MOVE(MoveMigrationStrategy.class),
INLINE(InlineMigrationStrategy.class);
COPY(CopyMigrationStrategy.class,
"Copy the repository data files to the new native location inside SCM-Manager home directory. " +
"This will keep the original directory."),
MOVE(MoveMigrationStrategy.class,
"Move the repository data files to the new native location inside SCM-Manager home directory. " +
"The original directory will be deleted."),
INLINE(InlineMigrationStrategy.class,
"Use the current directory where the repository data files are stored, but modify the directory " +
"structure so that it can be used for SCM-Manager v2. The repository data files will be moved to a new " +
"subdirectory 'data' inside the current directory."),
IGNORE(IgnoreMigrationStrategy.class,
"The repository will not be migrated and will not be visible inside SCM-Manager. " +
"The data files will be kept at the current location."),
DELETE(DeleteMigrationStrategy.class,
"The repository will not be migrated and will not be visible inside SCM-Manager. " +
"The data files will be deleted!");
private Class<? extends Instance> implementationClass;
private final Class<? extends Instance> implementationClass;
private final String description;
MigrationStrategy(Class<? extends Instance> implementationClass) {
MigrationStrategy(Class<? extends Instance> implementationClass, String description) {
this.implementationClass = implementationClass;
this.description = description;
}
public Class<? extends Instance> getImplementationClass() {
return implementationClass;
}
public String getDescription() {
return description;
}
Instance from(Injector injector) {
@@ -21,6 +45,6 @@ enum MigrationStrategy {
}
interface Instance {
Path migrate(String id, String name, String type);
Optional<Path> migrate(String id, String name, String type);
}
}

View File

@@ -4,8 +4,10 @@ import sonia.scm.store.ConfigurationStore;
import sonia.scm.store.ConfigurationStoreFactory;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.Optional;
@Singleton
public class MigrationStrategyDao {
private final RepositoryMigrationPlan plan;
@@ -17,12 +19,12 @@ public class MigrationStrategyDao {
this.plan = store.getOptional().orElse(new RepositoryMigrationPlan());
}
public Optional<MigrationStrategy> get(String id) {
public Optional<RepositoryMigrationPlan.RepositoryMigrationEntry> get(String id) {
return plan.get(id);
}
public void set(String repositoryId, MigrationStrategy strategy) {
plan.set(repositoryId, strategy);
public void set(String repositoryId, MigrationStrategy strategy, String newNamespace, String newName) {
plan.set(repositoryId, strategy, newNamespace, newName);
store.set(plan);
}
}

View File

@@ -11,8 +11,10 @@ import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Optional;
import static java.util.Arrays.asList;
import static java.util.Optional.of;
class MoveMigrationStrategy extends BaseMigrationStrategy {
@@ -27,14 +29,15 @@ class MoveMigrationStrategy extends BaseMigrationStrategy {
}
@Override
public Path migrate(String id, String name, String type) {
Path repositoryBasePath = locationResolver.forClass(Path.class).getLocation(id);
public Optional<Path> migrate(String id, String name, String type) {
Path repositoryBasePath = locationResolver.forClass(Path.class).createLocation(id);
Path targetDataPath = repositoryBasePath
.resolve(RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY);
Path sourceDataPath = getSourceDataPath(name, type);
LOG.info("moving repository data from {} to {}", sourceDataPath, targetDataPath);
moveData(sourceDataPath, targetDataPath);
deleteOldDataDir(getTypeDependentPath(type), name);
return repositoryBasePath;
return of(repositoryBasePath);
}
private void deleteOldDataDir(Path rootPath, String name) {

View File

@@ -13,57 +13,74 @@ import static java.util.Arrays.asList;
@XmlRootElement(name = "repository-migration")
class RepositoryMigrationPlan {
private List<RepositoryEntry> entries;
private List<RepositoryMigrationEntry> entries;
RepositoryMigrationPlan() {
this(new RepositoryEntry[0]);
this(new RepositoryMigrationEntry[0]);
}
RepositoryMigrationPlan(RepositoryEntry... entries) {
RepositoryMigrationPlan(RepositoryMigrationEntry... entries) {
this.entries = new ArrayList<>(asList(entries));
}
Optional<MigrationStrategy> get(String repositoryId) {
return findEntry(repositoryId)
.map(RepositoryEntry::getDataMigrationStrategy);
}
public void set(String repositoryId, MigrationStrategy strategy) {
Optional<RepositoryEntry> entry = findEntry(repositoryId);
if (entry.isPresent()) {
entry.get().setStrategy(strategy);
} else {
entries.add(new RepositoryEntry(repositoryId, strategy));
}
}
private Optional<RepositoryEntry> findEntry(String repositoryId) {
Optional<RepositoryMigrationEntry> get(String repositoryId) {
return entries.stream()
.filter(repositoryEntry -> repositoryId.equals(repositoryEntry.repositoryId))
.findFirst();
}
public void set(String repositoryId, MigrationStrategy strategy, String newNamespace, String newName) {
Optional<RepositoryMigrationEntry> entry = get(repositoryId);
if (entry.isPresent()) {
entry.get().setStrategy(strategy);
entry.get().setNewNamespace(newNamespace);
entry.get().setNewName(newName);
} else {
entries.add(new RepositoryMigrationEntry(repositoryId, strategy, newNamespace, newName));
}
}
@XmlRootElement(name = "entries")
@XmlAccessorType(XmlAccessType.FIELD)
static class RepositoryEntry {
static class RepositoryMigrationEntry {
private String repositoryId;
private MigrationStrategy dataMigrationStrategy;
private String newNamespace;
private String newName;
RepositoryEntry() {
RepositoryMigrationEntry() {
}
RepositoryEntry(String repositoryId, MigrationStrategy dataMigrationStrategy) {
RepositoryMigrationEntry(String repositoryId, MigrationStrategy dataMigrationStrategy, String newNamespace, String newName) {
this.repositoryId = repositoryId;
this.dataMigrationStrategy = dataMigrationStrategy;
this.newNamespace = newNamespace;
this.newName = newName;
}
public MigrationStrategy getDataMigrationStrategy() {
return dataMigrationStrategy;
}
public String getNewNamespace() {
return newNamespace;
}
public String getNewName() {
return newName;
}
private void setStrategy(MigrationStrategy strategy) {
this.dataMigrationStrategy = strategy;
}
private void setNewNamespace(String newNamespace) {
this.newNamespace = newNamespace;
}
private void setNewName(String newName) {
this.newName = newName;
}
}
}

View File

@@ -0,0 +1,25 @@
package sonia.scm.update.repository;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "permissions")
class V1Permission {
private boolean groupPermission;
private String name;
private String type;
public boolean isGroupPermission() {
return groupPermission;
}
public String getName() {
return name;
}
public String getType() {
return type;
}
}

View File

@@ -0,0 +1,92 @@
package sonia.scm.update.repository;
import sonia.scm.update.properties.V1Properties;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import java.util.List;
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "repositories")
public class V1Repository {
private String contact;
private long creationDate;
private Long lastModified;
private String description;
private String id;
private String name;
private boolean isPublic;
private boolean archived;
private String type;
private List<V1Permission> permissions;
private V1Properties properties;
public V1Repository() {
}
public V1Repository(String id, String type, String name) {
this.id = id;
this.type = type;
this.name = name;
}
public String getId() {
return id;
}
public String getName() {
return name;
}
public String getType() {
return type;
}
public String getContact() {
return contact;
}
public long getCreationDate() {
return creationDate;
}
public Long getLastModified() {
return lastModified;
}
public String getDescription() {
return description;
}
public boolean isPublic() {
return isPublic;
}
public boolean isArchived() {
return archived;
}
public List<V1Permission> getPermissions() {
return permissions;
}
public V1Properties getProperties() {
return properties;
}
@Override
public String toString() {
return "V1Repository{" +
", contact='" + contact + '\'' +
", creationDate=" + creationDate +
", lastModified=" + lastModified +
", description='" + description + '\'' +
", id='" + id + '\'' +
", name='" + name + '\'' +
", isPublic=" + isPublic +
", archived=" + archived +
", type='" + type + '\'' +
'}';
}
}

View File

@@ -6,6 +6,7 @@ import sonia.scm.SCMContextProvider;
import sonia.scm.migration.UpdateStep;
import sonia.scm.plugin.Extension;
import sonia.scm.repository.xml.PathBasedRepositoryLocationResolver;
import sonia.scm.repository.xml.XmlRepositoryDAO;
import sonia.scm.store.StoreConstants;
import sonia.scm.version.Version;
@@ -27,10 +28,12 @@ public class XmlRepositoryFileNameUpdateStep implements UpdateStep {
private static final Logger LOG = LoggerFactory.getLogger(XmlRepositoryFileNameUpdateStep.class);
private final SCMContextProvider contextProvider;
private final XmlRepositoryDAO repositoryDAO;
@Inject
public XmlRepositoryFileNameUpdateStep(SCMContextProvider contextProvider) {
public XmlRepositoryFileNameUpdateStep(SCMContextProvider contextProvider, XmlRepositoryDAO repositoryDAO) {
this.contextProvider = contextProvider;
this.repositoryDAO = repositoryDAO;
}
@Override
@@ -41,6 +44,7 @@ public class XmlRepositoryFileNameUpdateStep implements UpdateStep {
if (Files.exists(oldRepositoriesFile)) {
LOG.info("moving old repositories database files to repository-paths file");
Files.move(oldRepositoriesFile, newRepositoryPathsFile);
repositoryDAO.refresh();
}
}

View File

@@ -28,11 +28,12 @@ import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static java.util.Collections.emptyList;
import static java.util.Optional.empty;
import static java.util.Optional.of;
import static sonia.scm.version.Version.parse;
@@ -102,13 +103,30 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep {
JAXBContext jaxbContext = JAXBContext.newInstance(V1RepositoryDatabase.class);
readV1Database(jaxbContext).ifPresent(
v1Database -> {
v1Database.repositoryList.repositories.forEach(this::readMigrationStrategy);
v1Database.repositoryList.repositories.forEach(this::readMigrationEntry);
v1Database.repositoryList.repositories.forEach(this::update);
backupOldRepositoriesFile();
}
);
}
public List<V1Repository> getRepositoriesWithoutMigrationStrategies() {
if (!resolveV1File().exists()) {
LOG.info("no v1 repositories database file found");
return emptyList();
}
try {
JAXBContext jaxbContext = JAXBContext.newInstance(XmlRepositoryV1UpdateStep.V1RepositoryDatabase.class);
return readV1Database(jaxbContext)
.map(v1Database -> v1Database.repositoryList.repositories.stream())
.orElse(Stream.empty())
.filter(v1Repository -> !this.findMigrationStrategy(v1Repository).isPresent())
.collect(Collectors.toList());
} catch (JAXBException e) {
throw new UpdateException("could not read v1 repository database", e);
}
}
private void backupOldRepositoriesFile() {
Path configDir = contextProvider.getBaseDirectory().toPath().resolve(StoreConstants.CONFIG_DIRECTORY_NAME);
Path oldRepositoriesFile = configDir.resolve("repositories.xml");
@@ -122,61 +140,59 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep {
}
private void update(V1Repository v1Repository) {
Path destination = handleDataDirectory(v1Repository);
Repository repository = new Repository(
v1Repository.id,
v1Repository.type,
getNamespace(v1Repository),
getName(v1Repository),
v1Repository.contact,
v1Repository.description,
createPermissions(v1Repository));
LOG.info("creating new repository {} with id {} from old repository {} in directory {}", repository.getNamespaceAndName(), repository.getId(), v1Repository.name, destination);
repositoryDao.add(repository, destination);
propertyStore.put(v1Repository.id, v1Repository.properties);
RepositoryMigrationPlan.RepositoryMigrationEntry repositoryMigrationEntry = readMigrationEntry(v1Repository);
Optional<Path> destination = handleDataDirectory(v1Repository, repositoryMigrationEntry.getDataMigrationStrategy());
LOG.info("using strategy {} to migrate repository {} with id {} using new namespace {} and name {}",
repositoryMigrationEntry.getDataMigrationStrategy().getClass(),
v1Repository.getName(),
v1Repository.getId(),
repositoryMigrationEntry.getNewNamespace(),
repositoryMigrationEntry.getNewName());
destination.ifPresent(
newPath -> {
Repository repository = new Repository(
v1Repository.getId(),
v1Repository.getType(),
repositoryMigrationEntry.getNewNamespace(),
repositoryMigrationEntry.getNewName(),
v1Repository.getContact(),
v1Repository.getDescription(),
createPermissions(v1Repository));
LOG.info("creating new repository {} with id {} from old repository {} in directory {}", repository.getNamespaceAndName(), repository.getId(), v1Repository.getName(), newPath);
repositoryDao.add(repository, newPath);
propertyStore.put(v1Repository.getId(), v1Repository.getProperties());
}
);
}
private Path handleDataDirectory(V1Repository v1Repository) {
MigrationStrategy dataMigrationStrategy = readMigrationStrategy(v1Repository);
return dataMigrationStrategy.from(injector).migrate(v1Repository.id, v1Repository.name, v1Repository.type);
private Optional<Path> handleDataDirectory(V1Repository v1Repository, MigrationStrategy dataMigrationStrategy) {
return dataMigrationStrategy
.from(injector)
.migrate(v1Repository.getId(), v1Repository.getName(), v1Repository.getType());
}
private MigrationStrategy readMigrationStrategy(V1Repository v1Repository) {
return migrationStrategyDao.get(v1Repository.id)
.orElseThrow(() -> new IllegalStateException("no strategy found for repository with id " + v1Repository.id + " and name " + v1Repository.name));
private RepositoryMigrationPlan.RepositoryMigrationEntry readMigrationEntry(V1Repository v1Repository) {
return findMigrationStrategy(v1Repository)
.orElseThrow(() -> new IllegalStateException("no strategy found for repository with id " + v1Repository.getId() + " and name " + v1Repository.getName()));
}
private Optional<RepositoryMigrationPlan.RepositoryMigrationEntry> findMigrationStrategy(V1Repository v1Repository) {
return migrationStrategyDao.get(v1Repository.getId());
}
private RepositoryPermission[] createPermissions(V1Repository v1Repository) {
if (v1Repository.permissions == null) {
if (v1Repository.getPermissions() == null) {
return new RepositoryPermission[0];
}
return v1Repository.permissions
return v1Repository.getPermissions()
.stream()
.map(this::createPermission)
.toArray(RepositoryPermission[]::new);
}
private RepositoryPermission createPermission(V1Permission v1Permission) {
LOG.info("creating permission {} for {}", v1Permission.type, v1Permission.name);
return new RepositoryPermission(v1Permission.name, v1Permission.type, v1Permission.groupPermission);
}
private String getNamespace(V1Repository v1Repository) {
String[] nameParts = getNameParts(v1Repository.name);
return nameParts.length > 1 ? nameParts[0] : v1Repository.type;
}
private String getName(V1Repository v1Repository) {
String[] nameParts = getNameParts(v1Repository.name);
return nameParts.length == 1 ? nameParts[0] : concatPathElements(nameParts);
}
private String concatPathElements(String[] nameParts) {
return Arrays.stream(nameParts).skip(1).collect(Collectors.joining("_"));
}
private String[] getNameParts(String v1Name) {
return v1Name.split("/");
LOG.info("creating permission {} for {}", v1Permission.getType(), v1Permission.getName());
return new RepositoryPermission(v1Permission.getName(), v1Permission.getType(), v1Permission.isGroupPermission());
}
private Optional<V1RepositoryDatabase> readV1Database(JAXBContext jaxbContext) throws JAXBException {
@@ -195,45 +211,6 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep {
).toFile();
}
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "permissions")
private static class V1Permission {
private boolean groupPermission;
private String name;
private String type;
}
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "repositories")
private static class V1Repository {
private String contact;
private long creationDate;
private Long lastModified;
private String description;
private String id;
private String name;
private boolean isPublic;
private boolean archived;
private String type;
private List<V1Permission> permissions;
private V1Properties properties;
@Override
public String toString() {
return "V1Repository{" +
", contact='" + contact + '\'' +
", creationDate=" + creationDate +
", lastModified=" + lastModified +
", description='" + description + '\'' +
", id='" + id + '\'' +
", name='" + name + '\'' +
", isPublic=" + isPublic +
", archived=" + archived +
", type='" + type + '\'' +
'}';
}
}
private static class RepositoryList {
@XmlElement(name = "repository")
private List<V1Repository> repositories;