Skip to content

Commit

Permalink
Merge pull request #98 from xenit-eu/curie-provider
Browse files Browse the repository at this point in the history
Add custom CurieProvider
  • Loading branch information
vierbergenlars authored Aug 2, 2023
2 parents 9e9ed3e + fb2ed10 commit 3ce7eb2
Show file tree
Hide file tree
Showing 22 changed files with 375 additions and 14 deletions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package com.contentgrid.spring.boot.autoconfigure.data.web;

import com.contentgrid.spring.data.rest.affordances.ContentGridSpringDataRestAffordancesConfiguration;
import com.contentgrid.spring.data.rest.hal.ContentGridCurieConfiguration;
import com.contentgrid.spring.data.rest.hal.CurieProviderCustomizer;
import com.contentgrid.spring.data.rest.webmvc.ContentGridSpringDataRestProfileConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.hateoas.HypermediaAutoConfiguration;
import org.springframework.boot.context.properties.ConfigurationProperties;
Expand All @@ -15,8 +18,11 @@

@Configuration
@ConditionalOnClass({ContentGridSpringDataRestConfiguration.class, RepositoryRestMvcConfiguration.class})
@Import({ContentGridSpringDataRestConfiguration.class, ContentGridSpringDataRestProfileConfiguration.class,
ContentGridSpringDataRestAffordancesConfiguration.class})
@Import({
ContentGridSpringDataRestConfiguration.class,
ContentGridSpringDataRestProfileConfiguration.class,
ContentGridSpringDataRestAffordancesConfiguration.class
})
@AutoConfigureAfter(HypermediaAutoConfiguration.class)
public class ContentGridSpringDataRestAutoConfiguration {

Expand All @@ -26,4 +32,10 @@ ContentGridRestProperties contentGridRestProperties() {
return new ContentGridRestProperties();
}

@ConditionalOnBean(CurieProviderCustomizer.class)
@Import(ContentGridCurieConfiguration.class)
static class CurieAutoConfiguration {

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,19 @@

import static org.assertj.core.api.Assertions.assertThat;

import com.contentgrid.spring.data.rest.hal.CurieProviderCustomizer;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.data.rest.RepositoryRestMvcAutoConfiguration;
import org.springframework.boot.context.annotation.UserConfigurations;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.rest.webmvc.ContentGridSpringDataRestConfiguration;
import org.springframework.data.rest.webmvc.DelegatingRepositoryPropertyReferenceController;
import org.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguration;
import org.springframework.hateoas.mediatype.hal.CurieProvider;
import org.springframework.hateoas.mediatype.hal.HalConfiguration;
import org.springframework.hateoas.mediatype.hal.forms.HalFormsConfiguration;

Expand All @@ -26,6 +31,7 @@ void checkContentGridSpringDataRest() {
contextRunner.run(context -> {
assertThat(context).hasSingleBean(DelegatingRepositoryPropertyReferenceController.class);
assertThat(context).hasBean("repositoryPropertyReferenceController");
assertThat(context).doesNotHaveBean(CurieProvider.class);
assertThat(context).hasNotFailed();
});
}
Expand Down Expand Up @@ -62,4 +68,23 @@ void when_springDataRestWebmvc_isNotOnClasspath() {
assertThat(context).hasNotFailed();
});
}

@Test
void when_curieProviderCustomizer_isPresent() {
contextRunner
.withConfiguration(UserConfigurations.of(CurieCustomizerConfiguration.class))
.run(context -> {
assertThat(context).hasSingleBean(CurieProvider.class);
assertThat(context).hasNotFailed();
});
}

@Configuration(proxyBeanMethods = false)
private static class CurieCustomizerConfiguration {
@Bean
CurieProviderCustomizer myCurieProviderCustomizer() {
return CurieProviderCustomizer.register("test", "https://test.invalid/{rel}");
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.contentgrid.spring.data.rest.hal;

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.hateoas.mediatype.hal.CurieProvider;

@Configuration(proxyBeanMethods = false)
public class ContentGridCurieConfiguration {

@Bean
CurieProvider contentGridCurieProvider(ObjectProvider<CurieProviderCustomizer> customizers) {
CurieProviderBuilder curieProvider = new ContentGridCurieProvider();

for (CurieProviderCustomizer customizer : customizers) {
curieProvider = customizer.customize(curieProvider);
}

return curieProvider.build();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package com.contentgrid.spring.data.rest.hal;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.springframework.hateoas.IanaLinkRelations;
import org.springframework.hateoas.IanaUriSchemes;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.LinkRelation;
import org.springframework.hateoas.Links;
import org.springframework.hateoas.UriTemplate;
import org.springframework.hateoas.mediatype.hal.CurieProvider;
import org.springframework.hateoas.mediatype.hal.HalLinkRelation;

@RequiredArgsConstructor
class ContentGridCurieProvider implements CurieProvider, CurieProviderBuilder {

private final Map<String, UriTemplate> curies;

public ContentGridCurieProvider() {
this(Map.of());
}

@Override
public HalLinkRelation getNamespacedRelFrom(Link link) {
return getNamespacedRelFor(link.getRel());
}

@Override
public HalLinkRelation getNamespacedRelFor(LinkRelation rel) {
assertRegisteredCurie(rel);
return HalLinkRelation.of(rel);
}

@Override
public Collection<?> getCurieInformation(Links links) {
return curies.entrySet().stream()
.map(it -> createCurieLink(it.getKey(), it.getValue()))
.toList();
}

private Link createCurieLink(String name, UriTemplate template) {
return Link.of(
template,
HalLinkRelation.CURIES
).withName(name);
}

@Override
public CurieProviderBuilder withCurie(String prefix, UriTemplate template) {
if(curies.containsKey(prefix)) {
throw new IllegalArgumentException("CURIE prefix '%s' is already registered with template '%s' and can not be re-registered with template '%s'.".formatted(
prefix,
curies.get(prefix),
template
));
}
if(IanaUriSchemes.isIanaUriScheme(prefix)) {
throw new IllegalArgumentException("CURIE prefix '%s' can not be an IANA-registered URI scheme.".formatted(prefix));
}
var curies = new HashMap<>(this.curies);
curies.put(prefix, template);
return new ContentGridCurieProvider(curies);
}

@Override
public CurieProvider build() {
return this;
}

private void assertRegisteredCurie(LinkRelation rel) {
var relation = rel.value();
int firstColonIndex = relation.indexOf(':');

String curie = firstColonIndex == -1 ? null : relation.substring(0, firstColonIndex);

if(curie == null) {
// Not curie -> need to check if it's a registered link relation
if(!IanaLinkRelations.isIanaRel(relation)) {
throw new IllegalArgumentException("Relation '%s' is not an IANA-registered relation".formatted(relation));
}
return;
}

if(IanaUriSchemes.isIanaUriScheme(curie)) {
// Not a curie, but a RFC 5988 #4.2a extension relation type
return;
}

if(!curies.containsKey(curie)) {
throw new IllegalArgumentException("Relation '%s' uses CURIE that is not registered".formatted(relation));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.contentgrid.spring.data.rest.hal;

import org.springframework.hateoas.UriTemplate;
import org.springframework.hateoas.mediatype.hal.CurieProvider;

/**
* Fluent builder for {@link CurieProvider}
*/
public interface CurieProviderBuilder {

/**
* Adds a mapping from CURIE prefix to a {@link UriTemplate} for resolving the CURIE against
* @param prefix CURIE prefix
* @param template Template to use to resolve the CURIE
* @return Copy with new curie mapping applied
*/
CurieProviderBuilder withCurie(String prefix, UriTemplate template);

/**
* Builds a {@link CurieProvider} with the mappings specified in the builder
* @return An immutable {@link CurieProvider} instance
*/
CurieProvider build();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.contentgrid.spring.data.rest.hal;

import org.springframework.hateoas.UriTemplate;

/**
* Customizer interface for {@link CurieProviderBuilder}.
* <p>
* By implementing this interface and exposing them as beans, they will automatically be applied to the default {@link org.springframework.hateoas.mediatype.hal.CurieProvider}
*/
@FunctionalInterface
public interface CurieProviderCustomizer {

/**
* Customize the {@link CurieProviderBuilder}
* @param builder current builder
* @return builder with the desired customizations applied
*/
CurieProviderBuilder customize(CurieProviderBuilder builder);

/**
* Register a mapping between CURIE prefix and a URI template
*
* @param curiePrefix The CURIE prefix
* @param template The URI template to use
* @return customizer that registers a CURIE prefix mapping
*
* @see CurieProviderBuilder#withCurie(String, UriTemplate) for the builder method
* @see #register(String, String) for a shortcut that does not require a {@link UriTemplate} instance
*/
static CurieProviderCustomizer register(String curiePrefix, UriTemplate template) {
return builder -> builder.withCurie(curiePrefix, template);
}

/**
* Register a mapping between CURIE prefix and a URI template
*
* @param curiePrefix The CURIE prefix
* @param template The URI template to use
* @return customizer that registers a CURIE prefix mapping
*
* @see CurieProviderBuilder#withCurie(String, UriTemplate) for the builder method
* @see #register(String, UriTemplate) for passing a {@link UriTemplate} directly
*/
static CurieProviderCustomizer register(String curiePrefix, String template) {
return register(curiePrefix, UriTemplate.of(template));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ void templatesAddedOnCollectionResource() throws Exception {
.andExpect(MockMvcResultMatchers.content().json("""
{
_embedded: {
customers: [
"d:customers": [
{
_links: {
self: {
Expand Down Expand Up @@ -137,7 +137,7 @@ void templatesAddedOnCollectionResource() throws Exception {
// No top-level _templates are present
.andExpect(MockMvcResultMatchers.jsonPath("$.keys()", Matchers.not(Matchers.contains("_templates"))))
// The templates of the embedded object only contain a default and a delete template
.andExpect(MockMvcResultMatchers.jsonPath("$._embedded.customers[0]._templates.keys()", Matchers.containsInAnyOrder("default", "delete")));
.andExpect(MockMvcResultMatchers.jsonPath("$._embedded.['d:customers'][0]._templates.keys()", Matchers.containsInAnyOrder("default", "delete")));
;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.contentgrid.spring.data.rest.hal;

import static org.assertj.core.api.Assertions.assertThat;

import com.contentgrid.spring.data.rest.hal.ContentGridCurieConfigurationTest.CurieProviderCustomizers;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.Links;
import org.springframework.hateoas.UriTemplate;
import org.springframework.hateoas.mediatype.hal.CurieProvider;
import org.springframework.hateoas.mediatype.hal.HalLinkRelation;
import org.springframework.test.context.ContextConfiguration;

@SpringBootTest
@ContextConfiguration(classes = {
ContentGridCurieConfiguration.class,
CurieProviderCustomizers.class
})
@AutoConfigureMockMvc(printOnlyOnFailure = false)
class ContentGridCurieConfigurationTest {

@TestConfiguration(proxyBeanMethods = false)
static class CurieProviderCustomizers {
@Bean
CurieProviderCustomizer curieProviderTestCustomizer() {
return builder -> builder.withCurie("test", UriTemplate.of("https://example.com/rels/{x}"));
}

@Bean
CurieProviderCustomizer curieProviderExtCustomizer() {
return builder -> builder.withCurie("ext", UriTemplate.of("https://ext.invalid/{rel}"));
}
}

@Test
void customizersApplied(ApplicationContext context) {
var curies = context.getBean(CurieProvider.class).getCurieInformation(Links.NONE);

assertThat(curies).asInstanceOf(InstanceOfAssertFactories.list(Link.class)).containsExactlyInAnyOrder(
Link.of("https://example.com/rels/{x}", HalLinkRelation.CURIES).withName("test"),
Link.of("https://ext.invalid/{rel}", HalLinkRelation.CURIES).withName("ext")
);
}


}
Loading

0 comments on commit 3ce7eb2

Please sign in to comment.