rename package sonia.scm.boot to sonia.scm.lifecycle

This commit is contained in:
Sebastian Sdorra
2019-06-25 08:36:57 +02:00
parent 56c7fcc114
commit 99f1c8c55e
45 changed files with 44 additions and 47 deletions

View File

@@ -0,0 +1,78 @@
package sonia.scm.lifecycle;
import com.google.common.base.Throwables;
import com.google.inject.Module;
import org.apache.shiro.guice.web.ShiroWebModule;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.ClassOverrides;
import sonia.scm.ResteasyModule;
import sonia.scm.SCMContext;
import sonia.scm.ScmSecurityModule;
import sonia.scm.ScmServletModule;
import sonia.scm.Stage;
import sonia.scm.api.v2.resources.MapperModule;
import sonia.scm.debug.DebugModule;
import sonia.scm.filter.WebElementModule;
import sonia.scm.plugin.ExtensionProcessor;
import sonia.scm.plugin.PluginLoader;
import javax.servlet.ServletContext;
import java.util.ArrayList;
import java.util.List;
class ApplicationModuleProvider implements ModuleProvider {
private static final Logger LOG = LoggerFactory.getLogger(ApplicationModuleProvider.class);
private final ServletContext servletContext;
private final PluginLoader pluginLoader;
ApplicationModuleProvider(ServletContext servletContext, PluginLoader pluginLoader) {
this.servletContext = servletContext;
this.pluginLoader = pluginLoader;
}
@Override
public List<Module> createModules() {
ClassOverrides overrides = createClassOverrides();
return createModules(overrides);
}
private List<Module> createModules(ClassOverrides overrides) {
List<Module> moduleList = new ArrayList<>();
moduleList.add(new ResteasyModule());
moduleList.add(ShiroWebModule.guiceFilterModule());
moduleList.add(new WebElementModule(pluginLoader));
moduleList.add(new ScmServletModule(pluginLoader, overrides));
moduleList.add(
new ScmSecurityModule(servletContext, pluginLoader.getExtensionProcessor())
);
appendModules(pluginLoader.getExtensionProcessor(), moduleList);
moduleList.addAll(overrides.getModules());
if (SCMContext.getContext().getStage() == Stage.DEVELOPMENT){
moduleList.add(new DebugModule());
}
moduleList.add(new MapperModule());
return moduleList;
}
private ClassOverrides createClassOverrides() {
ClassLoader uberClassLoader = pluginLoader.getUberClassLoader();
return ClassOverrides.findOverrides(uberClassLoader);
}
private void appendModules(ExtensionProcessor ep, List<Module> moduleList) {
for (Class<? extends Module> module : ep.byExtensionPoint(Module.class)) {
try {
LOG.info("add module {}", module);
moduleList.add(module.newInstance());
} catch (IllegalAccessException | InstantiationException ex) {
throw Throwables.propagate(ex);
}
}
}
}

View File

@@ -0,0 +1,11 @@
package sonia.scm.lifecycle;
/**
* This ClassLoader is mainly a wrapper around the web application class loader and its goal is to make it easier to
* find it in a heap dump.
*/
class BootstrapClassLoader extends ClassLoader {
BootstrapClassLoader(ClassLoader webappClassLoader) {
super(webappClassLoader);
}
}

View File

@@ -0,0 +1,122 @@
/**
* 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.lifecycle;
//~--- 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.event.ScmEventBus;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletException;
//~--- JDK imports ------------------------------------------------------------
/**
*
* @author Sebastian Sdorra
*/
public class BootstrapContextFilter extends GuiceFilter {
/**
* the logger for BootstrapContextFilter
*/
private static final Logger LOG = LoggerFactory.getLogger(BootstrapContextFilter.class);
private final BootstrapContextListener listener = new BootstrapContextListener();
/** Field description */
private FilterConfig filterConfig;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
this.filterConfig = filterConfig;
initializeContext();
}
private void initializeContext() throws ServletException {
super.init(filterConfig);
LOG.info("register for restart events");
ScmEventBus.getInstance().register(this);
listener.contextInitialized(new ServletContextEvent(filterConfig.getServletContext()));
}
@Override
public void destroy() {
super.destroy();
listener.contextDestroyed(new ServletContextEvent(filterConfig.getServletContext()));
}
/**
* Restart SCM-Manager.
*
* @param event restart event
*/
@Subscribe
public void handleRestartEvent(RestartEvent event) {
LOG.warn("received restart event from {} with reason: {}",
event.getCause(), event.getReason());
if (filterConfig == null) {
LOG.error("filter config is null, scm-manager is not initialized");
} else {
RestartStrategy restartStrategy = RestartStrategy.get();
restartStrategy.restart(new GuiceInjectionContext());
}
}
private class GuiceInjectionContext implements RestartStrategy.InjectionContext {
@Override
public void initialize() {
try {
BootstrapContextFilter.this.initializeContext();
} catch (ServletException e) {
throw new IllegalStateException("failed to initialize guice", e);
}
}
@Override
public void destroy() {
BootstrapContextFilter.this.destroy();
}
}
}

View File

@@ -0,0 +1,170 @@
/**
* Copyright (c) 2010, Sebastian Sdorra All rights reserved.
* <p>
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* <p>
* 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.
* <p>
* 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.
* <p>
* http://bitbucket.org/sdorra/scm-manager
*/
package sonia.scm.lifecycle;
import com.google.common.collect.ImmutableList;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Module;
import com.google.inject.servlet.GuiceServletContextListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.CloseableModule;
import sonia.scm.EagerSingletonModule;
import sonia.scm.SCMContext;
import sonia.scm.ScmEventBusModule;
import sonia.scm.ScmInitializerModule;
import sonia.scm.plugin.PluginLoader;
import sonia.scm.update.MigrationWizardModuleProvider;
import sonia.scm.update.UpdateEngine;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.List;
/**
* @author Sebastian Sdorra
*/
public class BootstrapContextListener extends GuiceServletContextListener {
private static final Logger LOG = LoggerFactory.getLogger(BootstrapContextListener.class);
private final ClassLoaderLifeCycle classLoaderLifeCycle = ClassLoaderLifeCycle.create();
private ServletContext context;
private InjectionLifeCycle injectionLifeCycle;
@Override
public void contextInitialized(ServletContextEvent sce) {
LOG.info("start scm-manager initialization");
context = sce.getServletContext();
classLoaderLifeCycle.init();
super.contextInitialized(sce);
Injector injector = (Injector) context.getAttribute(Injector.class.getName());
injectionLifeCycle = new InjectionLifeCycle(injector);
injectionLifeCycle.initialize();
}
@Override
protected Injector getInjector() {
Throwable startupError = SCMContext.getContext().getStartupError();
if (startupError != null) {
return createStageOneInjector(SingleView.error(startupError));
} else if (Versions.isTooOld()) {
return createStageOneInjector(SingleView.view("/templates/too-old.mustache", HttpServletResponse.SC_CONFLICT));
} else {
try {
return createStageTwoInjector();
} catch (Exception ex) {
return createStageOneInjector(SingleView.error(ex));
}
}
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
LOG.info("shutdown scm-manager context");
ServletContextCleaner.cleanup(context);
injectionLifeCycle.shutdown();
injectionLifeCycle = null;
classLoaderLifeCycle.shutdown();
}
private Injector createStageTwoInjector() {
PluginBootstrap pluginBootstrap = new PluginBootstrap(context, classLoaderLifeCycle);
ModuleProvider provider = createMigrationOrNormalModuleProvider(pluginBootstrap);
return createStageTwoInjector(provider, pluginBootstrap.getPluginLoader());
}
private ModuleProvider createMigrationOrNormalModuleProvider(PluginBootstrap pluginBootstrap) {
Injector bootstrapInjector = createBootstrapInjector(pluginBootstrap.getPluginLoader());
return startEitherMigrationOrApplication(pluginBootstrap.getPluginLoader(), bootstrapInjector);
}
private ModuleProvider startEitherMigrationOrApplication(PluginLoader pluginLoader, Injector bootstrapInjector) {
MigrationWizardModuleProvider wizardModuleProvider = new MigrationWizardModuleProvider(bootstrapInjector);
if (wizardModuleProvider.wizardNecessary()) {
return wizardModuleProvider;
} else {
processUpdates(pluginLoader, bootstrapInjector);
Versions.writeNew();
return new ApplicationModuleProvider(context, pluginLoader);
}
}
private Injector createStageOneInjector(ModuleProvider provider) {
return Guice.createInjector(provider.createModules());
}
private Injector createStageTwoInjector(ModuleProvider provider, PluginLoader pluginLoader) {
List<Module> modules = new ArrayList<>(createBootstrapModules(pluginLoader));
modules.addAll(provider.createModules());
return Guice.createInjector(modules);
}
private Injector createBootstrapInjector(PluginLoader pluginLoader) {
return Guice.createInjector(createBootstrapModules(pluginLoader));
}
private List<Module> createBootstrapModules(PluginLoader pluginLoader) {
List<Module> modules = new ArrayList<>(createBaseModules());
modules.add(new BootstrapModule(pluginLoader));
return modules;
}
private List<Module> createBaseModules() {
return ImmutableList.of(
new EagerSingletonModule(),
new ScmInitializerModule(),
new ScmEventBusModule(),
new ServletContextModule(),
new CloseableModule()
);
}
private void processUpdates(PluginLoader pluginLoader, Injector bootstrapInjector) {
Injector updateInjector = bootstrapInjector.createChildInjector(new UpdateStepModule(pluginLoader));
UpdateEngine updateEngine = updateInjector.getInstance(UpdateEngine.class);
updateEngine.update();
}
}

View File

@@ -0,0 +1,86 @@
package sonia.scm.lifecycle;
import com.google.inject.AbstractModule;
import com.google.inject.throwingproviders.ThrowingProviderBinder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.ClassOverrides;
import sonia.scm.SCMContext;
import sonia.scm.SCMContextProvider;
import sonia.scm.io.DefaultFileSystem;
import sonia.scm.io.FileSystem;
import sonia.scm.plugin.PluginLoader;
import sonia.scm.repository.RepositoryLocationResolver;
import sonia.scm.repository.xml.PathBasedRepositoryLocationResolver;
import sonia.scm.security.CipherHandler;
import sonia.scm.security.CipherUtil;
import sonia.scm.security.DefaultKeyGenerator;
import sonia.scm.security.KeyGenerator;
import sonia.scm.store.BlobStoreFactory;
import sonia.scm.store.ConfigurationEntryStoreFactory;
import sonia.scm.store.ConfigurationStoreFactory;
import sonia.scm.store.DataStoreFactory;
import sonia.scm.store.FileBlobStoreFactory;
import sonia.scm.store.JAXBConfigurationEntryStoreFactory;
import sonia.scm.store.JAXBConfigurationStoreFactory;
import sonia.scm.store.JAXBDataStoreFactory;
public class BootstrapModule extends AbstractModule {
private static final Logger LOG = LoggerFactory.getLogger(BootstrapModule.class);
private final ClassOverrides overrides;
private final PluginLoader pluginLoader;
BootstrapModule(PluginLoader pluginLoader) {
this.overrides = ClassOverrides.findOverrides(pluginLoader.getUberClassLoader());
this.pluginLoader = pluginLoader;
}
@Override
protected void configure() {
install(ThrowingProviderBinder.forModule(this));
SCMContextProvider context = SCMContext.getContext();
bind(SCMContextProvider.class).toInstance(context);
bind(KeyGenerator.class).to(DefaultKeyGenerator.class);
bind(RepositoryLocationResolver.class).to(PathBasedRepositoryLocationResolver.class);
bind(FileSystem.class, DefaultFileSystem.class);
// note CipherUtil uses an other generator
bind(CipherHandler.class).toInstance(CipherUtil.getInstance().getCipherHandler());
// bind core
bind(ConfigurationStoreFactory.class, JAXBConfigurationStoreFactory.class);
bind(ConfigurationEntryStoreFactory.class, JAXBConfigurationEntryStoreFactory.class);
bind(DataStoreFactory.class, JAXBDataStoreFactory.class);
bind(BlobStoreFactory.class, FileBlobStoreFactory.class);
bind(PluginLoader.class).toInstance(pluginLoader);
}
private <T> void bind(Class<T> clazz, Class<? extends T> defaultImplementation) {
Class<? extends T> implementation = find(clazz, defaultImplementation);
LOG.debug("bind {} to {}", clazz, implementation);
bind(clazz).to(implementation);
}
private <T> Class<? extends T> find(Class<T> clazz, Class<? extends T> defaultImplementation) {
Class<? extends T> implementation = overrides.getOverride(clazz);
if (implementation != null) {
LOG.info("found override {} for {}", implementation, clazz);
} else {
implementation = defaultImplementation;
LOG.trace(
"no override available for {}, using default implementation {}",
clazz, implementation);
}
return implementation;
}
}

View File

@@ -0,0 +1,126 @@
package sonia.scm.lifecycle;
import com.google.common.annotations.VisibleForTesting;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import se.jiderhamn.classloader.leak.prevention.ClassLoaderLeakPreventor;
import se.jiderhamn.classloader.leak.prevention.ClassLoaderLeakPreventorFactory;
import se.jiderhamn.classloader.leak.prevention.cleanup.MBeanCleanUp;
import sonia.scm.plugin.ChildFirstPluginClassLoader;
import sonia.scm.plugin.DefaultPluginClassLoader;
import java.io.Closeable;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.function.UnaryOperator;
import static com.google.common.base.Preconditions.checkState;
/**
* Creates and shutdown SCM-Manager ClassLoaders.
*/
public final class ClassLoaderLifeCycle {
private static final Logger LOG = LoggerFactory.getLogger(ClassLoaderLifeCycle.class);
private final Deque<ClassLoaderAndPreventor> classLoaders = new ArrayDeque<>();
private final ClassLoaderLeakPreventorFactory classLoaderLeakPreventorFactory;
private final ClassLoader webappClassLoader;
private ClassLoader bootstrapClassLoader;
private UnaryOperator<ClassLoader> classLoaderAppendListener = c -> c;
@VisibleForTesting
public static ClassLoaderLifeCycle create() {
ClassLoaderLeakPreventorFactory classLoaderLeakPreventorFactory = new ClassLoaderLeakPreventorFactory();
classLoaderLeakPreventorFactory.setLogger(new LoggingAdapter());
classLoaderLeakPreventorFactory.removeCleanUp(MBeanCleanUp.class);
return new ClassLoaderLifeCycle(Thread.currentThread().getContextClassLoader(), classLoaderLeakPreventorFactory);
}
ClassLoaderLifeCycle(ClassLoader webappClassLoader, ClassLoaderLeakPreventorFactory classLoaderLeakPreventorFactory) {
this.classLoaderLeakPreventorFactory = classLoaderLeakPreventorFactory;
this.webappClassLoader = initAndAppend(webappClassLoader);
}
void init() {
bootstrapClassLoader = initAndAppend(new BootstrapClassLoader(webappClassLoader));
}
@VisibleForTesting
void setClassLoaderAppendListener(UnaryOperator<ClassLoader> classLoaderAppendListener) {
this.classLoaderAppendListener = classLoaderAppendListener;
}
public ClassLoader getBootstrapClassLoader() {
checkState(bootstrapClassLoader != null, "%s was not initialized", ClassLoaderLifeCycle.class.getName());
return bootstrapClassLoader;
}
public ClassLoader createPluginClassLoader(URL[] urls, ClassLoader parent, String plugin) {
LOG.debug("create new PluginClassLoader for {}", plugin);
DefaultPluginClassLoader pluginClassLoader = new DefaultPluginClassLoader(urls, parent, plugin);
return initAndAppend(pluginClassLoader);
}
public ClassLoader createChildFirstPluginClassLoader(URL[] urls, ClassLoader parent, String plugin) {
LOG.debug("create new ChildFirstPluginClassLoader for {}", plugin);
ChildFirstPluginClassLoader pluginClassLoader = new ChildFirstPluginClassLoader(urls, parent, plugin);
return initAndAppend(pluginClassLoader);
}
void shutdown() {
LOG.info("shutdown classloader infrastructure");
ClassLoaderAndPreventor clap = classLoaders.poll();
while (clap != null) {
clap.shutdown();
clap = classLoaders.poll();
}
bootstrapClassLoader = null;
}
private ClassLoader initAndAppend(ClassLoader originalClassLoader) {
LOG.debug("init classloader {}", originalClassLoader);
ClassLoader classLoader = classLoaderAppendListener.apply(originalClassLoader);
ClassLoaderLeakPreventor preventor = classLoaderLeakPreventorFactory.newLeakPreventor(classLoader);
preventor.runPreClassLoaderInitiators();
classLoaders.push(new ClassLoaderAndPreventor(classLoader, preventor));
return classLoader;
}
private class ClassLoaderAndPreventor {
private final ClassLoader classLoader;
private final ClassLoaderLeakPreventor preventor;
private ClassLoaderAndPreventor(ClassLoader classLoader, ClassLoaderLeakPreventor preventor) {
this.classLoader = classLoader;
this.preventor = preventor;
}
void shutdown() {
LOG.debug("shutdown classloader {}", classLoader);
preventor.runCleanUps();
if (classLoader != webappClassLoader) {
close();
}
}
private void close() {
if (classLoader instanceof Closeable) {
LOG.trace("close classloader {}", classLoader);
try {
((Closeable) classLoader).close();
} catch (IOException e) {
LOG.warn("failed to close classloader", e);
}
}
}
}
}

View File

@@ -0,0 +1,50 @@
package sonia.scm.lifecycle;
import com.google.common.annotations.VisibleForTesting;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.event.RecreateEventBusEvent;
import sonia.scm.event.ScmEventBus;
import java.util.concurrent.atomic.AtomicLong;
/**
* Restart strategy implementation which destroy the injection context and re initialize it.
*/
public class InjectionContextRestartStrategy implements RestartStrategy {
private static final AtomicLong INSTANCE_COUNTER = new AtomicLong();
private static final Logger LOG = LoggerFactory.getLogger(InjectionContextRestartStrategy.class);
private long waitInMs = 250L;
@VisibleForTesting
void setWaitInMs(long waitInMs) {
this.waitInMs = waitInMs;
}
@Override
public void restart(InjectionContext context) {
LOG.warn("destroy injection context");
context.destroy();
LOG.warn("send recreate eventbus event");
ScmEventBus.getInstance().post(new RecreateEventBusEvent());
// restart context delayed, to avoid timing problems
new Thread(() -> {
try {
Thread.sleep(waitInMs);
LOG.warn("reinitialize injection context");
context.initialize();
LOG.debug("register injection context on new eventbus");
ScmEventBus.getInstance().register(context);
} catch ( Exception ex) {
LOG.error("failed to restart", ex);
}
}, "Delayed-Restart-" + INSTANCE_COUNTER.incrementAndGet()).start();
}
}

View File

@@ -0,0 +1,60 @@
package sonia.scm.lifecycle;
import com.google.inject.Binding;
import com.google.inject.Injector;
import com.google.inject.Key;
import sonia.scm.CloseableModule;
import sonia.scm.Default;
import sonia.scm.EagerSingletonModule;
import sonia.scm.ServletContextListenerHolder;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import java.util.Optional;
class InjectionLifeCycle {
private final Injector injector;
InjectionLifeCycle(Injector injector) {
this.injector = injector;
}
void initialize() {
initializeEagerSingletons();
initializeServletContextListeners();
}
void shutdown() {
destroyServletContextListeners();
closeRegisteredCloseables();
}
private void initializeServletContextListeners() {
ServletContextListenerHolder instance = injector.getInstance(ServletContextListenerHolder.class);
ServletContext context = injector.getInstance(Key.get(ServletContext.class, Default.class));
instance.contextInitialized(new ServletContextEvent(context));
}
private void initializeEagerSingletons() {
findInstance(EagerSingletonModule.class).ifPresent(m -> m.initialize(injector));
}
private void closeRegisteredCloseables() {
findInstance(CloseableModule.class).ifPresent(CloseableModule::closeAll);
}
private void destroyServletContextListeners() {
ServletContextListenerHolder instance = injector.getInstance(ServletContextListenerHolder.class);
ServletContext context = injector.getInstance(Key.get(ServletContext.class, Default.class));
instance.contextDestroyed(new ServletContextEvent(context));
}
private <T> Optional<T> findInstance(Class<T> clazz) {
Binding<T> binding = injector.getExistingBinding(Key.get(clazz));
if (binding != null) {
return Optional.of(binding.getProvider().get());
}
return Optional.empty();
}
}

View File

@@ -0,0 +1,44 @@
package sonia.scm.lifecycle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import se.jiderhamn.classloader.leak.prevention.ClassLoaderLeakPreventor;
/**
* Logging adapter for {@link ClassLoaderLeakPreventor}.
*/
public class LoggingAdapter implements se.jiderhamn.classloader.leak.prevention.Logger {
@SuppressWarnings("squid:S3416") // suppress "loggers should be named for their enclosing classes" rule
private static final Logger LOG = LoggerFactory.getLogger(ClassLoaderLeakPreventor.class);
@Override
public void debug(String msg) {
LOG.debug(msg);
}
@Override
public void info(String msg) {
LOG.info(msg);
}
@Override
public void warn(String msg) {
LOG.warn(msg);
}
@Override
public void warn(Throwable t) {
LOG.warn(t.getMessage(), t);
}
@Override
public void error(String msg) {
LOG.error(msg);
}
@Override
public void error(Throwable t) {
LOG.error(t.getMessage(), t);
}
}

View File

@@ -0,0 +1,11 @@
package sonia.scm.lifecycle;
import com.google.inject.Module;
import java.util.Collection;
public interface ModuleProvider {
Collection<Module> createModules();
}

View File

@@ -0,0 +1,208 @@
package sonia.scm.lifecycle;
import com.google.common.base.Charsets;
import com.google.common.collect.ImmutableList;
import com.google.common.io.Files;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.SCMContext;
import sonia.scm.migration.UpdateException;
import sonia.scm.plugin.DefaultPluginLoader;
import sonia.scm.plugin.Plugin;
import sonia.scm.plugin.PluginException;
import sonia.scm.plugin.PluginLoadException;
import sonia.scm.plugin.PluginLoader;
import sonia.scm.plugin.PluginWrapper;
import sonia.scm.plugin.PluginsInternal;
import sonia.scm.plugin.SmpArchive;
import sonia.scm.util.IOUtil;
import javax.servlet.ServletContext;
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.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
public final class PluginBootstrap {
private static final Logger LOG = LoggerFactory.getLogger(PluginBootstrap.class);
private static final String DIRECTORY_PLUGINS = "plugins";
private static final String PLUGIN_DIRECTORY = "/WEB-INF/plugins/";
private static final String PLUGIN_COREINDEX = PLUGIN_DIRECTORY.concat("plugin-index.xml");
private final ClassLoaderLifeCycle classLoaderLifeCycle;
private final ServletContext servletContext;
private final Set<PluginWrapper> plugins;
private final PluginLoader pluginLoader;
PluginBootstrap(ServletContext servletContext, ClassLoaderLifeCycle classLoaderLifeCycle) {
this.servletContext = servletContext;
this.classLoaderLifeCycle = classLoaderLifeCycle;
this.plugins = collectPlugins();
this.pluginLoader = createPluginLoader();
}
public PluginLoader getPluginLoader() {
return pluginLoader;
}
public Set<PluginWrapper> getPlugins() {
return plugins;
}
private PluginLoader createPluginLoader() {
return new DefaultPluginLoader(servletContext, classLoaderLifeCycle.getBootstrapClassLoader(), plugins);
}
private Set<PluginWrapper> collectPlugins() {
try {
File pluginDirectory = getPluginDirectory();
renameOldPluginsFolder(pluginDirectory);
if (!isCorePluginExtractionDisabled()) {
extractCorePlugins(servletContext, pluginDirectory);
} else {
LOG.info("core plugin extraction is disabled");
}
return PluginsInternal.collectPlugins(classLoaderLifeCycle, pluginDirectory.toPath());
} catch (IOException ex) {
throw new PluginLoadException("could not load plugins", ex);
}
}
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) {
LOG.warn("moved old plugins directory to {}", backupDirectory);
} else {
throw new UpdateException("could not rename existing v1 plugin directory");
}
}
}
private boolean isCorePluginExtractionDisabled() {
return Boolean.getBoolean("sonia.scm.boot.disable-core-plugin-extraction");
}
private void extractCorePlugin(ServletContext context, File pluginDirectory,
PluginIndexEntry entry) throws IOException {
URL url = context.getResource(PLUGIN_DIRECTORY.concat(entry.getName()));
SmpArchive archive = SmpArchive.create(url);
Plugin plugin = archive.getPlugin();
File directory = PluginsInternal.createPluginDirectory(pluginDirectory, plugin);
File checksumFile = PluginsInternal.getChecksumFile(directory);
if (!directory.exists()) {
LOG.warn("install plugin {}", plugin.getInformation().getId());
PluginsInternal.extract(archive, entry.getChecksum(), directory, checksumFile, true);
} else if (!checksumFile.exists()) {
LOG.warn("plugin directory {} exists without checksum file.", directory);
PluginsInternal.extract(archive, entry.getChecksum(), directory, checksumFile, true);
} else {
String checksum = Files.toString(checksumFile, Charsets.UTF_8).trim();
if (checksum.equals(entry.getChecksum())) {
LOG.debug("plugin {} is up to date", plugin.getInformation().getId());
} else {
LOG.warn("checksum mismatch of pluing {}, start update", plugin.getInformation().getId());
PluginsInternal.extract(archive, entry.getChecksum(), directory, checksumFile, true);
}
}
}
private void extractCorePlugins(ServletContext context, File pluginDirectory) throws IOException {
IOUtil.mkdirs(pluginDirectory);
PluginIndex index = readCorePluginIndex(context);
for (PluginIndexEntry entry : index) {
extractCorePlugin(context, pluginDirectory, entry);
}
}
private PluginIndex readCorePluginIndex(ServletContext context) {
PluginIndex index;
try {
URL indexUrl = context.getResource(PLUGIN_COREINDEX);
if (indexUrl == null) {
throw new PluginException("no core plugin index found");
}
index = JAXB.unmarshal(indexUrl, PluginIndex.class);
} catch (MalformedURLException ex) {
throw new PluginException("could not load core plugin index", ex);
} catch (DataBindingException ex) {
throw new PluginException("could not unmarshal core plugin index", ex);
}
return index;
}
private File getPluginDirectory() {
File baseDirectory = SCMContext.getContext().getBaseDirectory();
return new File(baseDirectory, DIRECTORY_PLUGINS);
}
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "plugin-index")
private static class PluginIndex implements Iterable<PluginIndexEntry> {
@XmlElement(name = "plugins")
private List<PluginIndexEntry> plugins;
@Override
public Iterator<PluginIndexEntry> iterator() {
return getPlugins().iterator();
}
public List<PluginIndexEntry> getPlugins() {
if (plugins == null) {
plugins = ImmutableList.of();
}
return plugins;
}
}
@XmlRootElement(name = "plugins")
@XmlAccessorType(XmlAccessType.FIELD)
private static class PluginIndexEntry {
private String checksum;
private String name;
public String getName() {
return name;
}
public String getChecksum() {
return checksum;
}
}
}

View File

@@ -0,0 +1,97 @@
package sonia.scm.lifecycle;
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,38 @@
package sonia.scm.lifecycle;
/**
* Strategy for restarting SCM-Manager.
*/
public interface RestartStrategy {
/**
* Context for Injection in SCM-Manager.
*/
interface InjectionContext {
/**
* Initialize the injection context.
*/
void initialize();
/**
* Destroys the injection context.
*/
void destroy();
}
/**
* Restart SCM-Manager.
* @param context injection context
*/
void restart(InjectionContext context);
/**
* Returns the configured strategy.
*
* @return configured strategy
*/
static RestartStrategy get() {
return new InjectionContextRestartStrategy();
}
}

View File

@@ -0,0 +1,61 @@
package sonia.scm.lifecycle;
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

@@ -0,0 +1,14 @@
package sonia.scm.lifecycle;
import com.google.inject.servlet.ServletModule;
import sonia.scm.Default;
import javax.servlet.ServletContext;
class ServletContextModule extends ServletModule {
@Override
protected void configureServlets() {
bind(ServletContext.class).annotatedWith(Default.class).toInstance(getServletContext());
}
}

View File

@@ -0,0 +1,79 @@
package sonia.scm.lifecycle;
import com.google.common.annotations.VisibleForTesting;
import org.apache.shiro.authc.credential.PasswordService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.plugin.Extension;
import sonia.scm.security.PermissionAssigner;
import sonia.scm.security.PermissionDescriptor;
import sonia.scm.user.User;
import sonia.scm.user.UserManager;
import sonia.scm.web.security.AdministrationContext;
import sonia.scm.web.security.PrivilegedAction;
import javax.inject.Inject;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import java.util.Collections;
@Extension
public class SetupContextListener implements ServletContextListener {
private static final Logger LOG = LoggerFactory.getLogger(SetupContextListener.class);
private final AdministrationContext administrationContext;
@Inject
public SetupContextListener(AdministrationContext administrationContext) {
this.administrationContext = administrationContext;
}
@Override
public void contextInitialized(ServletContextEvent sce) {
if (Boolean.getBoolean("sonia.scm.skipAdminCreation")) {
LOG.info("found skipAdminCreation flag; skipping creation of scmadmin");
} else {
administrationContext.runAsAdmin(SetupAction.class);
}
}
@Override
public void contextDestroyed(ServletContextEvent sce) {}
@VisibleForTesting
static class SetupAction implements PrivilegedAction {
private final UserManager userManager;
private final PasswordService passwordService;
private final PermissionAssigner permissionAssigner;
@Inject
public SetupAction(UserManager userManager, PasswordService passwordService, PermissionAssigner permissionAssigner) {
this.userManager = userManager;
this.passwordService = passwordService;
this.permissionAssigner = permissionAssigner;
}
@Override
public void run() {
if (isFirstStart()) {
createAdminAccount();
}
}
private boolean isFirstStart() {
return userManager.getAll().isEmpty();
}
private void createAdminAccount() {
User scmadmin = new User("scmadmin", "SCM Administrator", "scm-admin@scm-manager.org");
String password = passwordService.encryptPassword("scmadmin");
scmadmin.setPassword(password);
userManager.create(scmadmin);
PermissionDescriptor descriptor = new PermissionDescriptor("*");
permissionAssigner.setPermissionsForUser("scmadmin", Collections.singleton(descriptor));
}
}
}

View File

@@ -0,0 +1,115 @@
package sonia.scm.lifecycle;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.inject.Module;
import com.google.inject.multibindings.Multibinder;
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.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Collection;
final class SingleView {
private SingleView() {
}
static SingleViewModuleProvider 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 SingleViewModuleProvider(controller);
}
static SingleViewModuleProvider 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 SingleViewModuleProvider(controller);
}
private static class SingleViewModuleProvider implements ModuleProvider {
private final ViewController controller;
private SingleViewModuleProvider(ViewController controller) {
this.controller = controller;
}
@Override
public Collection<Module> createModules() {
return ImmutableList.of(new ServletContextModule(), 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);
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.lifecycle;
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,47 @@
package sonia.scm.lifecycle;
import com.github.sdorra.webresources.CacheControl;
import com.github.sdorra.webresources.WebResourceSender;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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 static final Logger LOG = LoggerFactory.getLogger(StaticResourceServlet.class);
private final WebResourceSender sender = WebResourceSender.create()
.withGZIP()
.withGZIPMinLength(512)
.withBufferSize(16384)
.withCacheControl(CacheControl.create().noCache());
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) {
try {
URL resource = createResourceUrlFromRequest(request);
if (resource != null) {
sender.resource(resource).get(request, response);
} else {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
}
} catch (IOException ex) {
LOG.warn("failed to servce resource", ex);
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
}
private URL createResourceUrlFromRequest(HttpServletRequest request) throws MalformedURLException {
String uri = HttpUtil.getStrippedURI(request);
return request.getServletContext().getResource(uri);
}
}

View File

@@ -0,0 +1,24 @@
package sonia.scm.lifecycle;
import com.google.inject.AbstractModule;
import com.google.inject.multibindings.Multibinder;
import sonia.scm.migration.UpdateStep;
import sonia.scm.plugin.PluginLoader;
class UpdateStepModule extends AbstractModule {
private final PluginLoader pluginLoader;
UpdateStepModule(PluginLoader pluginLoader) {
this.pluginLoader = pluginLoader;
}
@Override
protected void configure() {
Multibinder<UpdateStep> updateStepBinder = Multibinder.newSetBinder(binder(), UpdateStep.class);
pluginLoader
.getExtensionProcessor()
.byExtensionPoint(UpdateStep.class)
.forEach(stepClass -> updateStepBinder.addBinding().to(stepClass));
}
}

View File

@@ -0,0 +1,77 @@
package sonia.scm.lifecycle;
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.lifecycle;
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.lifecycle;
import javax.servlet.http.HttpServletRequest;
public interface ViewController {
String getTemplate();
View createView(HttpServletRequest request);
}