ca.gc.aafc.dina.repository.DinaRepositoryV2 Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of dina-base-api Show documentation
Show all versions of dina-base-api Show documentation
Base DINA API package for Java built on SpringBoot and Crnk
The newest version!
package ca.gc.aafc.dina.repository;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.info.BuildProperties;
import org.springframework.hateoas.CollectionModel;
import org.springframework.hateoas.RepresentationModel;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.tennaito.rsql.misc.ArgumentParser;
import com.toedter.spring.hateoas.jsonapi.JsonApiModelBuilder;
import ca.gc.aafc.dina.dto.ExternalRelationDto;
import ca.gc.aafc.dina.dto.JsonApiExternalResource;
import ca.gc.aafc.dina.dto.JsonApiMeta;
import ca.gc.aafc.dina.dto.JsonApiResource;
import ca.gc.aafc.dina.dto.JsonApiDto;
import ca.gc.aafc.dina.entity.DinaEntity;
import ca.gc.aafc.dina.filter.DinaFilterArgumentParser;
import ca.gc.aafc.dina.filter.EntityFilterHelper;
import ca.gc.aafc.dina.filter.FilterComponent;
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.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 java.lang.reflect.InvocationTargetException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import javax.persistence.criteria.Predicate;
import javax.servlet.http.HttpServletRequest;
import lombok.NonNull;
import lombok.extern.log4j.Log4j2;
import static com.toedter.spring.hateoas.jsonapi.JsonApiModelBuilder.jsonApiModel;
@Log4j2
public class DinaRepositoryV2 {
// default page limit/page size
private static final int DEFAULT_PAGE_LIMIT = 20;
private static final int MAX_PAGE_LIMIT = 100;
private final DinaAuthorizationService authorizationService;
private final DinaService dinaService;
private final Class entityClass;
private final Class resourceClass;
private final DinaMapperV2 dinaMapper;
private final BuildProperties buildProperties;
protected final DinaMappingRegistry registry;
private final ArgumentParser rsqlArgumentParser = new DinaFilterArgumentParser();
public DinaRepositoryV2(@NonNull DinaService dinaService,
@NonNull DinaAuthorizationService authorizationService,
@NonNull Optional auditService,
@NonNull DinaMapperV2 dinaMapper,
@NonNull Class resourceClass,
@NonNull Class entityClass,
@NonNull BuildProperties buildProperties,
ObjectMapper objMapper) {
this.authorizationService = authorizationService;
this.dinaService = dinaService;
this.entityClass = entityClass;
this.resourceClass = resourceClass;
this.dinaMapper = dinaMapper;
this.buildProperties = buildProperties;
// build registry instance for resource class (dto)
this.registry = new DinaMappingRegistry(resourceClass);
}
/**
* Used to provide mapping between {@link ExternalRelationDto} and new JsonApiExternalResource instances.
* By default, this method returns null since the supported types are unknown.
* @param externalRelationDto
* @return
*/
protected JsonApiExternalResource externalRelationDtoToJsonApiExternalResource(ExternalRelationDto externalRelationDto) {
return null;
}
/**
* @param identifier
* @param queryString
* @return
*/
public JsonApiDto getOne(UUID identifier, String queryString) {
// the only part of QueryComponent that can be used on getOne is "includes"
QueryComponent queryComponents = QueryStringParser.parse(queryString);
Set includes = queryComponents.getIncludes() != null ? queryComponents.getIncludes() : Set.of();
validateIncludes(includes);
E entity = dinaService.findOne(identifier, entityClass, includes);
authorizationService.authorizeRead(entity);
Set attributes = new HashSet<>(registry.getAttributesPerClass().get(entityClass));
attributes.addAll(includes);
addNestedAttributesFromIncludes(attributes, includes);
D dto = dinaMapper.toDto(entity, attributes, null);
return toJsonApiDto(dto, includes);
}
public PagedResource> getAll(String queryString) {
QueryComponent queryComponents = QueryStringParser.parse(queryString);
return getAll(queryComponents);
}
public PagedResource> getAll(QueryComponent queryComponents) {
FilterComponent fc = queryComponents.getFilters();
Set relationshipsPath = EntityFilterHelper.extractRelationships(queryComponents.getIncludes(), resourceClass, registry);
Set includes = queryComponents.getIncludes() != null ? queryComponents.getIncludes() : Set.of();
validateIncludes(includes);
int pageOffset = toSafePageOffset(queryComponents.getPageOffset());
int pageLimit = toSafePageLimit(queryComponents.getPageLimit());
List entities = dinaService.findAll(
entityClass,
(criteriaBuilder, root, em) -> {
EntityFilterHelper.leftJoinSortRelations(root, queryComponents.getSorts(), resourceClass, registry);
Predicate restriction = SimpleFilterHandlerV2.getRestriction(root, criteriaBuilder, rsqlArgumentParser::parse, em.getMetamodel(), fc != null ? List.of(fc) : List.of());
return new Predicate[]{restriction};
},
(cb, root) -> EntityFilterHelper.getOrders(cb, root, queryComponents.getSorts(), false),
pageOffset, pageLimit, includes, relationshipsPath);
List> dtos = new ArrayList<>(entities.size());
Set attributes = new HashSet<>(registry.getAttributesPerClass().get(entityClass));
attributes.addAll(queryComponents.getIncludes() != null ? queryComponents.getIncludes() : Set.of());
addNestedAttributesFromIncludes(attributes, includes);
for (E e : entities) {
dtos.add(toJsonApiDto(dinaMapper.toDto(e, attributes, null), includes));
}
Long resourceCount = dinaService.getResourceCount( entityClass,
(criteriaBuilder, root, em) -> {
Predicate restriction = SimpleFilterHandlerV2.getRestriction(root, criteriaBuilder, rsqlArgumentParser::parse, em.getMetamodel(), fc != null ? List.of(fc) : List.of());
return new Predicate[]{restriction};
});
return new PagedResource<>(pageOffset, pageLimit, resourceCount.intValue(), dtos);
}
/**
* Build a {@link JsonApiDto} for a given dto and a set of includes.
*
* @param dto
* @param includes
* @return
*/
private JsonApiDto toJsonApiDto(D dto, Set includes) {
JsonApiDto.JsonApiDtoBuilder jsonApiDtoBuilder = JsonApiDto.builder();
for (String include : includes) {
try {
Object rel = PropertyUtils.getProperty(dto, include);
if (rel instanceof Collection> coll) {
handleToManyRelationship(jsonApiDtoBuilder, include, coll);
} else if (rel != null) {
handleToOneRelationship(jsonApiDtoBuilder, include, rel);
} else {
handleNullValueRelationship(jsonApiDtoBuilder, include);
}
} catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
return jsonApiDtoBuilder.dto(dto).build();
}
private void handleToOneRelationship(JsonApiDto.JsonApiDtoBuilder> builder, String name,
Object rel) {
switch (rel) {
case JsonApiExternalResource jaer -> builder.relationship(name,
JsonApiDto.RelationshipToOneExternal.builder()
.included(jaer).build());
case JsonApiResource ddto -> builder.relationship(name,
JsonApiDto.RelationshipToOne.builder()
.included(ddto).build());
case ExternalRelationDto erd -> builder.relationship(name,
JsonApiDto.RelationshipToOneExternal.builder()
.included(externalRelationDtoToJsonApiExternalResource(erd)).build());
case null, default -> log.warn("Not an instance of JsonApiResource, ignoring {}", name);
}
}
private void handleToManyRelationship(JsonApiDto.JsonApiDtoBuilder> builder, String name,
Collection> rel) {
List castSafeList = new ArrayList<>(rel.size());
List castSafeListExternal = new ArrayList<>();
for (Object element : rel) {
switch (element) {
case JsonApiExternalResource jaer -> castSafeListExternal.add(jaer);
case JsonApiResource jar -> castSafeList.add(jar);
case ExternalRelationDto erd ->
castSafeListExternal.add(externalRelationDtoToJsonApiExternalResource(erd));
case null, default -> log.warn("Not an instance of JsonApiResource, ignoring {}", name);
}
}
if (!castSafeListExternal.isEmpty()) {
builder.relationship(name, JsonApiDto.RelationshipManyExternal.builder()
.included(castSafeListExternal).build());
} else {
builder.relationship(name,
JsonApiDto.RelationshipToMany.builder()
.included(castSafeList).build());
}
}
/**
* Handle relationships when the assigned value is null.
* @param builder
* @param relationshipName
*/
private void handleNullValueRelationship(JsonApiDto.JsonApiDtoBuilder> builder, String relationshipName) {
Class> internalRelClass = registry.getInternalRelationClass(resourceClass, relationshipName);
if (internalRelClass != null) {
if (DinaMappingRegistry.isCollection(internalRelClass)) {
builder.relationship(relationshipName,
JsonApiDto.RelationshipToMany.builder().build());
} else {
builder.relationship(relationshipName,
JsonApiDto.RelationshipToOne.builder().build());
}
} else {
Class> relClass = registry.getExternalRelationClass(resourceClass, relationshipName);
if (DinaMappingRegistry.isCollection(relClass)) {
builder.relationship(relationshipName,
JsonApiDto.RelationshipManyExternal.builder().build());
} else {
builder.relationship(relationshipName,
JsonApiDto.RelationshipToOneExternal.builder().build());
}
}
}
/**
* Responsible to create the {@link JsonApiModelBuilder} for the provided {@link JsonApiDto}.
*
* @param jsonApiDto
* @return
*/
protected JsonApiModelBuilder createJsonApiModelBuilder(JsonApiDto jsonApiDto) {
Set included = new HashSet<>(jsonApiDto.getRelationships().size());
JsonApiModelBuilder mainBuilder = jsonApiModel();
JsonApiModelBuilder builder = createJsonApiModelBuilder(jsonApiDto, mainBuilder, included);
JsonApiMeta.builder()
.moduleVersion(buildProperties.getVersion())
.build()
.populateMeta(mainBuilder::meta);
mainBuilder.model(builder.build());
return mainBuilder;
}
/**
* Same as {@link #createJsonApiModelBuilder(JsonApiDto)} but for pages resource.
* @param jsonApiDtos
* @return
*/
protected JsonApiModelBuilder createJsonApiModelBuilder(PagedResource> jsonApiDtos) {
JsonApiModelBuilder mainBuilder = jsonApiModel();
List> repModels = new ArrayList<>();
Set included = new HashSet<>();
for (JsonApiDto currResource : jsonApiDtos.resourceList()) {
JsonApiModelBuilder builder = createJsonApiModelBuilder(currResource, mainBuilder, included);
repModels.add(builder.build());
}
// use custom metadata instead of PagedModel.PageMetadata so we can control
// the content and key names
JsonApiMeta.builder()
.totalResourceCount(jsonApiDtos.totalCount)
.moduleVersion(buildProperties.getVersion())
.build()
.populateMeta(mainBuilder::meta);
mainBuilder.model(CollectionModel.of(repModels));
return mainBuilder;
}
/**
* Internal method to create {@link JsonApiModelBuilder}.
* @param jsonApiDto
* @param includeBuilder builder to use to add "included" documents. if null, the builder
* created for that document will be used.
* @param included set of already included uuid
* @return
*/
private JsonApiModelBuilder createJsonApiModelBuilder(JsonApiDto jsonApiDto,
JsonApiModelBuilder includeBuilder,
Set included) {
JsonApiModelBuilder builder = jsonApiModel().model(RepresentationModel.of(jsonApiDto.getDto()));
for (var rel : jsonApiDto.getRelationships().entrySet()) {
switch (rel.getValue()) {
case JsonApiDto.RelationshipToOne toOne ->
setToOneOnJsonApiModelBuilder(builder, rel.getKey(), toOne, includeBuilder, included);
case JsonApiDto.RelationshipToMany toMany ->
setToManyOnJsonApiModelBuilder(builder, rel.getKey(), toMany, includeBuilder, included);
case JsonApiDto.RelationshipToOneExternal toOneExt ->
setToOneExtOnJsonApiModelBuilder(builder, rel.getKey(), toOneExt);
case JsonApiDto.RelationshipManyExternal toManyExt ->
setToManyExtOnJsonApiModelBuilder(builder, rel.getKey(), toManyExt);
default -> throw new IllegalStateException("Unexpected value: " + rel.getValue());
}
}
return builder;
}
/**
* Internal method to set {@link JsonApiDto.RelationshipToOne} to a {@link JsonApiModelBuilder}.
* @param builder current builder
* @param relationshipName
* @param toOne
* @param includeBuilder if null, builder will be used
* @param included set of already included uuid
*/
private static void setToOneOnJsonApiModelBuilder(JsonApiModelBuilder builder, String relationshipName,
JsonApiDto.RelationshipToOne toOne,
JsonApiModelBuilder includeBuilder,
Set included) {
if (toOne.getIncluded() != null) {
builder.relationship(relationshipName, toOne.getIncluded());
addUniqueIncluded(includeBuilder != null ? includeBuilder : builder, toOne.getIncluded(), included);
} else {
// requires spring-hateoas-jsonapi 2.x
// builder.relationship(relationshipName, (Object) null);
log.warn("Ignoring null value for toOne internal relationship {}", relationshipName);
}
}
/**
* Internal method to set {@link JsonApiDto.RelationshipToMany} to a {@link JsonApiModelBuilder}.
* @param builder current builder
* @param relationshipName
* @param toMany
* @param includeBuilder if null, builder will be used
* @param included set of already included uuid
*/
private static void setToManyOnJsonApiModelBuilder(JsonApiModelBuilder builder, String relationshipName,
JsonApiDto.RelationshipToMany toMany, JsonApiModelBuilder includeBuilder,Set included) {
if (toMany.getIncluded() != null) {
builder.relationship(relationshipName, toMany.getIncluded());
for (var includedResource : toMany.getIncluded()) {
addUniqueIncluded(includeBuilder != null ? includeBuilder : builder, includedResource, included);
}
} else {
// requires spring-hateoas-jsonapi 2.x
// builder.relationship(relationshipName, (Object) null);
log.warn("Ignoring null value for toMany internal relationship {}", relationshipName);
}
}
private static void setToOneExtOnJsonApiModelBuilder(JsonApiModelBuilder builder, String relationshipName,
JsonApiDto.RelationshipToOneExternal toOneExt) {
if (toOneExt.getIncluded() != null) {
builder.relationship(relationshipName, toOneExt.getIncluded());
// addUniqueIncluded(builder, toOneExt.getIncluded(), included);
} else {
//requires spring-hateoas-jsonapi 2.x
// builder.relationship(relationshipName, (Object) null);
log.warn("Ignoring null value for toOne external relationship {}", relationshipName);
}
}
private static void setToManyExtOnJsonApiModelBuilder(JsonApiModelBuilder builder, String relationshipName,
JsonApiDto.RelationshipManyExternal toManyExt) {
if (toManyExt.getIncluded() != null) {
builder.relationship(relationshipName, toManyExt.getIncluded());
} else {
//requires spring-hateoas-jsonapi 2.x
log.warn("Ignoring null value for toMany external relationship {}", relationshipName);
}
}
/**
* Add an include {@link JsonApiResource} is not already present.
* The main goal of this method is to avoid duplicates in the include section.
*
* @param builder the current builder
* @param include the {@link JsonApiResource} to include
* @param included writable non-null set containing the already included uuid
*/
private static void addUniqueIncluded(JsonApiModelBuilder builder,
JsonApiResource include, Set included) {
Objects.requireNonNull(include);
if (!included.contains(include.getJsonApiId())) {
builder.included(include);
included.add(include.getJsonApiId());
}
}
/**
* null-safe query string UTF-8 decode function.
* @param req
* @return decoded query string or empty string if query string is absent
*/
protected static String decodeQueryString(HttpServletRequest req) {
Objects.requireNonNull(req);
if (StringUtils.isBlank(req.getQueryString())) {
return "";
}
return URLDecoder.decode(req.getQueryString(), StandardCharsets.UTF_8);
}
/**
* Make sure the include list is valid.
* @param includes
*/
private void validateIncludes(Set includes) throws IllegalArgumentException {
for (String inc : includes) {
if (!registry.isInternalRelationship(entityClass, inc)
&& !registry.isRelationExternal(entityClass, inc)) {
throw new IllegalArgumentException("Invalid include");
}
}
}
/**
* From a list of included relationships, add the nested attributes
* using the name of the relationship as prefix.
*
* @param attributes current attribute set (new attributes to be added to it)
* @param includes list of relationship included (could be internal or external)
*/
private void addNestedAttributesFromIncludes(Set attributes, Set includes) {
for (String inc : includes) {
Class> c = registry.getInternalRelationClass(entityClass, inc);
// external relationship would return null
if (c != null) {
Set ca = registry.getAttributesPerClass().get(c);
if (ca != null) {
for (String cal : ca) {
attributes.add(inc + "." + cal);
}
}
}
}
}
private static int toSafePageOffset(Integer pageOffset) {
if (pageOffset == null || pageOffset <= 0) {
return 0;
}
return pageOffset;
}
private static int toSafePageLimit(Integer pageLimit) {
if (pageLimit == null || pageLimit <= 0) {
return DEFAULT_PAGE_LIMIT;
}
if (pageLimit > MAX_PAGE_LIMIT) {
return DEFAULT_PAGE_LIMIT;
}
return pageLimit;
}
/**
*
* @param pageOffset
* @param pageLimit
* @param totalCount
* @param resourceList
* @param
*/
public record PagedResource(int pageOffset, int pageLimit, int totalCount,
List resourceList) {
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy