From 38eea06312d1f67c9efe37f054744c964c9ee39e Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Tue, 28 Apr 2020 14:10:11 +0200 Subject: [PATCH] added optional store parameter for ClassLoader and adapter --- .../sonia/scm/store/TypedStoreParameters.java | 9 +++ .../store/TypedStoreParametersBuilder.java | 41 ++++++++++ .../sonia/scm/store/TypedStoreContext.java | 51 +++++++++---- .../scm/store/TypedStoreContextTest.java | 75 ++++++++++++++++++- 4 files changed, 162 insertions(+), 14 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/store/TypedStoreParameters.java b/scm-core/src/main/java/sonia/scm/store/TypedStoreParameters.java index 0a28b0d455..4f80078743 100644 --- a/scm-core/src/main/java/sonia/scm/store/TypedStoreParameters.java +++ b/scm-core/src/main/java/sonia/scm/store/TypedStoreParameters.java @@ -24,6 +24,10 @@ package sonia.scm.store; +import javax.xml.bind.annotation.adapters.XmlAdapter; +import java.util.Optional; +import java.util.Set; + /** * The fields of the {@link TypedStoreParameters} are used from the {@link ConfigurationStoreFactory}, * {@link ConfigurationEntryStoreFactory} and {@link DataStoreFactory} to create a type safe store. @@ -35,4 +39,9 @@ public interface TypedStoreParameters extends StoreParameters { Class getType(); + Optional getClassLoader(); + + @SuppressWarnings("java:S1452") // we could not provide generic type, because we don't know it here + Set> getAdapters(); + } diff --git a/scm-core/src/main/java/sonia/scm/store/TypedStoreParametersBuilder.java b/scm-core/src/main/java/sonia/scm/store/TypedStoreParametersBuilder.java index e590ce40a0..efda2187a6 100644 --- a/scm-core/src/main/java/sonia/scm/store/TypedStoreParametersBuilder.java +++ b/scm-core/src/main/java/sonia/scm/store/TypedStoreParametersBuilder.java @@ -24,11 +24,18 @@ package sonia.scm.store; +import com.google.common.collect.ImmutableSet; import lombok.AccessLevel; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.Setter; +import sonia.scm.plugin.PluginLoader; import sonia.scm.repository.Repository; + +import javax.xml.bind.annotation.adapters.XmlAdapter; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; import java.util.function.Function; /** @@ -64,10 +71,18 @@ public final class TypedStoreParametersBuilder { private final Class type; private String name; private String repositoryId; + private ClassLoader classLoader; + private Set> adapters; + + public Optional getClassLoader() { + return Optional.ofNullable(classLoader); + } } public class OptionalRepositoryBuilder { + private final Set> adapters = new HashSet<>(); + /** * Use this to create or get a store for a specific repository. This step is optional. If you * want to have a global store, omit this. @@ -90,11 +105,37 @@ public final class TypedStoreParametersBuilder { return this; } + /** + * Sets the {@link ClassLoader} which is used as context class loader during marshaling and unmarshalling. + * This is especially useful for storing class objects which come from an unknown source, in this case the + * UberClassLoader ({@link PluginLoader#getUberClassLoader()} could be used for the store. + * + * @param classLoader classLoader for the context + * + * @return {@code this} + */ + public OptionalRepositoryBuilder withClassLoader(ClassLoader classLoader) { + parameters.setClassLoader(classLoader); + return this; + } + + /** + * Sets an instance of an {@link XmlAdapter}. + * + * @param adapter adapter + * @return {@code this} + */ + public OptionalRepositoryBuilder withAdapter(XmlAdapter adapter) { + adapters.add(adapter); + return this; + } + /** * Creates or gets the store with the given name and (if specified) the given repository. If no * repository is given, the store will be global. */ public S build(){ + parameters.setAdapters(ImmutableSet.copyOf(adapters)); return factory.apply(parameters); } } diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/TypedStoreContext.java b/scm-dao-xml/src/main/java/sonia/scm/store/TypedStoreContext.java index 5963804931..2664444e55 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/store/TypedStoreContext.java +++ b/scm-dao-xml/src/main/java/sonia/scm/store/TypedStoreContext.java @@ -28,7 +28,10 @@ import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; import javax.xml.bind.Unmarshaller; +import javax.xml.bind.annotation.adapters.XmlAdapter; import java.io.File; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; final class TypedStoreContext { @@ -50,38 +53,53 @@ final class TypedStoreContext { } T unmarshall(File file) { - Unmarshaller unmarshaller = createUnmarshaller(); - try { - return parameters.getType().cast(unmarshaller.unmarshal(file)); - } catch (JAXBException e) { - throw new StoreException("failed to unmarshall " + file); - } + AtomicReference ref = new AtomicReference<>(); + withUnmarshaller(unmarshaller -> { + T value = parameters.getType().cast(unmarshaller.unmarshal(file)); + ref.set(value); + }); + return ref.get(); } void marshal(Object object, File file) { - Marshaller marshaller = createMarshaller(); - try { - marshaller.marshal(object, file); - } catch (JAXBException e) { - throw new StoreException("failed to marshall " + object + " to " + file); - } + withMarshaller(marshaller -> marshaller.marshal(object, file)); } void withMarshaller(ThrowingConsumer consumer) { Marshaller marshaller = createMarshaller(); + ClassLoader contextClassLoader = null; + Optional classLoader = parameters.getClassLoader(); + if (classLoader.isPresent()) { + contextClassLoader = Thread.currentThread().getContextClassLoader(); + Thread.currentThread().setContextClassLoader(classLoader.get()); + } try { consumer.consume(marshaller); } catch (Exception e) { - throw new StoreException("failure during work with marshaller"); + throw new StoreException("failure during work with marshaller", e); + } finally { + if (contextClassLoader != null) { + Thread.currentThread().setContextClassLoader(contextClassLoader); + } } } void withUnmarshaller(ThrowingConsumer consumer) { Unmarshaller unmarshaller = createUnmarshaller(); + ClassLoader contextClassLoader = null; + Optional classLoader = parameters.getClassLoader(); + if (classLoader.isPresent()) { + contextClassLoader = Thread.currentThread().getContextClassLoader(); + Thread.currentThread().setContextClassLoader(classLoader.get()); + } try { consumer.consume(unmarshaller); } catch (Exception e) { throw new StoreException("failure during work with unmarshaller", e); + } finally { + if (contextClassLoader != null) { + Thread.currentThread().setContextClassLoader(contextClassLoader); + } } } @@ -89,6 +107,9 @@ final class TypedStoreContext { try { Marshaller marshaller = jaxbContext.createMarshaller(); marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); + for (XmlAdapter adapter : parameters.getAdapters()) { + marshaller.setAdapter(adapter); + } return marshaller; } catch (JAXBException e) { throw new StoreException("could not create marshaller", e); @@ -98,6 +119,9 @@ final class TypedStoreContext { private Unmarshaller createUnmarshaller() { try { Unmarshaller unmarshaller = jaxbContext.createUnmarshaller(); + for (XmlAdapter adapter : parameters.getAdapters()) { + unmarshaller.setAdapter(adapter); + } return unmarshaller; } catch (JAXBException e) { throw new StoreException("could not create unmarshaller", e); @@ -106,6 +130,7 @@ final class TypedStoreContext { @FunctionalInterface interface ThrowingConsumer { + @SuppressWarnings("java:S112") // we need to throw Exception here void consume(T item) throws Exception; } diff --git a/scm-dao-xml/src/test/java/sonia/scm/store/TypedStoreContextTest.java b/scm-dao-xml/src/test/java/sonia/scm/store/TypedStoreContextTest.java index a790bd51d9..d29fd44298 100644 --- a/scm-dao-xml/src/test/java/sonia/scm/store/TypedStoreContextTest.java +++ b/scm-dao-xml/src/test/java/sonia/scm/store/TypedStoreContextTest.java @@ -32,9 +32,14 @@ import org.mockito.junit.jupiter.MockitoExtension; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlRootElement; - +import javax.xml.bind.annotation.adapters.XmlAdapter; +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; import java.io.File; +import java.net.URL; +import java.net.URLClassLoader; import java.nio.file.Path; +import java.util.Collections; +import java.util.Optional; import java.util.concurrent.atomic.AtomicReference; import static org.assertj.core.api.Assertions.assertThat; @@ -75,6 +80,41 @@ class TypedStoreContextTest { assertThat(ref.get().value).isEqualTo("wow"); } + @Test + void shouldSetContextClassLoader() { + ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); + + ClassLoader classLoader = new URLClassLoader(new URL[0], contextClassLoader); + + TypedStoreParameters params = params(Sample.class); + when(params.getClassLoader()).thenReturn(Optional.of(classLoader)); + + TypedStoreContext context = TypedStoreContext.of(params); + + AtomicReference ref = new AtomicReference<>(); + context.withMarshaller(marshaller -> { + ref.set(Thread.currentThread().getContextClassLoader()); + }); + + assertThat(ref.get()).isSameAs(classLoader); + assertThat(Thread.currentThread().getContextClassLoader()).isSameAs(contextClassLoader); + } + + @Test + void shouldConfigureAdapter(@TempDirectory.TempDir Path tempDir) { + TypedStoreParameters params = params(SampleWithAdapter.class); + when(params.getAdapters()).thenReturn(Collections.singleton(new AppendingAdapter("!"))); + + TypedStoreContext context = TypedStoreContext.of(params); + + File file = tempDir.resolve("test.xml").toFile(); + context.marshal(new SampleWithAdapter("awesome"), file); + SampleWithAdapter sample = context.unmarshall(file); + + // one ! should be added for marshal and one for unmarshal + assertThat(sample.value).isEqualTo("awesome!!"); + } + private TypedStoreContext context(Class type) { return TypedStoreContext.of(params(type)); } @@ -98,4 +138,37 @@ class TypedStoreContextTest { } } + @XmlRootElement + @XmlAccessorType(XmlAccessType.FIELD) + public static class SampleWithAdapter { + @XmlJavaTypeAdapter(AppendingAdapter.class) + private String value; + + public SampleWithAdapter() { + } + + public SampleWithAdapter(String value) { + this.value = value; + } + } + + public static class AppendingAdapter extends XmlAdapter { + + private final String suffix; + + public AppendingAdapter(String suffix) { + this.suffix = suffix; + } + + @Override + public String unmarshal(String v) { + return v + suffix; + } + + @Override + public String marshal(String v) { + return v + suffix; + } + } + }