diff --git a/CHANGELOG.md b/CHANGELOG.md index 462b378436..9a4de014bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased +### Added +- Support for Java versions > 8 +- Simple ClassLoaderLifeCycle to fix integration tests on Java > 8 + +### Changed +- Upgrade [Legman](https://github.com/sdorra/legman) to v1.6.2 in order to fix execution on Java versions > 8 +- Upgrade [Lombok](https://projectlombok.org/) to version 1.18.10 in order to fix build on Java versions > 8 +- Upgrade [Mockito](https://site.mockito.org/) to version 2.28.2 in order to fix tests on Java versions > 8 +- Upgrade smp-maven-plugin to version 1.0.0-rc3 + ## 2.0.0-rc3 - 2020-01-31 ### Fixed - Broken plugin order fixed diff --git a/Jenkinsfile b/Jenkinsfile index 6ad4f7d459..43fece6567 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -37,7 +37,7 @@ node('docker') { } stage('Integration Test') { - mvn 'verify -Pit -pl :scm-webapp,:scm-it -Dmaven.test.failure.ignore=true -DClassLoaderLeakPreventor.threadWaitMs=10' + mvn 'verify -Pit -pl :scm-webapp,:scm-it -Dmaven.test.failure.ignore=true' } stage('SonarQube') { @@ -108,7 +108,7 @@ String mainBranch Maven setupMavenBuild() { // Keep this version number in sync with .mvn/maven-wrapper.properties - Maven mvn = new MavenInDocker(this, '3.5.2-jdk-8') + Maven mvn = new MavenInDocker(this, '3.6.3-jdk-11') if (isMainBranch()) { // Release starts javadoc, which takes very long, so do only for certain branches diff --git a/lombok.config b/lombok.config new file mode 100644 index 0000000000..d8ef4d1d98 --- /dev/null +++ b/lombok.config @@ -0,0 +1,4 @@ +# since v1.16.20 the @ConstructorProperties is no longer added to constructors, +# but this annotation seem to be used by Jackson. +# Not sure if this is realy required, but without sonia.scm.api.rest.JSONContextResolverTest breaks +lombok.anyConstructor.addConstructorProperties = true diff --git a/pom.xml b/pom.xml index a9e43c9015..48e94c11de 100644 --- a/pom.xml +++ b/pom.xml @@ -212,7 +212,7 @@ org.projectlombok lombok - 1.16.18 + 1.18.10 provided @@ -285,9 +285,9 @@ - org.hibernate + org.hibernate.validator hibernate-validator - ${hibernate-validator.version} + ${hibernate-validator.version} @@ -434,7 +434,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.0.1 + 3.1.1 org.apache.maven.plugins @@ -456,7 +456,7 @@ sonia.scm.maven smp-maven-plugin - 1.0.0-rc2 + 1.0.0-rc3 @@ -625,6 +625,7 @@ ${project.build.sourceEncoding} ${project.build.sourceEncoding} true + ${project.build.javaLevel} true true true @@ -664,11 +665,11 @@ org.apache.maven.plugins maven-release-plugin - 2.3.2 + 2.5.3 false true - release,APIviz,doc,nativepkg + release,doc,nativepkg @{project.version} true @@ -798,32 +799,6 @@ - - APIviz - - - sonia.apiviz - true - - - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - 3.0.0 - - false - - - - - - - - @@ -844,7 +819,7 @@ - 2.23.0 + 2.28.2 1.3 5.2.0 @@ -863,7 +838,7 @@ 6.1.0.Final - 1.6.1 + 1.6.2 9.4.22.v20191022 @@ -885,8 +860,8 @@ 1.16.0 - 1.8 - 1.8 + 8 + 8 UTF-8 SCM-BSD diff --git a/scm-annotations/src/main/java/sonia/scm/i18n/I18n.java b/scm-annotations/src/main/java/sonia/scm/i18n/I18n.java index 63d761fb17..5f3f6cd18f 100644 --- a/scm-annotations/src/main/java/sonia/scm/i18n/I18n.java +++ b/scm-annotations/src/main/java/sonia/scm/i18n/I18n.java @@ -40,7 +40,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** - * The I18n annotation is used by the {@link I18nMessages} class to define the + * The I18n annotation is used by the I18nMessages class to define the * resource bundle key. * * @author Sebastian Sdorra @@ -53,7 +53,7 @@ public @interface I18n { /** * Returns the key for the resource bundle. - * + * * @return resource bundle key */ String value(); diff --git a/scm-core/pom.xml b/scm-core/pom.xml index 086013364b..563ac0f40f 100644 --- a/scm-core/pom.xml +++ b/scm-core/pom.xml @@ -1,6 +1,6 @@ - + 4.0.0 @@ -15,7 +15,7 @@ scm-core - + @@ -55,13 +55,13 @@ - + org.apache.shiro shiro-core ${shiro.version} - + @@ -176,7 +176,7 @@ - org.hibernate + org.hibernate.validator hibernate-validator @@ -230,12 +230,12 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.0.0 true ${project.build.sourceEncoding} ${project.build.sourceEncoding} ${project.build.sourceEncoding} + ${project.build.javaLevel} true true true @@ -253,20 +253,6 @@ http://www.slf4j.org/api/ http://shiro.apache.org/static/${shiro.version}/apidocs/ - org.jboss.apiviz.APIviz - - org.jboss.apiviz - apiviz - 1.3.2.GA - - - - -sourceclasspath ${project.build.outputDirectory} - - - -nopackagediagram - - diff --git a/scm-core/src/main/java/sonia/scm/BasicContextProvider.java b/scm-core/src/main/java/sonia/scm/BasicContextProvider.java index f4ed04c3f5..1764aceba8 100644 --- a/scm-core/src/main/java/sonia/scm/BasicContextProvider.java +++ b/scm-core/src/main/java/sonia/scm/BasicContextProvider.java @@ -36,6 +36,7 @@ package sonia.scm; //~--- non-JDK imports -------------------------------------------------------- import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; import sonia.scm.util.Util; //~--- JDK imports ------------------------------------------------------------ @@ -57,7 +58,13 @@ public class BasicContextProvider implements SCMContextProvider { /** Default version {@link String} */ - public static final String DEFAULT_VERSION = "unknown"; + public static final String VERSION_DEFAULT = "unknown"; + + /** + * System property to override scm-manager version. + * Note: This should only be used for testing and never in production. + **/ + public static final String VERSION_PROPERTY = "sonia.scm.version.override"; /** Default name of the SCM-Manager base directory */ public static final String DIRECTORY_DEFAULT = ".scm"; @@ -95,7 +102,7 @@ public class BasicContextProvider implements SCMContextProvider try { baseDirectory = findBaseDirectory(); - version = loadVersion(); + version = determineVersion(); stage = loadProjectStage(); } catch (Exception ex) @@ -157,7 +164,7 @@ public class BasicContextProvider implements SCMContextProvider /** * Returns the version of the SCM-Manager. If the version is not set, the - * {@link #DEFAULT_VERSION} is returned. + * {@link #VERSION_DEFAULT} is returned. * * * @return the version of the SCM-Manager @@ -257,6 +264,14 @@ public class BasicContextProvider implements SCMContextProvider return s; } + private String determineVersion() { + String version = System.getProperty(VERSION_PROPERTY); + if (Strings.isNullOrEmpty(version)) { + version = loadVersion(); + } + return version; + } + /** * Loads the version of the SCM-Manager from maven properties file. * @@ -295,7 +310,7 @@ public class BasicContextProvider implements SCMContextProvider } } - return properties.getProperty(MAVEN_PROPERTY_VERSION, DEFAULT_VERSION); + return properties.getProperty(MAVEN_PROPERTY_VERSION, VERSION_DEFAULT); } //~--- get methods ---------------------------------------------------------- diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/MergeCommandDto.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/MergeCommandDto.java index 0661d6a4ef..dfadc08b3e 100644 --- a/scm-core/src/main/java/sonia/scm/api/v2/resources/MergeCommandDto.java +++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/MergeCommandDto.java @@ -2,7 +2,7 @@ package sonia.scm.api.v2.resources; import lombok.Getter; import lombok.Setter; -import org.hibernate.validator.constraints.NotEmpty; +import javax.validation.constraints.NotEmpty; @Getter @Setter public class MergeCommandDto { diff --git a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java index 080d66ebf2..586f28761b 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java @@ -91,6 +91,7 @@ public final class RepositoryService implements Closeable { private final PreProcessorUtil preProcessorUtil; private final RepositoryServiceProvider provider; private final Repository repository; + @SuppressWarnings("rawtypes") private final Set protocolProviders; private final WorkdirProvider workdirProvider; @@ -104,7 +105,10 @@ public final class RepositoryService implements Closeable { */ RepositoryService(CacheManager cacheManager, RepositoryServiceProvider provider, Repository repository, - PreProcessorUtil preProcessorUtil, Set protocolProviders, WorkdirProvider workdirProvider) { + PreProcessorUtil preProcessorUtil, + @SuppressWarnings("rawtypes") Set protocolProviders, + WorkdirProvider workdirProvider + ) { this.cacheManager = cacheManager; this.provider = provider; this.repository = repository; @@ -445,19 +449,23 @@ public final class RepositoryService implements Closeable { return provider.getSupportedFeatures().contains(feature); } - public Stream getSupportedProtocols() { + public Stream getSupportedProtocols() { return protocolProviders.stream() .filter(protocolProvider -> protocolProvider.getType().equals(getRepository().getType())) - .map(this::createProviderInstanceForRepository); + .map(this::createProviderInstanceForRepository); } - private T createProviderInstanceForRepository(ScmProtocolProvider protocolProvider) { + @SuppressWarnings("rawtypes") + private ScmProtocol createProviderInstanceForRepository(ScmProtocolProvider protocolProvider) { return protocolProvider.get(repository); } + @SuppressWarnings("unchecked") public T getProtocol(Class clazz) { - return this.getSupportedProtocols() + return this.getSupportedProtocols() .filter(scmProtocol -> clazz.isAssignableFrom(scmProtocol.getClass())) + // no idea how to fix this, without cast + .map(p -> (T) p) .findFirst() .orElseThrow(() -> new IllegalArgumentException(String.format("no implementation for %s and repository type %s", clazz.getName(),getRepository().getType()))); } diff --git a/scm-core/src/test/java/sonia/scm/BasicContextProviderTest.java b/scm-core/src/test/java/sonia/scm/BasicContextProviderTest.java index 4fb9dfd4fa..523e579b17 100644 --- a/scm-core/src/test/java/sonia/scm/BasicContextProviderTest.java +++ b/scm-core/src/test/java/sonia/scm/BasicContextProviderTest.java @@ -1,6 +1,7 @@ package sonia.scm; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junitpioneer.jupiter.TempDirectory; @@ -13,32 +14,59 @@ import static org.assertj.core.api.Assertions.assertThat; @ExtendWith(TempDirectory.class) class BasicContextProviderTest { - private Path baseDirectory; + @Nested + class VersionTests { - private BasicContextProvider context; + @Test + void shouldReturnVersionFromSystemProperty() { + System.setProperty(BasicContextProvider.VERSION_PROPERTY, "3.0.0"); + try { + SCMContextProvider context = new BasicContextProvider(); + assertThat(context.getVersion()).isEqualTo("3.0.0"); + } finally { + System.clearProperty(BasicContextProvider.VERSION_PROPERTY); + } + } + + @Test + void shouldReturnDefaultVersion() { + SCMContextProvider context = new BasicContextProvider(); + assertThat(context.getVersion()).isEqualTo(BasicContextProvider.VERSION_DEFAULT); + } - @BeforeEach - void setUpContext(@TempDirectory.TempDir Path baseDirectory) { - this.baseDirectory = baseDirectory; - context = new BasicContextProvider(baseDirectory.toFile(), "x.y.z", Stage.PRODUCTION); } - @Test - void shouldReturnAbsolutePathAsIs(@TempDirectory.TempDir Path path) { - Path absolutePath = path.toAbsolutePath(); - Path resolved = context.resolve(absolutePath); + @Nested + class PathTests { - assertThat(resolved).isSameAs(absolutePath); - } + private Path baseDirectory; - @Test - void shouldResolveRelatePath() { - Path path = Paths.get("repos", "42"); - Path resolved = context.resolve(path); + private BasicContextProvider context; + + @BeforeEach + void setUpContext(@TempDirectory.TempDir Path baseDirectory) { + this.baseDirectory = baseDirectory; + context = new BasicContextProvider(baseDirectory.toFile(), "x.y.z", Stage.PRODUCTION); + } + + @Test + void shouldReturnAbsolutePathAsIs(@TempDirectory.TempDir Path path) { + Path absolutePath = path.toAbsolutePath(); + Path resolved = context.resolve(absolutePath); + + assertThat(resolved).isSameAs(absolutePath); + } + + @Test + void shouldResolveRelatePath() { + Path path = Paths.get("repos", "42"); + Path resolved = context.resolve(path); + + assertThat(resolved).isAbsolute(); + assertThat(resolved).startsWithRaw(baseDirectory); + assertThat(resolved).endsWithRaw(path); + } - assertThat(resolved).isAbsolute(); - assertThat(resolved).startsWithRaw(baseDirectory); - assertThat(resolved).endsWithRaw(path); } } diff --git a/scm-it/pom.xml b/scm-it/pom.xml index c38c73ce3e..9064a39fc1 100644 --- a/scm-it/pom.xml +++ b/scm-it/pom.xml @@ -200,6 +200,10 @@ java.awt.headless true + + sonia.scm.classloading.lifecycle + simple + /scm diff --git a/scm-webapp/pom.xml b/scm-webapp/pom.xml index 487eb0bfeb..924e176024 100644 --- a/scm-webapp/pom.xml +++ b/scm-webapp/pom.xml @@ -720,6 +720,10 @@ scm.stage ${scm.stage} + + sonia.scm.classloading.lifecycle + simple + ${project.basedir}/src/main/conf/jetty.xml 0 @@ -805,6 +809,10 @@ scm.home target/scm-it + + sonia.scm.classloading.lifecycle + simple + ${project.basedir}/src/main/conf/jetty.xml 0 diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/ObjectMapperProvider.java b/scm-webapp/src/main/java/sonia/scm/api/rest/ObjectMapperProvider.java index 68522234d3..7a59ba1339 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/ObjectMapperProvider.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/ObjectMapperProvider.java @@ -7,7 +7,7 @@ import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.introspect.AnnotationIntrospectorPair; import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector; import com.fasterxml.jackson.databind.type.TypeFactory; -import com.fasterxml.jackson.databind.util.ISO8601DateFormat; +import com.fasterxml.jackson.databind.util.StdDateFormat; import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.fasterxml.jackson.module.jaxb.JaxbAnnotationIntrospector; @@ -27,7 +27,7 @@ public class ObjectMapperProvider implements Provider { mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); mapper.configure(SerializationFeature.WRITE_DATES_WITH_ZONE_ID, true); - mapper.setDateFormat(new ISO8601DateFormat()); + mapper.setDateFormat(new StdDateFormat()); return mapper; } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AutoCompleteResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AutoCompleteResource.java index 38b421f573..26868e32f5 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AutoCompleteResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AutoCompleteResource.java @@ -2,7 +2,7 @@ package sonia.scm.api.v2.resources; import com.webcohesion.enunciate.metadata.rs.ResponseCode; import com.webcohesion.enunciate.metadata.rs.StatusCodes; -import org.hibernate.validator.constraints.NotEmpty; +import javax.validation.constraints.NotEmpty; import sonia.scm.ReducedModelObject; import sonia.scm.group.GroupDisplayManager; import sonia.scm.user.UserDisplayManager; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchDto.java index c66428697d..f32b93f813 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchDto.java @@ -7,8 +7,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import org.hibernate.validator.constraints.Length; -import org.hibernate.validator.constraints.NotEmpty; - +import javax.validation.constraints.NotEmpty; import javax.validation.constraints.Pattern; @Getter @Setter @NoArgsConstructor diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRequestDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRequestDto.java index 3db338ea85..02b9e37ade 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRequestDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRequestDto.java @@ -3,7 +3,7 @@ package sonia.scm.api.v2.resources; import lombok.Getter; import lombok.Setter; import org.hibernate.validator.constraints.Length; -import org.hibernate.validator.constraints.NotEmpty; +import javax.validation.constraints.NotEmpty; import javax.validation.constraints.Pattern; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PasswordChangeDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PasswordChangeDto.java index 8a69c58e86..d3e96c7f02 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PasswordChangeDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PasswordChangeDto.java @@ -3,7 +3,7 @@ package sonia.scm.api.v2.resources; import lombok.Getter; import lombok.Setter; import lombok.ToString; -import org.hibernate.validator.constraints.NotEmpty; +import javax.validation.constraints.NotEmpty; @Getter @Setter diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PasswordOverwriteDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PasswordOverwriteDto.java index 0570ed81e1..21b5c0bec6 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PasswordOverwriteDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PasswordOverwriteDto.java @@ -3,7 +3,7 @@ package sonia.scm.api.v2.resources; import lombok.Getter; import lombok.Setter; import lombok.ToString; -import org.hibernate.validator.constraints.NotEmpty; +import javax.validation.constraints.NotEmpty; @Getter @Setter diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java index f3f28191ae..df0fb0bafc 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java @@ -7,8 +7,8 @@ import de.otto.edison.hal.Links; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import org.hibernate.validator.constraints.Email; -import org.hibernate.validator.constraints.NotEmpty; +import javax.validation.constraints.Email; +import javax.validation.constraints.NotEmpty; import sonia.scm.util.ValidationUtil; import javax.validation.constraints.Pattern; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionDto.java index 398e219207..37d2dce537 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionDto.java @@ -6,7 +6,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import lombok.ToString; -import org.hibernate.validator.constraints.NotEmpty; +import javax.validation.constraints.NotEmpty; import sonia.scm.util.ValidationUtil; import javax.validation.constraints.Pattern; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleDto.java index 7840cad0ee..c6c5f2a779 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleDto.java @@ -6,7 +6,7 @@ import de.otto.edison.hal.Links; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import org.hibernate.validator.constraints.NotEmpty; +import javax.validation.constraints.NotEmpty; import java.time.Instant; import java.util.Collection; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserDto.java index a46f353be4..d8a9885398 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserDto.java @@ -7,8 +7,8 @@ import de.otto.edison.hal.Links; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import org.hibernate.validator.constraints.Email; -import org.hibernate.validator.constraints.NotEmpty; +import javax.validation.constraints.Email; +import javax.validation.constraints.NotEmpty; import sonia.scm.util.ValidationUtil; import javax.validation.constraints.Pattern; diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/classloading/ClassLoaderLifeCycle.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/classloading/ClassLoaderLifeCycle.java index a64ed6fa43..fb7d991c1e 100644 --- a/scm-webapp/src/main/java/sonia/scm/lifecycle/classloading/ClassLoaderLifeCycle.java +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/classloading/ClassLoaderLifeCycle.java @@ -3,198 +3,75 @@ package sonia.scm.lifecycle.classloading; 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.IIOServiceProviderCleanUp; -import se.jiderhamn.classloader.leak.prevention.cleanup.MBeanCleanUp; -import se.jiderhamn.classloader.leak.prevention.cleanup.ShutdownHookCleanUp; -import se.jiderhamn.classloader.leak.prevention.cleanup.StopThreadsCleanUp; -import se.jiderhamn.classloader.leak.prevention.preinit.AwtToolkitInitiator; -import se.jiderhamn.classloader.leak.prevention.preinit.Java2dDisposerInitiator; -import se.jiderhamn.classloader.leak.prevention.preinit.Java2dRenderQueueInitiator; -import se.jiderhamn.classloader.leak.prevention.preinit.SunAwtAppContextInitiator; import sonia.scm.lifecycle.LifeCycle; 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 static com.google.common.base.Preconditions.checkState; -import static se.jiderhamn.classloader.leak.prevention.cleanup.ShutdownHookCleanUp.SHUTDOWN_HOOK_WAIT_MS_DEFAULT; /** - * Creates and shutdown SCM-Manager ClassLoaders. + * Base class for ClassLoader LifeCycle implementation in SCM-Manager. */ -public final class ClassLoaderLifeCycle implements LifeCycle { +public abstract class ClassLoaderLifeCycle implements LifeCycle { private static final Logger LOG = LoggerFactory.getLogger(ClassLoaderLifeCycle.class); - private Deque classLoaders = new ArrayDeque<>(); + @VisibleForTesting + static final String PROPERTY = "sonia.scm.classloading.lifecycle"; + + public static ClassLoaderLifeCycle create() { + ClassLoader webappClassLoader = Thread.currentThread().getContextClassLoader(); + String implementation = System.getProperty(PROPERTY); + if (SimpleClassLoaderLifeCycle.NAME.equalsIgnoreCase(implementation)) { + LOG.info("create new simple ClassLoaderLifeCycle"); + return new SimpleClassLoaderLifeCycle(webappClassLoader); + } + LOG.info("create new ClassLoaderLifeCycle with leak prevention"); + return new ClassLoaderLifeCycleWithLeakPrevention(webappClassLoader); + } - private final ClassLoaderLeakPreventorFactory classLoaderLeakPreventorFactory; private final ClassLoader webappClassLoader; private BootstrapClassLoader bootstrapClassLoader; - private ClassLoaderAppendListener classLoaderAppendListener = new ClassLoaderAppendListener() { - @Override - public C apply(C classLoader) { - return classLoader; - } - }; - - @VisibleForTesting - public static ClassLoaderLifeCycle create() { - ClassLoader webappClassLoader = Thread.currentThread().getContextClassLoader(); - ClassLoaderLeakPreventorFactory classLoaderLeakPreventorFactory = createClassLoaderLeakPreventorFactory(webappClassLoader); - return new ClassLoaderLifeCycle(webappClassLoader, classLoaderLeakPreventorFactory); - } - - ClassLoaderLifeCycle(ClassLoader webappClassLoader, ClassLoaderLeakPreventorFactory classLoaderLeakPreventorFactory) { - this.classLoaderLeakPreventorFactory = classLoaderLeakPreventorFactory; - this.webappClassLoader = initAndAppend(webappClassLoader); - } - - private static ClassLoaderLeakPreventorFactory createClassLoaderLeakPreventorFactory(ClassLoader webappClassLoader) { - // Should threads tied to the web app classloader be forced to stop at application shutdown? - boolean stopThreads = Boolean.getBoolean("ClassLoaderLeakPreventor.stopThreads"); - - // Should Timer threads tied to the web app classloader be forced to stop at application shutdown? - boolean stopTimerThreads = Boolean.getBoolean("ClassLoaderLeakPreventor.stopTimerThreads"); - - // Should shutdown hooks registered from the application be executed at application shutdown? - boolean executeShutdownHooks = Boolean.getBoolean("ClassLoaderLeakPreventor.executeShutdownHooks"); - - // No of milliseconds to wait for threads to finish execution, before stopping them. - int threadWaitMs = Integer.getInteger("ClassLoaderLeakPreventor.threadWaitMs", ClassLoaderLeakPreventor.THREAD_WAIT_MS_DEFAULT); - - /* - * No of milliseconds to wait for shutdown hooks to finish execution, before stopping them. - * If set to -1 there will be no waiting at all, but Thread is allowed to run until finished. - */ - int shutdownHookWaitMs = Integer.getInteger("ClassLoaderLeakPreventor.shutdownHookWaitMs", SHUTDOWN_HOOK_WAIT_MS_DEFAULT); - - LOG.info("Settings for {} (CL: 0x{}):", ClassLoaderLifeCycle.class.getName(), Integer.toHexString(System.identityHashCode(webappClassLoader)) ); - LOG.info(" stopThreads = {}", stopThreads); - LOG.info(" stopTimerThreads = {}", stopTimerThreads); - LOG.info(" executeShutdownHooks = {}", executeShutdownHooks); - LOG.info(" threadWaitMs = {} ms", threadWaitMs); - LOG.info(" shutdownHookWaitMs = {} ms", shutdownHookWaitMs); - - // use webapp classloader as safe base? or system? - ClassLoaderLeakPreventorFactory classLoaderLeakPreventorFactory = new ClassLoaderLeakPreventorFactory(webappClassLoader); - classLoaderLeakPreventorFactory.setLogger(new LoggingAdapter()); - - final ShutdownHookCleanUp shutdownHookCleanUp = classLoaderLeakPreventorFactory.getCleanUp(ShutdownHookCleanUp.class); - shutdownHookCleanUp.setExecuteShutdownHooks(executeShutdownHooks); - shutdownHookCleanUp.setShutdownHookWaitMs(shutdownHookWaitMs); - - final StopThreadsCleanUp stopThreadsCleanUp = classLoaderLeakPreventorFactory.getCleanUp(StopThreadsCleanUp.class); - stopThreadsCleanUp.setStopThreads(stopThreads); - stopThreadsCleanUp.setStopTimerThreads(stopTimerThreads); - stopThreadsCleanUp.setThreadWaitMs(threadWaitMs); - - // remove awt and imageio cleanup - classLoaderLeakPreventorFactory.removePreInitiator(AwtToolkitInitiator.class); - classLoaderLeakPreventorFactory.removePreInitiator(SunAwtAppContextInitiator.class); - classLoaderLeakPreventorFactory.removeCleanUp(IIOServiceProviderCleanUp.class); - classLoaderLeakPreventorFactory.removePreInitiator(Java2dRenderQueueInitiator.class); - classLoaderLeakPreventorFactory.removePreInitiator(Java2dDisposerInitiator.class); - - // the MBeanCleanUp causes a Exception and we use no mbeans - classLoaderLeakPreventorFactory.removeCleanUp(MBeanCleanUp.class); - - return classLoaderLeakPreventorFactory; + ClassLoaderLifeCycle(ClassLoader webappClassLoader) { + this.webappClassLoader = webappClassLoader; } + @Override public void initialize() { bootstrapClassLoader = initAndAppend(new BootstrapClassLoader(webappClassLoader)); } - @VisibleForTesting - void setClassLoaderAppendListener(ClassLoaderAppendListener classLoaderAppendListener) { - this.classLoaderAppendListener = classLoaderAppendListener; - } + protected abstract T initAndAppend(T classLoader); 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); } + 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); + } + + @Override public void shutdown() { LOG.info("shutdown classloader infrastructure"); - ClassLoaderAndPreventor clap = classLoaders.poll(); - while (clap != null) { - clap.shutdown(); - clap = classLoaders.poll(); - } - // be sure it is realy empty - classLoaders.clear(); - classLoaders = new ArrayDeque<>(); + shutdownClassLoaders(); bootstrapClassLoader.markAsShutdown(); bootstrapClassLoader = null; } - private T initAndAppend(T originalClassLoader) { - LOG.debug("init classloader {}", originalClassLoader); - T classLoader = classLoaderAppendListener.apply(originalClassLoader); - - ClassLoaderLeakPreventor preventor = classLoaderLeakPreventorFactory.newLeakPreventor(classLoader); - preventor.runPreClassLoaderInitiators(); - classLoaders.push(new ClassLoaderAndPreventor(classLoader, preventor)); - - return classLoader; - } - - interface ClassLoaderAppendListener { - C apply(C 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); - } - } - } - } + protected abstract void shutdownClassLoaders(); } diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/classloading/ClassLoaderLifeCycleWithLeakPrevention.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/classloading/ClassLoaderLifeCycleWithLeakPrevention.java new file mode 100644 index 0000000000..e2e7a032b7 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/classloading/ClassLoaderLifeCycleWithLeakPrevention.java @@ -0,0 +1,163 @@ +package sonia.scm.lifecycle.classloading; + +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.IIOServiceProviderCleanUp; +import se.jiderhamn.classloader.leak.prevention.cleanup.MBeanCleanUp; +import se.jiderhamn.classloader.leak.prevention.cleanup.ShutdownHookCleanUp; +import se.jiderhamn.classloader.leak.prevention.cleanup.StopThreadsCleanUp; +import se.jiderhamn.classloader.leak.prevention.preinit.AwtToolkitInitiator; +import se.jiderhamn.classloader.leak.prevention.preinit.Java2dDisposerInitiator; +import se.jiderhamn.classloader.leak.prevention.preinit.Java2dRenderQueueInitiator; +import se.jiderhamn.classloader.leak.prevention.preinit.SunAwtAppContextInitiator; + +import java.io.Closeable; +import java.io.IOException; +import java.util.ArrayDeque; +import java.util.Deque; + +import static se.jiderhamn.classloader.leak.prevention.cleanup.ShutdownHookCleanUp.SHUTDOWN_HOOK_WAIT_MS_DEFAULT; + +/** + * Creates and shutdown SCM-Manager ClassLoaders with ClassLoader leak detection. + */ +final class ClassLoaderLifeCycleWithLeakPrevention extends ClassLoaderLifeCycle { + + private static final Logger LOG = LoggerFactory.getLogger(ClassLoaderLifeCycleWithLeakPrevention.class); + + private Deque classLoaders = new ArrayDeque<>(); + + private final ClassLoaderLeakPreventorFactory classLoaderLeakPreventorFactory; + + private ClassLoaderAppendListener classLoaderAppendListener = new ClassLoaderAppendListener() { + @Override + public C apply(C classLoader) { + return classLoader; + } + }; + + ClassLoaderLifeCycleWithLeakPrevention(ClassLoader webappClassLoader) { + this(webappClassLoader, createClassLoaderLeakPreventorFactory(webappClassLoader)); + } + + ClassLoaderLifeCycleWithLeakPrevention(ClassLoader webappClassLoader, ClassLoaderLeakPreventorFactory classLoaderLeakPreventorFactory) { + super(webappClassLoader); + this.classLoaderLeakPreventorFactory = classLoaderLeakPreventorFactory; + } + + private static ClassLoaderLeakPreventorFactory createClassLoaderLeakPreventorFactory(ClassLoader webappClassLoader) { + // Should threads tied to the web app classloader be forced to stop at application shutdown? + boolean stopThreads = Boolean.getBoolean("ClassLoaderLeakPreventor.stopThreads"); + + // Should Timer threads tied to the web app classloader be forced to stop at application shutdown? + boolean stopTimerThreads = Boolean.getBoolean("ClassLoaderLeakPreventor.stopTimerThreads"); + + // Should shutdown hooks registered from the application be executed at application shutdown? + boolean executeShutdownHooks = Boolean.getBoolean("ClassLoaderLeakPreventor.executeShutdownHooks"); + + // No of milliseconds to wait for threads to finish execution, before stopping them. + int threadWaitMs = Integer.getInteger("ClassLoaderLeakPreventor.threadWaitMs", ClassLoaderLeakPreventor.THREAD_WAIT_MS_DEFAULT); + + /* + * No of milliseconds to wait for shutdown hooks to finish execution, before stopping them. + * If set to -1 there will be no waiting at all, but Thread is allowed to run until finished. + */ + int shutdownHookWaitMs = Integer.getInteger("ClassLoaderLeakPreventor.shutdownHookWaitMs", SHUTDOWN_HOOK_WAIT_MS_DEFAULT); + + LOG.info("Settings for {} (CL: 0x{}):", ClassLoaderLifeCycleWithLeakPrevention.class.getName(), Integer.toHexString(System.identityHashCode(webappClassLoader)) ); + LOG.info(" stopThreads = {}", stopThreads); + LOG.info(" stopTimerThreads = {}", stopTimerThreads); + LOG.info(" executeShutdownHooks = {}", executeShutdownHooks); + LOG.info(" threadWaitMs = {} ms", threadWaitMs); + LOG.info(" shutdownHookWaitMs = {} ms", shutdownHookWaitMs); + + // use webapp classloader as safe base? or system? + ClassLoaderLeakPreventorFactory classLoaderLeakPreventorFactory = new ClassLoaderLeakPreventorFactory(webappClassLoader); + classLoaderLeakPreventorFactory.setLogger(new LoggingAdapter()); + + final ShutdownHookCleanUp shutdownHookCleanUp = classLoaderLeakPreventorFactory.getCleanUp(ShutdownHookCleanUp.class); + shutdownHookCleanUp.setExecuteShutdownHooks(executeShutdownHooks); + shutdownHookCleanUp.setShutdownHookWaitMs(shutdownHookWaitMs); + + final StopThreadsCleanUp stopThreadsCleanUp = classLoaderLeakPreventorFactory.getCleanUp(StopThreadsCleanUp.class); + stopThreadsCleanUp.setStopThreads(stopThreads); + stopThreadsCleanUp.setStopTimerThreads(stopTimerThreads); + stopThreadsCleanUp.setThreadWaitMs(threadWaitMs); + + // remove awt and imageio cleanup + classLoaderLeakPreventorFactory.removePreInitiator(AwtToolkitInitiator.class); + classLoaderLeakPreventorFactory.removePreInitiator(SunAwtAppContextInitiator.class); + classLoaderLeakPreventorFactory.removeCleanUp(IIOServiceProviderCleanUp.class); + classLoaderLeakPreventorFactory.removePreInitiator(Java2dRenderQueueInitiator.class); + classLoaderLeakPreventorFactory.removePreInitiator(Java2dDisposerInitiator.class); + + // the MBeanCleanUp causes a Exception and we use no mbeans + classLoaderLeakPreventorFactory.removeCleanUp(MBeanCleanUp.class); + + return classLoaderLeakPreventorFactory; + } + + @VisibleForTesting + void setClassLoaderAppendListener(ClassLoaderAppendListener classLoaderAppendListener) { + this.classLoaderAppendListener = classLoaderAppendListener; + } + + @Override + protected void shutdownClassLoaders() { + ClassLoaderAndPreventor clap = classLoaders.poll(); + while (clap != null) { + clap.shutdown(); + clap = classLoaders.poll(); + } + // be sure it is realy empty + classLoaders.clear(); + classLoaders = new ArrayDeque<>(); + } + + @Override + protected T initAndAppend(T originalClassLoader) { + LOG.debug("init classloader {}", originalClassLoader); + T classLoader = classLoaderAppendListener.apply(originalClassLoader); + + ClassLoaderLeakPreventor preventor = classLoaderLeakPreventorFactory.newLeakPreventor(classLoader); + preventor.runPreClassLoaderInitiators(); + classLoaders.push(new ClassLoaderAndPreventor(classLoader, preventor)); + + return classLoader; + } + + interface ClassLoaderAppendListener { + C apply(C classLoader); + } + + private static 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(); + 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); + } + } + } + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/classloading/SimpleClassLoaderLifeCycle.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/classloading/SimpleClassLoaderLifeCycle.java new file mode 100644 index 0000000000..9ea80e0592 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/classloading/SimpleClassLoaderLifeCycle.java @@ -0,0 +1,56 @@ +package sonia.scm.lifecycle.classloading; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.Closeable; +import java.io.IOException; +import java.util.ArrayDeque; +import java.util.Deque; + +/** + * Creates and shutdown SCM-Manager ClassLoaders without ClassLoader leak detection. + */ +class SimpleClassLoaderLifeCycle extends ClassLoaderLifeCycle { + + static final String NAME = "simple"; + + private static final Logger LOG = LoggerFactory.getLogger(SimpleClassLoaderLifeCycle.class); + + private Deque classLoaders = new ArrayDeque<>(); + + SimpleClassLoaderLifeCycle(ClassLoader webappClassLoader) { + super(webappClassLoader); + } + + @Override + protected T initAndAppend(T classLoader) { + LOG.debug("init classloader {}", classLoader); + classLoaders.push(classLoader); + return classLoader; + } + + @Override + protected void shutdownClassLoaders() { + ClassLoader classLoader = classLoaders.poll(); + while (classLoader != null) { + shutdown(classLoader); + classLoader = classLoaders.poll(); + } + // be sure it is realy empty + classLoaders.clear(); + classLoaders = new ArrayDeque<>(); + } + + private void shutdown(ClassLoader classLoader) { + LOG.debug("shutdown classloader {}", classLoader); + if (classLoader instanceof Closeable) { + LOG.trace("close classloader {}", classLoader); + try { + ((Closeable) classLoader).close(); + } catch (IOException e) { + LOG.warn("failed to close classloader", e); + } + } + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/rest/ObjectMapperProviderTest.java b/scm-webapp/src/test/java/sonia/scm/api/rest/ObjectMapperProviderTest.java new file mode 100644 index 0000000000..0676961d06 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/rest/ObjectMapperProviderTest.java @@ -0,0 +1,24 @@ +package sonia.scm.api.rest; + +import com.fasterxml.jackson.core.JsonProcessingException; +import org.junit.jupiter.api.Test; + +import java.time.ZoneId; +import java.time.ZonedDateTime; + +import static org.assertj.core.api.Assertions.assertThat; + +class ObjectMapperProviderTest { + + private final ObjectMapperProvider provider = new ObjectMapperProvider(); + + @Test + void shouldFormatInstantAsISO8601() throws JsonProcessingException { + ZoneId zone = ZoneId.of("Europe/Berlin"); + ZonedDateTime date = ZonedDateTime.of(2020, 2, 4, 15, 21, 42, 0, zone); + + String value = provider.get().writeValueAsString(date.toInstant()); + assertThat(value).isEqualTo("\"2020-02-04T14:21:42Z\""); + } + +} diff --git a/scm-webapp/src/test/java/sonia/scm/lifecycle/classloading/ClassLoaderLifeCycleTest.java b/scm-webapp/src/test/java/sonia/scm/lifecycle/classloading/ClassLoaderLifeCycleTest.java index a8f37777d7..619b09940e 100644 --- a/scm-webapp/src/test/java/sonia/scm/lifecycle/classloading/ClassLoaderLifeCycleTest.java +++ b/scm-webapp/src/test/java/sonia/scm/lifecycle/classloading/ClassLoaderLifeCycleTest.java @@ -1,116 +1,25 @@ package sonia.scm.lifecycle.classloading; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import se.jiderhamn.classloader.leak.prevention.ClassLoaderLeakPreventor; -import se.jiderhamn.classloader.leak.prevention.ClassLoaderLeakPreventorFactory; - -import java.io.Closeable; -import java.io.IOException; -import java.net.URL; -import java.net.URLClassLoader; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; -@ExtendWith(MockitoExtension.class) class ClassLoaderLifeCycleTest { - @Mock - private ClassLoaderLeakPreventorFactory classLoaderLeakPreventorFactory; - - @Mock - private ClassLoaderLeakPreventor classLoaderLeakPreventor; - @Test - void shouldThrowIllegalStateExceptionWithoutInit() { - ClassLoaderLifeCycle lifeCycle = ClassLoaderLifeCycle.create(); - assertThrows(IllegalStateException.class, lifeCycle::getBootstrapClassLoader); + void shouldCreateSimpleClassLoader() { + System.setProperty(ClassLoaderLifeCycle.PROPERTY, SimpleClassLoaderLifeCycle.NAME); + try { + ClassLoaderLifeCycle classLoaderLifeCycle = ClassLoaderLifeCycle.create(); + assertThat(classLoaderLifeCycle).isInstanceOf(SimpleClassLoaderLifeCycle.class); + } finally { + System.clearProperty(ClassLoaderLifeCycle.PROPERTY); + } } @Test - void shouldThrowIllegalStateExceptionAfterShutdown() { - ClassLoaderLifeCycle lifeCycle = createMockedLifeCycle(); - lifeCycle.initialize(); - - lifeCycle.shutdown(); - assertThrows(IllegalStateException.class, lifeCycle::getBootstrapClassLoader); + void shouldCreateDefaultClassLoader() { + ClassLoaderLifeCycle classLoaderLifeCycle = ClassLoaderLifeCycle.create(); + assertThat(classLoaderLifeCycle).isInstanceOf(ClassLoaderLifeCycleWithLeakPrevention.class); } - - @Test - void shouldCreateBootstrapClassLoaderOnInit() { - ClassLoaderLifeCycle lifeCycle = ClassLoaderLifeCycle.create(); - lifeCycle.initialize(); - - assertThat(lifeCycle.getBootstrapClassLoader()).isNotNull(); - } - - @Test - void shouldCallTheLeakPreventor() { - ClassLoaderLifeCycle lifeCycle = createMockedLifeCycle(); - - lifeCycle.initialize(); - verify(classLoaderLeakPreventor, times(2)).runPreClassLoaderInitiators(); - - lifeCycle.createChildFirstPluginClassLoader(new URL[0], null, "a"); - lifeCycle.createPluginClassLoader(new URL[0], null, "b"); - verify(classLoaderLeakPreventor, times(4)).runPreClassLoaderInitiators(); - - lifeCycle.shutdown(); - verify(classLoaderLeakPreventor, times(4)).runCleanUps(); - } - - @Test - void shouldCloseCloseableClassLoaders() throws IOException { - // we use URLClassLoader, because we must be sure that the classloader is closable - URLClassLoader webappClassLoader = spy(new URLClassLoader(new URL[0], Thread.currentThread().getContextClassLoader())); - - ClassLoaderLifeCycle lifeCycle = createMockedLifeCycle(webappClassLoader); - lifeCycle.setClassLoaderAppendListener(new ClassLoaderLifeCycle.ClassLoaderAppendListener() { - @Override - public C apply(C classLoader) { - return spy(classLoader); - } - }); - lifeCycle.initialize(); - - ClassLoader pluginA = lifeCycle.createChildFirstPluginClassLoader(new URL[0], null, "a"); - ClassLoader pluginB = lifeCycle.createPluginClassLoader(new URL[0], null, "b"); - - lifeCycle.shutdown(); - - closed(pluginB); - closed(pluginA); - - neverClosed(webappClassLoader); - } - - private void neverClosed(Object object) throws IOException { - Closeable closeable = closeable(object); - verify(closeable, never()).close(); - } - - private void closed(Object object) throws IOException { - Closeable closeable = closeable(object); - verify(closeable).close(); - } - - private Closeable closeable(Object object) { - assertThat(object).isInstanceOf(Closeable.class); - return (Closeable) object; - } - - private ClassLoaderLifeCycle createMockedLifeCycle() { - return createMockedLifeCycle(Thread.currentThread().getContextClassLoader()); - } - - private ClassLoaderLifeCycle createMockedLifeCycle(ClassLoader classLoader) { - when(classLoaderLeakPreventorFactory.newLeakPreventor(any(ClassLoader.class))).thenReturn(classLoaderLeakPreventor); - return new ClassLoaderLifeCycle(classLoader, classLoaderLeakPreventorFactory); - } - } diff --git a/scm-webapp/src/test/java/sonia/scm/lifecycle/classloading/ClassLoaderLifeCycleWithLeakPreventionTest.java b/scm-webapp/src/test/java/sonia/scm/lifecycle/classloading/ClassLoaderLifeCycleWithLeakPreventionTest.java new file mode 100644 index 0000000000..9c76fbfd00 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/lifecycle/classloading/ClassLoaderLifeCycleWithLeakPreventionTest.java @@ -0,0 +1,116 @@ +package sonia.scm.lifecycle.classloading; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import se.jiderhamn.classloader.leak.prevention.ClassLoaderLeakPreventor; +import se.jiderhamn.classloader.leak.prevention.ClassLoaderLeakPreventorFactory; + +import java.io.Closeable; +import java.io.IOException; +import java.net.URL; +import java.net.URLClassLoader; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class ClassLoaderLifeCycleWithLeakPreventionTest { + + @Mock + private ClassLoaderLeakPreventorFactory classLoaderLeakPreventorFactory; + + @Mock + private ClassLoaderLeakPreventor classLoaderLeakPreventor; + + @Test + void shouldThrowIllegalStateExceptionWithoutInit() { + ClassLoaderLifeCycleWithLeakPrevention lifeCycle = new ClassLoaderLifeCycleWithLeakPrevention(Thread.currentThread().getContextClassLoader()); + assertThrows(IllegalStateException.class, lifeCycle::getBootstrapClassLoader); + } + + @Test + void shouldThrowIllegalStateExceptionAfterShutdown() { + ClassLoaderLifeCycleWithLeakPrevention lifeCycle = createMockedLifeCycle(); + lifeCycle.initialize(); + + lifeCycle.shutdown(); + assertThrows(IllegalStateException.class, lifeCycle::getBootstrapClassLoader); + } + + @Test + void shouldCreateBootstrapClassLoaderOnInit() { + ClassLoaderLifeCycleWithLeakPrevention lifeCycle = new ClassLoaderLifeCycleWithLeakPrevention(Thread.currentThread().getContextClassLoader()); + lifeCycle.initialize(); + + assertThat(lifeCycle.getBootstrapClassLoader()).isNotNull(); + } + + @Test + void shouldCallTheLeakPreventor() { + ClassLoaderLifeCycleWithLeakPrevention lifeCycle = createMockedLifeCycle(); + + lifeCycle.initialize(); + verify(classLoaderLeakPreventor, times(1)).runPreClassLoaderInitiators(); + + lifeCycle.createChildFirstPluginClassLoader(new URL[0], null, "a"); + lifeCycle.createPluginClassLoader(new URL[0], null, "b"); + verify(classLoaderLeakPreventor, times(3)).runPreClassLoaderInitiators(); + + lifeCycle.shutdown(); + verify(classLoaderLeakPreventor, times(3)).runCleanUps(); + } + + @Test + void shouldCloseCloseableClassLoaders() throws IOException { + // we use URLClassLoader, because we must be sure that the classloader is closable + URLClassLoader webappClassLoader = spy(new URLClassLoader(new URL[0], Thread.currentThread().getContextClassLoader())); + + ClassLoaderLifeCycleWithLeakPrevention lifeCycle = createMockedLifeCycle(webappClassLoader); + lifeCycle.setClassLoaderAppendListener(new ClassLoaderLifeCycleWithLeakPrevention.ClassLoaderAppendListener() { + @Override + public C apply(C classLoader) { + return spy(classLoader); + } + }); + lifeCycle.initialize(); + + ClassLoader pluginA = lifeCycle.createChildFirstPluginClassLoader(new URL[0], null, "a"); + ClassLoader pluginB = lifeCycle.createPluginClassLoader(new URL[0], null, "b"); + + lifeCycle.shutdown(); + + closed(pluginB); + closed(pluginA); + + neverClosed(webappClassLoader); + } + + private void neverClosed(Object object) throws IOException { + Closeable closeable = closeable(object); + verify(closeable, never()).close(); + } + + private void closed(Object object) throws IOException { + Closeable closeable = closeable(object); + verify(closeable).close(); + } + + private Closeable closeable(Object object) { + assertThat(object).isInstanceOf(Closeable.class); + return (Closeable) object; + } + + private ClassLoaderLifeCycleWithLeakPrevention createMockedLifeCycle() { + return createMockedLifeCycle(Thread.currentThread().getContextClassLoader()); + } + + private ClassLoaderLifeCycleWithLeakPrevention createMockedLifeCycle(ClassLoader classLoader) { + when(classLoaderLeakPreventorFactory.newLeakPreventor(any(ClassLoader.class))).thenReturn(classLoaderLeakPreventor); + return new ClassLoaderLifeCycleWithLeakPrevention(classLoader, classLoaderLeakPreventorFactory); + } + +} diff --git a/scm-webapp/src/test/java/sonia/scm/lifecycle/classloading/SimpleClassLoaderLifeCycleTest.java b/scm-webapp/src/test/java/sonia/scm/lifecycle/classloading/SimpleClassLoaderLifeCycleTest.java new file mode 100644 index 0000000000..a40d0fb353 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/lifecycle/classloading/SimpleClassLoaderLifeCycleTest.java @@ -0,0 +1,37 @@ +package sonia.scm.lifecycle.classloading; + +import org.junit.jupiter.api.Test; + +import java.io.Closeable; + +import static org.assertj.core.api.Assertions.assertThat; + +class SimpleClassLoaderLifeCycleTest { + + @Test + void shouldCloseClosableClassLoaderOnShutdown() { + SimpleClassLoaderLifeCycle lifeCycle = new SimpleClassLoaderLifeCycle(Thread.currentThread().getContextClassLoader()); + lifeCycle.initialize(); + + ClosableClassLoader classLoader = new ClosableClassLoader(); + lifeCycle.initAndAppend(classLoader); + + lifeCycle.shutdown(); + + assertThat(classLoader.closed).isTrue(); + } + + private static class ClosableClassLoader extends ClassLoader implements Closeable { + + private boolean closed = false; + + public ClosableClassLoader() { + super(); + } + + @Override + public void close() { + closed = true; + } + } +}