added optional store parameter for ClassLoader and adapter

This commit is contained in:
Sebastian Sdorra
2020-04-28 14:10:11 +02:00
parent cf4c1092b9
commit 38eea06312
4 changed files with 162 additions and 14 deletions

View File

@@ -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<T> extends StoreParameters {
Class<T> getType();
Optional<ClassLoader> getClassLoader();
@SuppressWarnings("java:S1452") // we could not provide generic type, because we don't know it here
Set<XmlAdapter<?,?>> getAdapters();
}

View File

@@ -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<T,S> {
private final Class<T> type;
private String name;
private String repositoryId;
private ClassLoader classLoader;
private Set<XmlAdapter<?, ?>> adapters;
public Optional<ClassLoader> getClassLoader() {
return Optional.ofNullable(classLoader);
}
}
public class OptionalRepositoryBuilder {
private final Set<XmlAdapter<?,?>> 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<T,S> {
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);
}
}

View File

@@ -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<T> {
@@ -50,38 +53,53 @@ final class TypedStoreContext<T> {
}
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<T> 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<Marshaller> consumer) {
Marshaller marshaller = createMarshaller();
ClassLoader contextClassLoader = null;
Optional<ClassLoader> 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<Unmarshaller> consumer) {
Unmarshaller unmarshaller = createUnmarshaller();
ClassLoader contextClassLoader = null;
Optional<ClassLoader> 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<T> {
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<T> {
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<T> {
@FunctionalInterface
interface ThrowingConsumer<T> {
@SuppressWarnings("java:S112") // we need to throw Exception here
void consume(T item) throws Exception;
}

View File

@@ -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<Sample> params = params(Sample.class);
when(params.getClassLoader()).thenReturn(Optional.of(classLoader));
TypedStoreContext<Sample> context = TypedStoreContext.of(params);
AtomicReference<ClassLoader> 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<SampleWithAdapter> params = params(SampleWithAdapter.class);
when(params.getAdapters()).thenReturn(Collections.singleton(new AppendingAdapter("!")));
TypedStoreContext<SampleWithAdapter> 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 <T> TypedStoreContext<T> context(Class<T> 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<String, String> {
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;
}
}
}