Skip to content
Snippets Groups Projects
Commit 4c6820a9 authored by OZGCloud's avatar OZGCloud
Browse files

Merge pull request 'OZG-2737 OZG-3072 fix adding Links by Annotations' (#115)...

Merge pull request 'OZG-2737 OZG-3072 fix adding Links by Annotations' (#115) from OZG-3072_FixLinks into master

Reviewed-on: https://git.ozg-sh.de/mgm/goofy/pulls/115
parents 3b016eba f7fb7785
Branches
Tags
No related merge requests found
......@@ -18,9 +18,8 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize;
@JsonSerialize(using = LinkedUserProfileResourceSerializer.class)
@JsonDeserialize(using = LinkedUserProfileResourceDeserializer.class)
public @interface LinkedUserProfileResource {
Class<? extends IdExtractor<Object>> extractor()
default ToStringExtractor.class;
Class<? extends IdExtractor<Object>> extractor() default ToStringExtractor.class;
Class<? extends ObjectBuilder<Object>> builder() default IdBuilder.class;
}
......@@ -4,6 +4,8 @@ import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
......@@ -22,6 +24,7 @@ import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.server.mvc.WebMvcLinkBuilder;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
......@@ -29,7 +32,7 @@ import lombok.extern.log4j.Log4j2;
@Log4j2
public class ModelBuilder<T> {
private static final Map<Class<?>, List<Field>> ANNOTATED_FIELDS = new ConcurrentHashMap<>();
private static final Map<Class<?>, Map<Object, List<Field>>> ANNOTATED_FIELDS_BY_ANNOTATION = new ConcurrentHashMap<>();
private final T entity;
private final EntityModel<T> model;
......@@ -88,7 +91,9 @@ public class ModelBuilder<T> {
EntityModel<T> buildedModel = Objects.isNull(model) ? EntityModel.of(entity) : model;
buildedModel = buildedModel.add(filteredLinks);
addLinkByAnnotationIfMissing(buildedModel, LinkedResource.class, LinkedUserProfileResource.class);
addLinkByLinkedResourceAnnotationIfMissing(buildedModel);
addLinkByLinkedUserProfileResourceAnnotationIfMissing(buildedModel);
return applyMapper(buildedModel);
}
......@@ -102,29 +107,50 @@ public class ModelBuilder<T> {
return result;
}
private T getEntity() {
return Optional.ofNullable(entity == null ? model.getContent() : entity)
.orElseThrow(() -> new IllegalStateException("Entity must not null for ModelBuilding"));
private void addLinkByLinkedResourceAnnotationIfMissing(EntityModel<T> resource) {
getFields(LinkedResource.class).stream()
.filter(field -> shouldAddLink(resource, field))
.forEach(field -> handleLinkedResourceField(resource, field));
}
@SafeVarargs
private void addLinkByAnnotationIfMissing(EntityModel<T> resource, Class<? extends Annotation>... annotationClasses) {
Arrays.stream(annotationClasses).forEach(annotation -> {
var fields = ANNOTATED_FIELDS.get(getEntity().getClass());
if (CollectionUtils.isEmpty(fields)) {
fields = FieldUtils.getFieldsListWithAnnotation(getEntity().getClass(), annotation);
ANNOTATED_FIELDS.put(getEntity().getClass(), fields);
private void handleLinkedResourceField(EntityModel<T> resource, Field field) {
getEntityFieldValue(field).ifPresent(val -> resource
.add(WebMvcLinkBuilder.linkTo(field.getAnnotation(LinkedResource.class).controllerClass()).slash(val)
.withRel(sanitizeName(field.getName()))));
}
private void addLinkByLinkedUserProfileResourceAnnotationIfMissing(EntityModel<T> resource) {
getFields(LinkedUserProfileResource.class).stream()
.filter(field -> shouldAddLink(resource, field))
.forEach(field -> handleLinkedUserProfileResourceField(resource, field));
}
private void handleLinkedUserProfileResourceField(EntityModel<T> resource, Field field) {
getEntityFieldValue(field).ifPresent(val -> resource.add(Link.of(UserProfileUrlProvider.getUrl(val)).withRel(sanitizeName(field.getName()))));
}
private boolean shouldAddLink(EntityModel<T> resource, Field field) {
return !(field.getType().isArray() || Collection.class.isAssignableFrom(field.getType()) || resource.hasLink(sanitizeName(field.getName())));
}
fields.forEach(field -> {
String fieldName = sanitizeName(field.getName());
if (field.getType().isArray() || Collection.class.isAssignableFrom(field.getType()) || resource.hasLink(fieldName)) {
return;
private List<Field> getFields(Class<? extends Annotation> annotationClass) {
var fields = Optional.ofNullable(ANNOTATED_FIELDS_BY_ANNOTATION.get(getEntity().getClass()))
.map(fieldsByAnnotation -> fieldsByAnnotation.get(annotationClass))
.orElseGet(() -> Collections.emptyList());
if (CollectionUtils.isEmpty(fields)) {
fields = FieldUtils.getFieldsListWithAnnotation(getEntity().getClass(), annotationClass);
updateFields(annotationClass, fields);
}
return fields;
}
getEntityFieldValue(field).ifPresent(val -> resource.add(Link.of(UserProfileUrlProvider.getUrl(val)).withRel(fieldName)));
});
});
private void updateFields(Class<? extends Annotation> annotationClass, List<Field> fields) {
var annotationMap = Optional.ofNullable(ANNOTATED_FIELDS_BY_ANNOTATION.get(getEntity().getClass())).orElseGet(HashMap::new);
annotationMap.put(annotationClass, fields);
ANNOTATED_FIELDS_BY_ANNOTATION.put(annotationClass, annotationMap);
}
private String sanitizeName(String fieldName) {
......@@ -146,6 +172,11 @@ public class ModelBuilder<T> {
return Optional.empty();
}
private T getEntity() {
return Optional.ofNullable(entity == null ? model.getContent() : entity)
.orElseThrow(() -> new IllegalStateException("Entity must not null for ModelBuilding"));
}
@RequiredArgsConstructor
public class ConditionalLinkAdder {
......
package de.itvsh.goofy.common;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.util.UUID;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.springframework.context.ApplicationContext;
import org.springframework.core.env.Environment;
import org.springframework.web.bind.annotation.RequestMapping;
import lombok.Builder;
class ModelBuilderTest {
@DisplayName("Add link by annotation if missing")
@Nested
class TestAddLinkByAnnotationIfMissing {
private static final String USER_MANAGER_URL = "http://localhost";
private static final String USER_MANAGER_PROFILE_TEMPLATE = "/api/profile/%s";
private UserProfileUrlProvider provider = new UserProfileUrlProvider();
@Mock
private ApplicationContext context;
@Mock
private Environment env;
private TestEntity entity = TestEntityTestFactory.create();
@BeforeEach
void mockEnvironment() {
when(env.getProperty(UserProfileUrlProvider.URL_ROOT_KEY)).thenReturn(USER_MANAGER_URL);
when(env.getProperty(UserProfileUrlProvider.USER_PROFILES_TEMPLATE_KEY)).thenReturn(USER_MANAGER_PROFILE_TEMPLATE);
when(context.getEnvironment()).thenReturn(env);
provider.setApplicationContext(context);
}
@Test
void shouldHaveAddLinkByLinkedResource() {
var model = ModelBuilder.fromEntity(entity).buildModel();
assertThat(model.getLink(TestController.FILE_REL).get().getHref()).isEqualTo("/api/test/" + TestEntityTestFactory.FILE);
}
@Test
void shouldHaveAddLinkByLinkedUserProfileResource() {
var model = ModelBuilder.fromEntity(entity).buildModel();
assertThat(model.getLink(TestController.USER_REL).get().getHref())
.isEqualTo(String.format(USER_MANAGER_URL + USER_MANAGER_PROFILE_TEMPLATE, TestEntityTestFactory.USER));
}
}
}
@Builder
class TestEntity {
@LinkedResource(controllerClass = TestController.class)
private String file;
@LinkedUserProfileResource
private String user;
}
@RequestMapping("/api/test")
class TestController {
static final String USER_REL = "user";
static final String FILE_REL = "file";
}
class TestEntityTestFactory {
static final String USER = UUID.randomUUID().toString();
static final String FILE = UUID.randomUUID().toString();
public static TestEntity create() {
return TestEntity.builder()
.file(FILE)
.user(USER)
.build();
}
}
......@@ -74,6 +74,8 @@ public class WiedervorlageControllerITCase {
var response = callEndpoint();
response.andDo(print())
.andExpect(jsonPath("$._links.self").exists())
.andExpect(jsonPath("$._links.createdBy").exists())
.andExpect(jsonPath("$.createdAt").value(WiedervorlageTestFactory.CREATED_AT_STR))
.andExpect(jsonPath("$.frist").value(WiedervorlageTestFactory.FRIST.atStartOfDay().format(DateTimeFormatter.ISO_DATE_TIME)));
}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment