Skip to content

Commit

Permalink
Support @JsonApiRelationId in crnk-validation #248
Browse files Browse the repository at this point in the history
  • Loading branch information
remmeier committed Aug 27, 2018
1 parent f116958 commit 2087786
Show file tree
Hide file tree
Showing 7 changed files with 284 additions and 83 deletions.
Expand Up @@ -103,6 +103,8 @@ public class ResourceInformation {

private Map<String, ResourceField> fieldByUnderlyingName = new HashMap<>();

private Map<String, ResourceFieldAccessor> fieldAccessors = new HashMap<>();

private List<ResourceField> fields;

private AnyResourceFieldAccessor anyFieldAccessor;
Expand All @@ -126,26 +128,28 @@ public Object parse(String input) {

private ResourceFieldAccessor nestedIdAccessor;

private ResourceInformation resourceInformation;

public ResourceInformation(TypeParser parser, Type implementationType, String resourceType, String superResourceType,
List<ResourceField> fields, Class<? extends PagingSpec> pagingSpecType) {
List<ResourceField> fields, Class<? extends PagingSpec> pagingSpecType) {
this(parser, implementationType, resourceType, null, superResourceType, null, fields, pagingSpecType);
}

public ResourceInformation(TypeParser parser, Type implementationType, String resourceType, String resourcePath,
String superResourceType,
List<ResourceField> fields, Class<? extends PagingSpec> pagingSpecType) {
String superResourceType,
List<ResourceField> fields, Class<? extends PagingSpec> pagingSpecType) {
this(parser, implementationType, resourceType, resourcePath, superResourceType, null, fields, pagingSpecType);
}

public ResourceInformation(TypeParser parser, Type implementationType, String resourceType, String superResourceType,
ResourceInstanceBuilder<?> instanceBuilder, List<ResourceField> fields, Class<? extends PagingSpec> pagingSpecType) {
ResourceInstanceBuilder<?> instanceBuilder, List<ResourceField> fields, Class<? extends PagingSpec> pagingSpecType) {
this(parser, implementationType, resourceType, null, superResourceType, instanceBuilder, fields, pagingSpecType);
}

@SuppressWarnings({"rawtypes", "unchecked"})
@SuppressWarnings({ "rawtypes", "unchecked" })
public ResourceInformation(TypeParser parser, Type implementationType, String resourceType, String resourcePath,
String superResourceType,
ResourceInstanceBuilder<?> instanceBuilder, List<ResourceField> fields, Class<? extends PagingSpec> pagingSpecType) {
String superResourceType,
ResourceInstanceBuilder<?> instanceBuilder, List<ResourceField> fields, Class<? extends PagingSpec> pagingSpecType) {
this.parser = parser;
this.implementationClass = ClassUtils.getRawType(implementationType);
this.implementationType = implementationType;
Expand Down Expand Up @@ -175,27 +179,38 @@ private void setupNesting() {
for (String attributeName : beanInformation.getAttributeNames()) {
BeanAttributeInformation attribute = beanInformation.getAttribute(attributeName);
if (attribute.getAnnotation(JsonApiRelationId.class).isPresent()) {
PreconditionUtil.verify(parentAttribute == null, "nested identifiers can only have a single @JsonApiRelationId annotated field, got multiple for %s", beanInformation.getImplementationClass());
PreconditionUtil.verify(parentAttribute == null,
"nested identifiers can only have a single @JsonApiRelationId annotated field, got multiple for %s",
beanInformation.getImplementationClass());
parentAttribute = attribute;
} else if (attribute.getAnnotation(JsonApiId.class).isPresent()) {
PreconditionUtil.verify(idAttribute == null, "nested identifiers can only one attribute being annotated with @JsonApiId, got multiple for %s", beanInformation.getImplementationClass());
}
else if (attribute.getAnnotation(JsonApiId.class).isPresent()) {
PreconditionUtil.verify(idAttribute == null,
"nested identifiers can only one attribute being annotated with @JsonApiId, got multiple for %s",
beanInformation.getImplementationClass());
idAttribute = attribute;
}
}

if (parentAttribute != null || idAttribute != null) {
PreconditionUtil.verify(idAttribute != null, "nested identifiers must have attribute annotated with @JsonApiId, got none for %s", beanInformation.getImplementationClass());
PreconditionUtil.verify(parentAttribute != null, "nested identifiers must have attribute annotated with @JsonApiRelationId, got none for %s", beanInformation.getImplementationClass());
PreconditionUtil.verify(parentAttribute.getName().endsWith("Id"), "nested identifier must have @JsonApiRelationId field being named with a 'Id' suffix, got %s", parentAttribute.getName());
PreconditionUtil.verify(idAttribute != null,
"nested identifiers must have attribute annotated with @JsonApiId, got none for %s",
beanInformation.getImplementationClass());
PreconditionUtil.verify(parentAttribute != null,
"nested identifiers must have attribute annotated with @JsonApiRelationId, got none for %s",
beanInformation.getImplementationClass());
PreconditionUtil.verify(parentAttribute.getName().endsWith("Id"),
"nested identifier must have @JsonApiRelationId field being named with a 'Id' suffix, got %s",
parentAttribute.getName());
String relationshipName = parentAttribute.getName().substring(0, parentAttribute.getName().length() - 2);

parentField = findRelationshipFieldByName(relationshipName);
BeanAttributeInformation finalParentAttribute = parentAttribute;
ResourceFieldAccessor parentIdAccessor = new ResourceFieldAccessor(){
ResourceFieldAccessor parentIdAccessor = new ResourceFieldAccessor() {

@Override
public Object getValue(Object object) {
if(idField.getType().isInstance(object)){
if (idField.getType().isInstance(object)) {
return finalParentAttribute.getValue(object);
}

Expand All @@ -208,17 +223,24 @@ public void setValue(Object resource, Object fieldValue) {
throw new UnsupportedOperationException("cannot update nested ids");
}
};
PreconditionUtil.verify(parentField != null, "naming of relationship to parent resource and relationship identifier within resource identifier must match, not found for %s of %s", parentAttribute.getName(),
PreconditionUtil.verify(parentField != null,
"naming of relationship to parent resource and relationship identifier within resource identifier must "
+ "match, not found for %s of %s",
parentAttribute.getName(),
implementationClass);
PreconditionUtil.verify(parentField.getOppositeName() != null, "relationship of a nested resource pointing to its parent must have @JsonApiRelation.opposite defined, not found for '%s' of %s", parentAttribute.getName(),
PreconditionUtil.verify(parentField.getOppositeName() != null,
"relationship of a nested resource pointing to its parent must have @JsonApiRelation.opposite defined, not "
+ "found for '%s' of %s",
parentAttribute.getName(),
implementationClass);
((ResourceFieldImpl) parentField).setIdField(parentAttribute.getName(), parentAttribute.getImplementationClass(), parentIdAccessor);
((ResourceFieldImpl) parentField)
.setIdField(parentAttribute.getName(), parentAttribute.getImplementationClass(), parentIdAccessor);
BeanAttributeInformation finalIdAttribute = idAttribute;
this.nestedIdAccessor = new ResourceFieldAccessor() {

@Override
public Object getValue(Object object) {
if(idField.getType().isInstance(object)){
if (idField.getType().isInstance(object)) {
return finalIdAttribute.getValue(object);
}
Object id = getIdField().getAccessor().getValue(object);
Expand Down Expand Up @@ -273,7 +295,8 @@ private void initAny() {
public Object getValue(Object resource, String name) {
try {
return jsonAnyGetter.invoke(resource, name);
} catch (IllegalAccessException | InvocationTargetException e) {
}
catch (IllegalAccessException | InvocationTargetException e) {
throw new ResourceException(
String.format("Exception while reading %s.%s due to %s", resource, name, e.getMessage()), e);
}
Expand All @@ -283,7 +306,8 @@ public Object getValue(Object resource, String name) {
public void setValue(Object resource, String name, Object fieldValue) {
try {
jsonAnySetter.invoke(resource, name, fieldValue);
} catch (IllegalAccessException | InvocationTargetException e) {
}
catch (IllegalAccessException | InvocationTargetException e) {
throw new ResourceException(
String.format("Exception while writting %s.%s=%s due to %s", resource, name, fieldValue,
e.getMessage()), e);
Expand Down Expand Up @@ -323,10 +347,22 @@ private void initFields() {
resourceField.setResourceInformation(this);
fieldByJsonName.put(resourceField.getJsonName(), resourceField);
fieldByUnderlyingName.put(resourceField.getUnderlyingName(), resourceField);

fieldAccessors.put(resourceField.getUnderlyingName(), resourceField.getAccessor());
if (resourceField.getResourceFieldType() == ResourceFieldType.RELATIONSHIP && resourceField.hasIdField()) {
fieldAccessors.put(resourceField.getIdName(), resourceField.getIdAccessor());

// a non-JsonApiRelationId field can also be referenced => has its own ResourceField instance
if(!fieldByUnderlyingName.containsKey(resourceField.getIdName())) {
fieldByUnderlyingName.put(resourceField.getIdName(), resourceField);
}
}
}
} else {
}
else {
this.relationshipFields = Collections.emptyList();
this.attributeFields = Collections.emptyList();
this.fieldAccessors = null;
this.metaField = null;
this.linksField = null;
this.idField = null;
Expand All @@ -339,6 +375,14 @@ public void setFields(List<ResourceField> fields) {
this.initFields();
}

/**
* @param name name of regular field or field annotated with @JsonApiRelationId
* @return ResourceFieldAccessor to get and set values
*/
public ResourceFieldAccessor getAccessor(String name) {
return fieldAccessors.get(name);
}

private static <T> ResourceField getMetaField(Class<T> resourceClass, Collection<ResourceField> classFields) {
List<ResourceField> metaFields = new ArrayList<>(1);
for (ResourceField field : classFields) {
Expand All @@ -349,7 +393,8 @@ private static <T> ResourceField getMetaField(Class<T> resourceClass, Collection

if (metaFields.isEmpty()) {
return null;
} else if (metaFields.size() > 1) {
}
else if (metaFields.size() > 1) {
throw new MultipleJsonApiMetaInformationException(resourceClass.getCanonicalName());
}
return metaFields.get(0);
Expand All @@ -365,7 +410,8 @@ private static <T> ResourceField getLinksField(Class<T> resourceClass, Collectio

if (linksFields.isEmpty()) {
return null;
} else if (linksFields.size() > 1) {
}
else if (linksFields.size() > 1) {
throw new MultipleJsonApiLinksInformationException(resourceClass.getCanonicalName());
}
return linksFields.get(0);
Expand Down Expand Up @@ -454,7 +500,8 @@ public boolean equals(Object o) {
return false;
}
ResourceInformation that = (ResourceInformation) o;
return Objects.equals(implementationClass, that.implementationClass) && Objects.equals(resourceType, that.resourceType) && Objects
return Objects.equals(implementationClass, that.implementationClass) && Objects.equals(resourceType, that.resourceType)
&& Objects
.equals(resourcePath, that.resourcePath);
}

Expand Down Expand Up @@ -496,7 +543,8 @@ public ResourceIdentifier toResourceIdentifier(Object resourceOrId) {
String strId;
if (resourceOrId instanceof String) {
strId = (String) resourceOrId;
} else {
}
else {
strId = toIdString(resourceOrId);
}
return new ResourceIdentifier(strId, getResourceType());
Expand All @@ -509,7 +557,7 @@ public ResourceIdentifier toResourceIdentifier(Object resourceOrId) {
* @param id stringified id
* @return id
*/
@SuppressWarnings({"unchecked", "rawtypes"})
@SuppressWarnings({ "unchecked", "rawtypes" })
public Serializable parseIdString(String id) {
return (Serializable) idStringMapper.parse(id);
}
Expand Down Expand Up @@ -541,7 +589,9 @@ public Class<? extends PagingSpec> getPagingSpecType() {
}

/**
* @return true if this resource is a child of another resource. This will result in nested URLs like /api/foo/1/bar/2 for the nested bar resource.
* @return true if this resource is a child of another resource. This will result in nested URLs like /api/foo/1/bar/2 for
* the
* nested bar resource.
* The resource may still or may not be accessible from /api/bar/2 depending on @JsonApiExposed.
*/
public boolean isNested() {
Expand Down
Expand Up @@ -12,6 +12,8 @@
import io.crnk.core.engine.document.ResourceIdentifier;
import io.crnk.core.engine.information.resource.AnyResourceFieldAccessor;
import io.crnk.core.engine.information.resource.ResourceField;
import io.crnk.core.engine.information.resource.ResourceFieldAccess;
import io.crnk.core.engine.information.resource.ResourceFieldAccessor;
import io.crnk.core.engine.information.resource.ResourceFieldType;
import io.crnk.core.engine.information.resource.ResourceInformation;
import io.crnk.core.engine.internal.information.resource.ResourceFieldImpl;
Expand All @@ -22,21 +24,31 @@
import io.crnk.core.queryspec.pagingspec.OffsetLimitPagingSpec;
import io.crnk.core.resource.annotations.JsonApiId;
import io.crnk.core.resource.annotations.JsonApiResource;
import io.crnk.core.resource.annotations.LookupIncludeBehavior;
import io.crnk.core.resource.annotations.RelationshipRepositoryBehavior;
import io.crnk.core.resource.annotations.SerializeType;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;

public class ResourceInformationTest {

private ResourceInformation sut;

private TypeParser typeParser = new TypeParser();


private ResourceFieldImpl valueField;

@Before
public void setup() {
ResourceField idField = new ResourceFieldImpl("id", "id", ResourceFieldType.ID, Long.class, Long.class, null);
ResourceField valueField = new ResourceFieldImpl("value", "value", ResourceFieldType.RELATIONSHIP, String.class,
String.class, "projects");
ResourceFieldAccessor valueIdAccessor = Mockito.mock(ResourceFieldAccessor.class);
valueField = new ResourceFieldImpl("value", "value", ResourceFieldType.RELATIONSHIP, String.class,
String.class, "projects", null, SerializeType.LAZY, LookupIncludeBehavior.AUTOMATICALLY_ALWAYS,
new ResourceFieldAccess(true, true, true, true, true),
"valueId", String.class, valueIdAccessor, RelationshipRepositoryBehavior.DEFAULT, null);
sut = new ResourceInformation(typeParser, Task.class, "tasks", null, Arrays.asList(idField, valueField),
OffsetLimitPagingSpec.class);
}
Expand Down Expand Up @@ -85,6 +97,13 @@ public void checkIdAccess() {
Assert.assertEquals(13L, sut.getId(task));
}


@Test
public void checkGetAccessor() {
Assert.assertSame(valueField.getIdAccessor(), sut.getAccessor("valueId"));
Assert.assertSame(valueField.getAccessor(), sut.getAccessor("value"));
}

@Test
public void checkAnyAccessor() {
ResourceField idField = new ResourceFieldImpl("id", "id", ResourceFieldType.ID, Long.class, Long.class, null);
Expand Down

0 comments on commit 2087786

Please sign in to comment.