mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-03-09 22:00:20 +01:00
added new api to simplify the process of appending links to json responses
This commit is contained in:
@@ -3,7 +3,7 @@ package sonia.scm.api.v2.resources;
|
||||
import de.otto.edison.hal.HalRepresentation;
|
||||
import org.mapstruct.Mapping;
|
||||
|
||||
public abstract class BaseMapper<T, D extends HalRepresentation> implements InstantAttributeMapper {
|
||||
public abstract class BaseMapper<T, D extends HalRepresentation> extends LinkAppenderMapper implements InstantAttributeMapper {
|
||||
|
||||
@Mapping(target = "attributes", ignore = true) // We do not map HAL attributes
|
||||
public abstract D map(T modelObject);
|
||||
|
||||
10
scm-core/src/main/java/sonia/scm/api/v2/resources/Index.java
Normal file
10
scm-core/src/main/java/sonia/scm/api/v2/resources/Index.java
Normal file
@@ -0,0 +1,10 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
/**
|
||||
* The {@link Index} object can be used to register a {@link LinkEnricher} for the index resource.
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public final class Index {
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
/**
|
||||
* The {@link LinkAppender} can be used within an {@link LinkEnricher} to append hateoas links to a json response.
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public interface LinkAppender {
|
||||
|
||||
/**
|
||||
* Appends one link to the json response.
|
||||
*
|
||||
* @param rel name of relation
|
||||
* @param href link uri
|
||||
*/
|
||||
void appendOne(String rel, String href);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
public class LinkAppenderMapper {
|
||||
|
||||
@Inject
|
||||
private LinkEnricherRegistry registry;
|
||||
|
||||
@VisibleForTesting
|
||||
void setRegistry(LinkEnricherRegistry registry) {
|
||||
this.registry = registry;
|
||||
}
|
||||
|
||||
protected void appendLinks(LinkAppender appender, Object source, Object... contextEntries) {
|
||||
// null check is only their to not break existing tests
|
||||
if (registry != null) {
|
||||
|
||||
Object[] ctx = new Object[contextEntries.length + 1];
|
||||
ctx[0] = source;
|
||||
for (int i = 0; i < contextEntries.length; i++) {
|
||||
ctx[i + 1] = contextEntries[i];
|
||||
}
|
||||
|
||||
LinkEnricherContext context = LinkEnricherContext.of(ctx);
|
||||
|
||||
Iterable<LinkEnricher> enrichers = registry.allByType(source.getClass());
|
||||
for (LinkEnricher enricher : enrichers) {
|
||||
enricher.enrich(context, appender);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
/**
|
||||
* A {@link LinkEnricher} can be used to append hateoas links to a specific json response.
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface LinkEnricher {
|
||||
|
||||
/**
|
||||
* Enriches the response with hateoas links.
|
||||
*
|
||||
* @param context contains the source for the json mapping and related objects
|
||||
* @param appender can be used to append links to the json response
|
||||
*/
|
||||
void enrich(LinkEnricherContext context, LinkAppender appender);
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Context object for the {@link LinkEnricher}. The context holds the source object for the json and all related
|
||||
* objects, which can be useful for the link creation.
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public final class LinkEnricherContext {
|
||||
|
||||
private final Map<Class, Object> instanceMap;
|
||||
|
||||
private LinkEnricherContext(Map<Class,Object> instanceMap) {
|
||||
this.instanceMap = instanceMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a context with the given entries
|
||||
*
|
||||
* @param instances entries of the context
|
||||
*
|
||||
* @return context of given entries
|
||||
*/
|
||||
public static LinkEnricherContext of(Object... instances) {
|
||||
ImmutableMap.Builder<Class, Object> builder = ImmutableMap.builder();
|
||||
for (Object instance : instances) {
|
||||
builder.put(instance.getClass(), instance);
|
||||
}
|
||||
return new LinkEnricherContext(builder.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the registered object from the context. The method will return an empty optional, if no object with the
|
||||
* given type was registered.
|
||||
*
|
||||
* @param type type of instance
|
||||
* @param <T> type of instance
|
||||
* @return optional instance
|
||||
*/
|
||||
public <T> Optional<T> oneByType(Class<T> type) {
|
||||
Object instance = instanceMap.get(type);
|
||||
if (instance != null) {
|
||||
return Optional.of(type.cast(instance));
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the registered object from the context, but throws an {@link NoSuchElementException} if the type was not
|
||||
* registered.
|
||||
*
|
||||
* @param type type of instance
|
||||
* @param <T> type of instance
|
||||
* @return instance
|
||||
*/
|
||||
public <T> T oneRequireByType(Class<T> type) {
|
||||
return oneByType(type).get();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.google.common.collect.HashMultimap;
|
||||
import com.google.common.collect.Multimap;
|
||||
import sonia.scm.plugin.Extension;
|
||||
|
||||
import javax.inject.Singleton;
|
||||
|
||||
/**
|
||||
* The {@link LinkEnricherRegistry} is responsible for binding {@link LinkEnricher} instances to their source types.
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Extension
|
||||
@Singleton
|
||||
public final class LinkEnricherRegistry {
|
||||
|
||||
private final Multimap<Class, LinkEnricher> enrichers = HashMultimap.create();
|
||||
|
||||
/**
|
||||
* Registers a new {@link LinkEnricher} for the given source type.
|
||||
*
|
||||
* @param sourceType type of json mapping source
|
||||
* @param enricher link enricher instance
|
||||
*/
|
||||
public void register(Class sourceType, LinkEnricher enricher) {
|
||||
enrichers.put(sourceType, enricher);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all registered {@link LinkEnricher} for the given type.
|
||||
*
|
||||
* @param sourceType type of json mapping source
|
||||
* @return all registered enrichers
|
||||
*/
|
||||
public Iterable<LinkEnricher> allByType(Class sourceType) {
|
||||
return enrichers.get(sourceType);
|
||||
}
|
||||
}
|
||||
10
scm-core/src/main/java/sonia/scm/api/v2/resources/Me.java
Normal file
10
scm-core/src/main/java/sonia/scm/api/v2/resources/Me.java
Normal file
@@ -0,0 +1,10 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
/**
|
||||
* The {@link Me} object can be used to register a {@link LinkEnricher} for the me resource.
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public final class Me {
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class LinkAppenderMapperTest {
|
||||
|
||||
@Mock
|
||||
private LinkAppender appender;
|
||||
|
||||
private LinkEnricherRegistry registry;
|
||||
private LinkAppenderMapper mapper;
|
||||
|
||||
@BeforeEach
|
||||
void beforeEach() {
|
||||
registry = new LinkEnricherRegistry();
|
||||
mapper = new LinkAppenderMapper();
|
||||
mapper.setRegistry(registry);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAppendSimpleLink() {
|
||||
registry.register(String.class, (ctx, appender) -> appender.appendOne("42", "https://hitchhiker.com"));
|
||||
|
||||
mapper.appendLinks(appender, "hello");
|
||||
|
||||
verify(appender).appendOne("42", "https://hitchhiker.com");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCallMultipleEnrichers() {
|
||||
registry.register(String.class, (ctx, appender) -> appender.appendOne("42", "https://hitchhiker.com"));
|
||||
registry.register(String.class, (ctx, appender) -> appender.appendOne("21", "https://scm.hitchhiker.com"));
|
||||
|
||||
mapper.appendLinks(appender, "hello");
|
||||
|
||||
verify(appender).appendOne("42", "https://hitchhiker.com");
|
||||
verify(appender).appendOne("21", "https://scm.hitchhiker.com");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAppendLinkByUsingSourceFromContext() {
|
||||
registry.register(String.class, (ctx, appender) -> {
|
||||
Optional<String> rel = ctx.oneByType(String.class);
|
||||
appender.appendOne(rel.get(), "https://hitchhiker.com");
|
||||
});
|
||||
|
||||
mapper.appendLinks(appender, "42");
|
||||
|
||||
verify(appender).appendOne("42", "https://hitchhiker.com");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAppendLinkByUsingMultipleContextEntries() {
|
||||
registry.register(Integer.class, (ctx, appender) -> {
|
||||
Optional<Integer> rel = ctx.oneByType(Integer.class);
|
||||
Optional<String> href = ctx.oneByType(String.class);
|
||||
appender.appendOne(String.valueOf(rel.get()), href.get());
|
||||
});
|
||||
|
||||
mapper.appendLinks(appender, Integer.valueOf(42), "https://hitchhiker.com");
|
||||
|
||||
verify(appender).appendOne("42", "https://hitchhiker.com");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.NoSuchElementException;
|
||||
|
||||
class LinkEnricherContextTest {
|
||||
|
||||
@Test
|
||||
void shouldCreateContextFromSingleObject() {
|
||||
LinkEnricherContext context = LinkEnricherContext.of("hello");
|
||||
assertThat(context.oneByType(String.class)).contains("hello");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateContextFromMultipleObjects() {
|
||||
LinkEnricherContext context = LinkEnricherContext.of("hello", Integer.valueOf(42), Long.valueOf(21L));
|
||||
assertThat(context.oneByType(String.class)).contains("hello");
|
||||
assertThat(context.oneByType(Integer.class)).contains(42);
|
||||
assertThat(context.oneByType(Long.class)).contains(21L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnEmptyOptionalForUnknownTypes() {
|
||||
LinkEnricherContext context = LinkEnricherContext.of();
|
||||
assertThat(context.oneByType(String.class)).isNotPresent();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnRequiredObject() {
|
||||
LinkEnricherContext context = LinkEnricherContext.of("hello");
|
||||
assertThat(context.oneRequireByType(String.class)).isEqualTo("hello");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowAnNoSuchElementExceptionForUnknownTypes() {
|
||||
LinkEnricherContext context = LinkEnricherContext.of();
|
||||
assertThrows(NoSuchElementException.class, () -> context.oneRequireByType(String.class));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class LinkEnricherRegistryTest {
|
||||
|
||||
private LinkEnricherRegistry registry;
|
||||
|
||||
@BeforeEach
|
||||
void setUpObjectUnderTest() {
|
||||
registry = new LinkEnricherRegistry();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRegisterTheEnricher() {
|
||||
SampleLinkEnricher enricher = new SampleLinkEnricher();
|
||||
registry.register(String.class, enricher);
|
||||
|
||||
Iterable<LinkEnricher> enrichers = registry.allByType(String.class);
|
||||
assertThat(enrichers).containsOnly(enricher);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRegisterMultipleEnrichers() {
|
||||
SampleLinkEnricher one = new SampleLinkEnricher();
|
||||
registry.register(String.class, one);
|
||||
|
||||
SampleLinkEnricher two = new SampleLinkEnricher();
|
||||
registry.register(String.class, two);
|
||||
|
||||
Iterable<LinkEnricher> enrichers = registry.allByType(String.class);
|
||||
assertThat(enrichers).containsOnly(one, two);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRegisterEnrichersForDifferentTypes() {
|
||||
SampleLinkEnricher one = new SampleLinkEnricher();
|
||||
registry.register(String.class, one);
|
||||
|
||||
SampleLinkEnricher two = new SampleLinkEnricher();
|
||||
registry.register(Integer.class, two);
|
||||
|
||||
Iterable<LinkEnricher> enrichers = registry.allByType(String.class);
|
||||
assertThat(enrichers).containsOnly(one);
|
||||
|
||||
enrichers = registry.allByType(Integer.class);
|
||||
assertThat(enrichers).containsOnly(two);
|
||||
}
|
||||
|
||||
private static class SampleLinkEnricher implements LinkEnricher {
|
||||
@Override
|
||||
public void enrich(LinkEnricherContext context, LinkAppender appender) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user