Skip to content

Commit

Permalink
work-in-progress
Browse files Browse the repository at this point in the history
  • Loading branch information
cgendreau committed Jan 8, 2025
1 parent 0203530 commit dd40446
Show file tree
Hide file tree
Showing 13 changed files with 299 additions and 29 deletions.
2 changes: 1 addition & 1 deletion dina-base-api/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@

<jsoup.version>1.18.1</jsoup.version>

<org.mapstruct.version>1.6.0</org.mapstruct.version>
<org.mapstruct.version>1.6.3</org.mapstruct.version>
<lombok-mapstruct-binding.version>0.2.0</lombok-mapstruct-binding.version>

<commons-io.version>2.17.0</commons-io.version>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,14 @@ public Class<?> getInternalRelationClass(Class<?> cls, String attribute) {
.orElse(null);
}

public InternalRelation getInternalRelation(Class<?> cls, String attribute) {
checkClassTracked(cls);
return resourceGraph.get(cls).getInternalRelations().stream()
.filter( i -> i.name.equalsIgnoreCase(attribute))
.findAny()
.orElse(null);
}

/**
* Return the class of the external relationship represented by the attribute.
* @param cls
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,13 @@
import ca.gc.aafc.dina.filter.QueryComponent;
import ca.gc.aafc.dina.filter.QueryStringParser;
import ca.gc.aafc.dina.filter.SimpleFilterHandlerV2;
import ca.gc.aafc.dina.jsonapi.JsonApiDocument;
import ca.gc.aafc.dina.mapper.DinaMapperV2;
import ca.gc.aafc.dina.mapper.DinaMappingRegistry;
import ca.gc.aafc.dina.security.auth.DinaAuthorizationService;
import ca.gc.aafc.dina.service.AuditService;
import ca.gc.aafc.dina.service.DinaService;
import ca.gc.aafc.dina.util.ReflectionUtils;

import static com.toedter.spring.hateoas.jsonapi.JsonApiModelBuilder.jsonApiModel;

Expand Down Expand Up @@ -521,13 +523,13 @@ public UUID create(D dto) {
* Relationships are not supported at the moment.
* @param patchDto
*/
public void update(JsonApiPartialPatchDto patchDto) {
public void update(JsonApiDocument patchDto) {

// We need to use Jackson for now here since MapStruct doesn't support setting
// values from Map<String, Object> yet.
// Reflection can't really be used since we won't know the type of the source
// and how to convert it.
D dto = objMapper.convertValue(patchDto.getMap(), resourceClass);
D dto = objMapper.convertValue(patchDto.getAttributes(), resourceClass);

// load entity
E entity = dinaService.findOne(patchDto.getId(), entityClass);
Expand All @@ -539,11 +541,39 @@ public void update(JsonApiPartialPatchDto patchDto) {
authorizationService.authorizeUpdate(entity);

// apply DTO on entity using the keys from patchDto
dinaMapper.patchEntity(entity, dto, patchDto.getPropertiesName(), null);
dinaMapper.patchEntity(entity, dto, patchDto.getData().getAttributesName(), null);

var relationships = patchDto.getRelationships();
for (var relationship : relationships.entrySet()) {
String relName = relationship.getKey();
DinaMappingRegistry.InternalRelation relation = registry.getInternalRelation(entityClass, relName);
var relObject = relationship.getValue();
// to-many
if(relObject.isCollection()) {
for(Object el : relObject.getDataAsCollection()) {
var resourceIdentifier = toResourceIdentifier(el);

}
} else { // to-one
var resourceIdentifier = toResourceIdentifier(relObject.getData());
dinaService.setRelationshipByNaturalIdReference(relation.getEntityType(),
resourceIdentifier.getId(), (r) -> {
try {
ReflectionUtils.getSetterMethod(relName, entityClass).invoke(entity, r);
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
});
}
}

dinaService.update(entity);
}

private JsonApiDocument.ResourceIdentifier toResourceIdentifier(Object obj) {
return objMapper.convertValue(obj, JsonApiDocument.ResourceIdentifier.class);
}

/**
* Delete the resource identified by the provided identifier.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import ca.gc.aafc.dina.jpa.BaseDAO;
import ca.gc.aafc.dina.jpa.PredicateSupplier;
import ca.gc.aafc.dina.validation.ValidationErrorsHelper;

import java.util.function.Consumer;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;

Expand Down Expand Up @@ -98,6 +100,11 @@ public E update(E entity) {
return baseDAO.update(entity);
}

@Override
public <T> void setRelationshipByNaturalIdReference(Class<T> entityClass, Object naturalId, Consumer<T> objConsumer) {
baseDAO.setRelationshipByNaturalIdReference(entityClass, naturalId, objConsumer);
}

/**
* Remove the given entity from the database.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import ca.gc.aafc.dina.entity.DinaEntity;
import ca.gc.aafc.dina.jpa.PredicateSupplier;

import java.util.function.Consumer;
import lombok.NonNull;

import javax.persistence.criteria.CriteriaBuilder;
Expand Down Expand Up @@ -37,6 +39,8 @@ public interface DinaService<E extends DinaEntity> {
*/
E update(E entity);

<T> void setRelationshipByNaturalIdReference(Class<T> entityClass, Object naturalId, Consumer<T> objConsumer);

/**
* Deletes a given entity from the data source
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package ca.gc.aafc.dina.util;

import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Map;

import org.apache.commons.beanutils.PropertyUtils;

/**
* Mostly a utility class to simplify usage of reflection and {@link org.apache.commons.beanutils.BeanUtils}
*/
public final class ReflectionUtils {

private ReflectionUtils() {
// utility class
}

/**
* Creates a new instance of the provided class by using the default constructor.
*
* @param clazz
* @return instance of clazz
*/
public static <T> T newInstance(Class<T> clazz) {
try {
return clazz.getConstructor().newInstance();
} catch (InstantiationException | IllegalAccessException | InvocationTargetException |
NoSuchMethodException e) {
throw new RuntimeException(e);
}
}

public static Method getSetterMethod(String propertyName, Class<?> beanClass) {
try {
return new PropertyDescriptor(propertyName, beanClass).getWriteMethod();
} catch (IntrospectionException e) {
throw new RuntimeException(e);
}
}

/**
* Sets all provided attributes on the target object.
* As opposed to BeanUtils, this method will throw an exception if the attribute doesn't exist
* on the target.
* @param target
* @param attributes
* @throws IllegalArgumentException if attributes is not found or there is a type mismatch
*/
public static <T> void setAttributes(T target, Map<String, Object> attributes)
throws IllegalArgumentException {
for (Map.Entry<String, Object> attribute : attributes.entrySet()) {
try {
PropertyUtils.setProperty(target, attribute.getKey(), attribute.getValue());
} catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public final class ProjectDTO {

public static final String RESOURCE_TYPE = "Project";

@com.toedter.spring.hateoas.jsonapi.JsonApiId
@JsonApiId
@org.javers.core.metamodel.annotation.Id
@PropertyName("id")
Expand Down
4 changes: 4 additions & 0 deletions dina-base-api/src/test/java/ca/gc/aafc/dina/dto/TaskDTO.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,15 @@
@RelatedEntity(Task.class)
@TypeName(TaskDTO.RESOURCE_TYPE)
public final class TaskDTO {

public static final String RESOURCE_TYPE = "Task";

@com.toedter.spring.hateoas.jsonapi.JsonApiId
@JsonApiId
@org.javers.core.metamodel.annotation.Id
@PropertyName("id")
private UUID uuid;

private int powerLevel;

private Integer power;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,30 +1,53 @@
package ca.gc.aafc.dina.jsonapi;

import java.net.URI;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;
import javax.inject.Inject;
import javax.transaction.Transactional;
import lombok.Getter;

import org.apache.commons.lang3.RandomUtils;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.info.BuildProperties;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.RepresentationModel;
import org.springframework.http.HttpEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.fasterxml.jackson.databind.ObjectMapper;

import ca.gc.aafc.dina.dto.JsonApiPartialPatchDto;
import ca.gc.aafc.dina.dto.JsonApiPartialPatchDto2;
import ca.gc.aafc.dina.dto.PersonDTO;
import ca.gc.aafc.dina.dto.ProjectDTO;
import ca.gc.aafc.dina.dto.TaskDTO;
import ca.gc.aafc.dina.entity.Person;
import ca.gc.aafc.dina.entity.Project;
import ca.gc.aafc.dina.entity.Task;
import ca.gc.aafc.dina.mapper.PersonMapper;
import ca.gc.aafc.dina.mapper.ProjectDtoMapper;
import ca.gc.aafc.dina.mapper.TaskDtoMapper;
import ca.gc.aafc.dina.repository.DinaRepositoryV2;
import ca.gc.aafc.dina.security.auth.AllowAllAuthorizationService;
import ca.gc.aafc.dina.service.DinaService;
import ca.gc.aafc.dina.testsupport.BaseRestAssuredTest;
import ca.gc.aafc.dina.testsupport.PostgresTestContainerInitializer;
import ca.gc.aafc.dina.testsupport.jsonapi.JsonAPIRelationship;
import ca.gc.aafc.dina.testsupport.jsonapi.JsonAPITestHelper;

import static com.toedter.spring.hateoas.jsonapi.MediaTypes.JSON_API_VALUE;
Expand All @@ -34,24 +57,42 @@
properties = {"dev-user.enabled: true", "keycloak.enabled: false"},
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ContextConfiguration(initializers = { PostgresTestContainerInitializer.class })
@Import(DinaRepositoryV2IT.TestDynaBeanRepo.class)
@Import({DinaRepositoryV2IT.TestDynaBeanRepo.class, DinaRepositoryV2IT.RepoV2TestConfig.class})
public class DinaRepositoryV2IT extends BaseRestAssuredTest {

private static final String PATH = "dynabean";
private static final String PATH = "repo2";

@Inject
TestDynaBeanRepo dynaBeanRepo;


@Autowired
private DinaRepositoryV2<TaskDTO, Task> rep;

protected DinaRepositoryV2IT() {
super("");
}

@Test
public void sendTask() {

// Create a project
ProjectDTO project = ProjectDTO.builder().build();
UUID projectUuid = UUID.randomUUID();
sendPost(PATH + "/" + ProjectDTO.RESOURCE_TYPE, JsonAPITestHelper.toJsonAPIMap(
ProjectDTO.RESOURCE_TYPE, JsonAPITestHelper.toAttributeMap(project), null, projectUuid.toString()));

// Create a task
TaskDTO task = TaskDTO.builder().power(RandomUtils.nextInt()).build();
UUID uuid = UUID.randomUUID();
int returnCode = sendPatch(PATH, uuid.toString(), JsonAPITestHelper.toJsonAPIMap(
TaskDTO.RESOURCE_TYPE, JsonAPITestHelper.toAttributeMap(task), null, uuid.toString()))
UUID taskUuid = UUID.randomUUID();
sendPost(PATH + "/" + TaskDTO.RESOURCE_TYPE, JsonAPITestHelper.toJsonAPIMap(
TaskDTO.RESOURCE_TYPE, JsonAPITestHelper.toAttributeMap(task), null, taskUuid.toString()));

// Patch the project to set the task
int returnCode = sendPatch(PATH + "/" + ProjectDTO.RESOURCE_TYPE , projectUuid.toString(), JsonAPITestHelper.toJsonAPIMap(
ProjectDTO.RESOURCE_TYPE, JsonAPITestHelper.toAttributeMap(project),
JsonAPITestHelper.toRelationshipMap(JsonAPIRelationship.of("task", TaskDTO.RESOURCE_TYPE, taskUuid.toString()))
, projectUuid.toString()))
.extract().response().getStatusCode();

assertEquals(200, returnCode);
Expand All @@ -68,20 +109,60 @@ static class TestDynaBeanRepo {
@Getter
private Integer level;

@PatchMapping(PATH + "/{id}")
public ResponseEntity<RepresentationModel<?>> handlePatch(@RequestBody
EntityModel<JsonApiPartialPatchDto> partialPatchDto,
@Autowired
private DinaRepositoryV2<ProjectDTO, Project> projectRepo;

@Autowired
private DinaRepositoryV2<TaskDTO, Task> taskRepo;

@PostMapping(PATH + "/" + TaskDTO.RESOURCE_TYPE)
@Transactional
public ResponseEntity<RepresentationModel<?>> handlePostTask(@RequestBody EntityModel<TaskDTO> taskDTO) {
taskRepo.create(taskDTO.getContent());
return ResponseEntity.created(URI.create("/")).build();
}

@PostMapping(PATH + "/" + ProjectDTO.RESOURCE_TYPE)
@Transactional
public ResponseEntity<RepresentationModel<?>> handlePostProject(@RequestBody EntityModel<ProjectDTO> projectDTO) {
projectRepo.create(projectDTO.getContent());
return ResponseEntity.created(URI.create("/")).build();
}

@PatchMapping(PATH + "/" + ProjectDTO.RESOURCE_TYPE + "/{id}")
public ResponseEntity<RepresentationModel<?>> handlePatch(@RequestBody JsonApiDocument partialPatchDto,
@PathVariable String id) {
projectRepo.update(partialPatchDto);
// JsonApiDocument b = partialPatchDto;//.getContent();
// Map<String, String> s = b.getMap().entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey,
// e -> e.getValue().toString()));
// s.put("id", b.getId().toString());
// TaskDTO t = TaskDtoMapper.INSTANCE.toTaskDto(s);
// level = t.getPower();

JsonApiPartialPatchDto b = partialPatchDto.getContent();
Map<String, String> s = b.getMap().entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey,
e -> e.getValue().toString()));
s.put("id", b.getId().toString());
TaskDTO t = TaskDtoMapper.INSTANCE.toTaskDto(s);
level = t.getPower();
return null;
}
}

@TestConfiguration
static class RepoV2TestConfig {

@Bean
public DinaRepositoryV2<TaskDTO, Task> taskRepositoryV2(DinaService<Task> dinaService,
BuildProperties buildProperties,
ObjectMapper objMapper) {
return new DinaRepositoryV2<>(dinaService, new AllowAllAuthorizationService(),
Optional.empty(), TaskDtoMapper.INSTANCE, TaskDTO.class, Task.class,
buildProperties, objMapper);
}

@Bean
public DinaRepositoryV2<ProjectDTO, Project> projectRepositoryV2(DinaService<Project> dinaService,
BuildProperties buildProperties,
ObjectMapper objMapper) {
return new DinaRepositoryV2<>(dinaService, new AllowAllAuthorizationService(),
Optional.empty(), ProjectDtoMapper.INSTANCE, ProjectDTO.class, Project.class,
buildProperties, objMapper);
}
}
}
Loading

0 comments on commit dd40446

Please sign in to comment.