Skip to content
Snippets Groups Projects
Commit 26b296f2 authored by Felix Reichenbach's avatar Felix Reichenbach
Browse files

OZG-3936 add LinkedResourceProcessor

parent fc6b1e7a
No related branches found
No related tags found
1 merge request!16Ozg 3936 refactor user profile url provider
package de.ozgcloud.alfa.common;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Collection;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Stream;
import org.apache.commons.lang3.StringUtils;
import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.server.RepresentationModelProcessor;
import org.springframework.hateoas.server.mvc.WebMvcLinkBuilder;
import org.springframework.stereotype.Component;
import de.ozgcloud.alfa.common.user.UserManagerUrlProvider;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
@Log4j2
@Component
@RequiredArgsConstructor
public class LinkedResourceProcessor implements RepresentationModelProcessor<EntityModel<?>> {
private final UserManagerUrlProvider userManagerUrlProvider;
@Override
public EntityModel<?> process(EntityModel<?> model) {
addLinkByLinkedResourceAnnotationIfMissing(model);
addLinkByLinkedUserProfileResourceAnnotationIfMissing(model);
return model;
}
void addLinkByLinkedResourceAnnotationIfMissing(EntityModel<?> model) {
getFields(LinkedResource.class, model.getContent())
.filter(field -> shouldAddLink(model, field))
.forEach(field -> addLinkForLinkedResourceField(model, field));
}
void addLinkForLinkedResourceField(EntityModel<?> model, Field field) {
getEntityFieldValue(model.getContent(), field).map(Object::toString).filter(StringUtils::isNotBlank)
.ifPresent(val -> model
.add(WebMvcLinkBuilder.linkTo(field.getAnnotation(LinkedResource.class).controllerClass()).slash(val)
.withRel(trimIdSuffix(field.getName()))));
}
void addLinkByLinkedUserProfileResourceAnnotationIfMissing(EntityModel<?> resource) {
getFields(LinkedUserProfileResource.class, resource.getContent())
.filter(field -> shouldAddLink(resource, field))
.forEach(field -> addLinkForLinkedUserProfileResourceField(resource, field));
}
Stream<Field> getFields(Class<? extends Annotation> annotationClass, Object content) {
if (Objects.isNull(content)) {
return Stream.empty();
}
return Arrays.stream(content.getClass().getDeclaredFields())
.filter(field -> field.isAnnotationPresent(annotationClass));
}
boolean shouldAddLink(EntityModel<?> resource, Field field) {
return !(field.getType().isArray() || Collection.class.isAssignableFrom(field.getType()) || resource.hasLink(trimIdSuffix(field.getName())));
}
void addLinkForLinkedUserProfileResourceField(EntityModel<?> model, Field field) {
getEntityFieldValue(model.getContent(), field).map(Object::toString).filter(StringUtils::isNotBlank)
.ifPresent(value -> addUserProfileLink(model, field, value));
}
private void addUserProfileLink(EntityModel<?> model, Field field, String value) {
Optional.ofNullable(userManagerUrlProvider.getUserProfileTemplate()).filter(StringUtils::isNotBlank)
.ifPresent(template -> model.add(Link.of(template.formatted(value)).withRel(trimIdSuffix(field.getName()))));
}
private Optional<Object> getEntityFieldValue(Object content, Field field) {
try {
field.setAccessible(true);
Optional<Object> value = Optional.ofNullable(field.get(content));
field.setAccessible(false);
return value;
} catch (IllegalArgumentException | IllegalAccessException e) {
LOG.warn("Cannot access field value of LinkedResource field.", e);
}
return Optional.empty();
}
private String trimIdSuffix(String fieldName) {
if (fieldName.endsWith("Id")) {
return fieldName.substring(0, fieldName.indexOf("Id"));
}
return fieldName;
}
}
...@@ -27,21 +27,21 @@ import java.util.Optional; ...@@ -27,21 +27,21 @@ import java.util.Optional;
import java.util.function.Predicate; import java.util.function.Predicate;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.util.UriComponentsBuilder; import org.springframework.web.util.UriComponentsBuilder;
import de.ozgcloud.alfa.postfach.PostfachMail; import de.ozgcloud.alfa.postfach.PostfachMail;
import lombok.RequiredArgsConstructor;
@Service @Service
@RequiredArgsConstructor
public class UserManagerUrlProvider { public class UserManagerUrlProvider {
public static final String SYSTEM_USER_IDENTIFIER = "system"; public static final String SYSTEM_USER_IDENTIFIER = "system";
public static final Predicate<PostfachMail> SENT_BY_CLIENT_USER = postfachNachricht -> Optional.ofNullable(postfachNachricht.getCreatedBy()) public static final Predicate<PostfachMail> SENT_BY_CLIENT_USER = postfachNachricht -> Optional.ofNullable(postfachNachricht.getCreatedBy())
.map(createdBy -> !createdBy.toString().startsWith(SYSTEM_USER_IDENTIFIER)).orElse(false); .map(createdBy -> !createdBy.toString().startsWith(SYSTEM_USER_IDENTIFIER)).orElse(false);
@Autowired private final UserManagerProperties userManagerProperties;
private UserManagerProperties userManagerProperties;
/** only for building links */ /** only for building links */
public String getUserProfileTemplate() { public String getUserProfileTemplate() {
......
package de.ozgcloud.alfa.common;
import java.util.UUID;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.hateoas.EntityModel;
import org.springframework.test.web.servlet.assertj.MockMvcTester;
import org.springframework.test.web.servlet.assertj.MvcTestResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.WebApplicationContext;
import de.ozgcloud.common.test.ITCase;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.SneakyThrows;
@ITCase
class LinkedResourceProcessorITCase {
@Autowired
private WebApplicationContext wac;
private MockMvcTester mockMvc;
@BeforeEach
void setup() {
this.mockMvc = MockMvcTester.from(this.wac);
}
@Test
@SneakyThrows
void shouldHaveAddLinkByLinkedResource() {
var result = doRequest();
result.assertThat().bodyJson().extractingPath("$._links.id.href")
.isEqualTo("http://localhost" + TestIdController.PATH + "/" + TestEntityTestFactory.ID);
}
@Test
@SneakyThrows
void shouldHaveUserProfileLink() {
var result = doRequest();
result.assertThat().bodyJson().extractingPath("$._links.user.href")
.isEqualTo("https://localhost/api/userProfiles/" + TestEntityTestFactory.USER);
}
private MvcTestResult doRequest() {
return mockMvc.get().uri(TestEntityController.PATH).exchange();
}
@Builder
@Getter
@EqualsAndHashCode
static class TestEntity {
@LinkedResource(controllerClass = TestIdController.class)
private String id;
private String foo;
@LinkedUserProfileResource
private String user;
}
@RequestMapping(TestEntityController.PATH)
@RestController
static class TestEntityController {
static final String PATH = "/api/entity";
@GetMapping
public EntityModel<TestEntity> getTestEntity() {
return EntityModel.of(TestEntityTestFactory.create());
}
}
@RequestMapping(TestIdController.PATH)
static class TestIdController {
static final String PATH = "/api/test";
static final String USER_REL = "user";
static final String ID_REL = "id";
}
static class TestEntityTestFactory {
static final String USER = UUID.randomUUID().toString();
static final String ID = UUID.randomUUID().toString();
public static TestEntity create() {
return createBuilder().build();
}
public static TestEntity.TestEntityBuilder createBuilder() {
return TestEntity.builder()
.id(ID)
.user(USER);
}
}
}
package de.ozgcloud.alfa.common;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.UUID;
import java.util.stream.Stream;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.NullAndEmptySource;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy;
import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.Link;
import org.springframework.web.bind.annotation.RequestMapping;
import com.thedeanda.lorem.LoremIpsum;
import de.ozgcloud.alfa.common.user.UserManagerUrlProvider;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.SneakyThrows;
class LinkedResourceProcessorTest {
@Spy
@InjectMocks
private LinkedResourceProcessor processor;
@Mock
private UserManagerUrlProvider userManagerUrlProvider;
@Nested
class TestProcess {
private final TestEntity entity = TestEntityTestFactory.create();
private final EntityModel<TestEntity> model = EntityModel.of(entity);
@BeforeEach
void mock() {
doNothing().when(processor).addLinkByLinkedResourceAnnotationIfMissing(any());
doNothing().when(processor).addLinkByLinkedUserProfileResourceAnnotationIfMissing(any());
}
@Test
void shouldReturnSameEntityModel() {
var result = processor.process(model);
assertThat(result).isSameAs(model);
}
@Test
void shouldCallAddLinkByLinkedResourceAnnotationIfMissing() {
processor.process(model);
verify(processor).addLinkByLinkedResourceAnnotationIfMissing(model);
}
@Test
void shouldCallAddLinkByLinkedUserProfileResourceAnnotationIfMissing() {
processor.process(model);
verify(processor).addLinkByLinkedUserProfileResourceAnnotationIfMissing(model);
}
}
@Nested
class TestAddLinkByLinkedResourceAnnotationIfMissing {
private final TestEntity entity = TestEntityTestFactory.create();
private final EntityModel<TestEntity> model = EntityModel.of(entity);
@Mock
private Field field;
@BeforeEach
void mock() {
doReturn(Stream.of(field)).when(processor).getFields(any(), any());
}
@Test
void shouldGetFieldsWithLinkedResourceAnnotation() {
doReturn(false).when(processor).shouldAddLink(any(), any());
processor.addLinkByLinkedResourceAnnotationIfMissing(model);
verify(processor).getFields(LinkedResource.class, entity);
}
@Test
void shouldCallShouldAddLink() {
doReturn(false).when(processor).shouldAddLink(any(), any());
processor.addLinkByLinkedResourceAnnotationIfMissing(model);
verify(processor).shouldAddLink(model, field);
}
@Test
void shouldCallAddLinkForLinkedResourceField() {
doReturn(true).when(processor).shouldAddLink(any(), any());
processor.addLinkByLinkedResourceAnnotationIfMissing(model);
verify(processor).addLinkForLinkedResourceField(model, field);
}
@Test
void shouldNotCallAddLinkForLinkedResourceField() {
doReturn(false).when(processor).shouldAddLink(any(), any());
processor.addLinkByLinkedResourceAnnotationIfMissing(model);
verify(processor, never()).addLinkForLinkedResourceField(any(), any());
}
}
@Nested
class TestAddLinkByLinkedUserProfileResourceAnnotationIfMissing {
private final TestEntity entity = TestEntityTestFactory.create();
private final EntityModel<TestEntity> model = EntityModel.of(entity);
@Mock
private Field field;
@BeforeEach
void mock() {
doReturn(Stream.of(field)).when(processor).getFields(any(), any());
}
@Test
void shouldGetFieldsWithLinkedUserProfileResourceAnnotation() {
doReturn(false).when(processor).shouldAddLink(any(), any());
processor.addLinkByLinkedUserProfileResourceAnnotationIfMissing(model);
verify(processor).getFields(LinkedUserProfileResource.class, entity);
}
@Test
void shouldCallShouldAddLink() {
doReturn(false).when(processor).shouldAddLink(any(), any());
processor.addLinkByLinkedUserProfileResourceAnnotationIfMissing(model);
verify(processor).shouldAddLink(model, field);
}
@Test
void shouldCallAddLinkForLinkedUserProfileResourceField() {
doReturn(true).when(processor).shouldAddLink(any(), any());
processor.addLinkByLinkedUserProfileResourceAnnotationIfMissing(model);
verify(processor).addLinkForLinkedUserProfileResourceField(model, field);
}
@Test
void shouldNotCallAddLinkForLinkedUserProfileResourceField() {
doReturn(false).when(processor).shouldAddLink(any(), any());
processor.addLinkByLinkedUserProfileResourceAnnotationIfMissing(model);
verify(processor, never()).addLinkForLinkedUserProfileResourceField(any(), any());
}
}
@Nested
class TestGetFields {
@Test
void shouldReturnEmptyStreamIfContentIsNull() {
var result = processor.getFields(LinkedResource.class, null);
assertThat(result).isEmpty();
}
@Test
void shouldReturnAllFieldsWithLinkedResourceAnnotation() {
var entity = TestEntityTestFactory.create();
var expectedFields = List.of(getField("linkedResource"), getField("testId"));
var result = processor.getFields(LinkedResource.class, entity);
assertThat(result).containsExactlyInAnyOrderElementsOf(expectedFields);
}
@Test
void shouldReturnAllFieldsWithLinkedUserProfileResource() {
var entity = TestEntityTestFactory.create();
var expectedFields = List.of(getField("user"), getField("differentUserId"));
var result = processor.getFields(LinkedUserProfileResource.class, entity);
assertThat(result).containsExactlyInAnyOrderElementsOf(expectedFields);
}
@SneakyThrows
private Field getField(String fieldName) {
return TestEntity.class.getDeclaredField(fieldName);
}
}
@Nested
class TestShouldAddLink {
private final TestEntity entity = TestEntityTestFactory.create();
private final EntityModel<TestEntity> model = EntityModel.of(entity);
@Test
void shouldReturnFalseIfFieldIsArray() {
var field = getField("arrayField");
var result = processor.shouldAddLink(model, field);
assertThat(result).isFalse();
}
@Test
void shouldReturnFalseIfFieldIsCollection() {
var field = getField("collectionField");
var result = processor.shouldAddLink(model, field);
assertThat(result).isFalse();
}
@Test
void shouldReturnFalseIfFieldHasLink() {
var field = getField("linkedResource");
model.add(Link.of(LoremIpsum.getInstance().getUrl()).withRel(TestIdController.LINKED_RESOURCE_REL));
var result = processor.shouldAddLink(model, field);
assertThat(result).isFalse();
}
@Test
void shouldReturnTrueOtherwise() {
var fieldsToBuildLinksFor = Arrays.stream(TestEntity.class.getDeclaredFields())
.filter(field -> !(field.getName().equals("collectionField") || field.getName().equals("arrayField")));
var result = fieldsToBuildLinksFor.map(field -> processor.shouldAddLink(model, field));
assertThat(result).isNotEmpty().allMatch(value -> value.equals(true));
}
@SneakyThrows
private Field getField(String fieldName) {
return TestEntity.class.getDeclaredField(fieldName);
}
}
@Nested
class TestAddLinkForLinkedResourceField {
@Test
void shouldAddLinkOfLinkedResource() {
var model = EntityModel.of(TestEntityTestFactory.create());
var field = getField("linkedResource");
processor.addLinkForLinkedResourceField(model, field);
assertThat(model.getLink(TestIdController.LINKED_RESOURCE_REL).get().getHref())
.isEqualTo(TestIdController.PATH + "/" + TestEntityTestFactory.LINKED_RESOURCE);
}
@Test
void shouldTrimIdSuffix() {
var model = EntityModel.of(TestEntityTestFactory.create());
var field = getField("testId");
processor.addLinkForLinkedResourceField(model, field);
assertThat(model.getLink(TestIdController.ID_REL).get().getHref())
.isEqualTo(TestIdController.PATH + "/" + TestEntityTestFactory.ID);
}
@ParameterizedTest
@NullAndEmptySource
void shouldNotAddLinkIfFieldValueIsNullOrBlank(String linkedResourceValue) {
var model = EntityModel.of(TestEntityTestFactory.createBuilder().linkedResource(linkedResourceValue).build());
var field = getField("linkedResource");
processor.addLinkForLinkedResourceField(model, field);
assertThat(model.getLink(TestIdController.LINKED_RESOURCE_REL)).isEmpty();
}
@SneakyThrows
private Field getField(String fieldName) {
return TestEntity.class.getDeclaredField(fieldName);
}
}
@Nested
class TestAddLinkForLinkedUserProfileResourceField {
private final String userProfileTemplate = LoremIpsum.getInstance().getUrl() + "/%s";
@Test
void shouldGetUserProfileTemplate() {
when(userManagerUrlProvider.getUserProfileTemplate()).thenReturn(userProfileTemplate);
var field = getField("user");
processor.addLinkForLinkedUserProfileResourceField(EntityModel.of(TestEntityTestFactory.create()), field);
verify(userManagerUrlProvider).getUserProfileTemplate();
}
@Test
void shouldAddLinkOfLinkedUserProfileResource() {
when(userManagerUrlProvider.getUserProfileTemplate()).thenReturn(userProfileTemplate);
var model = EntityModel.of(TestEntityTestFactory.create());
var field = getField("user");
processor.addLinkForLinkedUserProfileResourceField(model, field);
assertThat(model.getLink(TestIdController.USER_REL).get().getHref())
.isEqualTo(userProfileTemplate.formatted(TestEntityTestFactory.USER));
}
@Test
void shouldTrimIdSuffix() {
when(userManagerUrlProvider.getUserProfileTemplate()).thenReturn(userProfileTemplate);
var model = EntityModel.of(TestEntityTestFactory.create());
var field = getField("differentUserId");
processor.addLinkForLinkedUserProfileResourceField(model, field);
assertThat(model.getLink(TestIdController.DIFFERENT_USER_REL).get().getHref())
.isEqualTo(userProfileTemplate.formatted(TestEntityTestFactory.DIFFERENT_USER_ID));
}
@ParameterizedTest
@NullAndEmptySource
void shouldNotAddLinkIfFieldValueIsNullOrBlank(String userValue) {
var model = EntityModel.of(TestEntityTestFactory.createBuilder().user(userValue).build());
var field = getField("user");
processor.addLinkForLinkedUserProfileResourceField(model, field);
assertThat(model.getLink(TestIdController.USER_REL)).isEmpty();
}
@ParameterizedTest
@NullAndEmptySource
void shouldNotAddLinkIfUserManagerUrlIsNullOrBlank(String userManagerUrl) {
when(userManagerUrlProvider.getUserProfileTemplate()).thenReturn(userManagerUrl);
var model = EntityModel.of(TestEntityTestFactory.create());
var field = getField("user");
processor.addLinkForLinkedUserProfileResourceField(model, field);
assertThat(model.getLink(TestIdController.USER_REL)).isEmpty();
}
@SneakyThrows
private Field getField(String fieldName) {
return TestEntity.class.getDeclaredField(fieldName);
}
}
@Builder
@Getter
@EqualsAndHashCode
static class TestEntity {
@LinkedResource(controllerClass = TestIdController.class)
private String linkedResource;
@LinkedResource(controllerClass = TestIdController.class)
private String testId;
private String foo;
private Collection<String> collectionField;
private String[] arrayField;
@LinkedUserProfileResource
private String user;
@LinkedUserProfileResource
private String differentUserId;
}
@RequestMapping(TestIdController.PATH)
static class TestIdController {
static final String PATH = "/api/test";
static final String USER_REL = "user";
static final String DIFFERENT_USER_REL = "differentUser";
static final String ID_REL = "test";
static final String LINKED_RESOURCE_REL = "linkedResource";
}
static class TestEntityTestFactory {
static final String USER = UUID.randomUUID().toString();
static final String DIFFERENT_USER_ID = UUID.randomUUID().toString();
static final String ID = UUID.randomUUID().toString();
static final String LINKED_RESOURCE = UUID.randomUUID().toString();
public static TestEntity create() {
return createBuilder().build();
}
public static TestEntity.TestEntityBuilder createBuilder() {
return TestEntity.builder()
.testId(ID)
.linkedResource(LINKED_RESOURCE)
.user(USER)
.differentUserId(DIFFERENT_USER_ID)
.foo("bar");
}
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment