Skip to content

Commit

Permalink
Support for JsonProperty in QuerySpec parsing #267
Browse files Browse the repository at this point in the history
JSON names mapped to Java names for QuerySpec-related paths. Can be disabled with DefaultQuerySpecUrlMapper.setMapJsonNames(..).
  • Loading branch information
remmeier committed Apr 29, 2018
1 parent 79490eb commit 3fd16b0
Show file tree
Hide file tree
Showing 28 changed files with 756 additions and 208 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package io.crnk.client;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.crnk.core.engine.document.Resource;
import io.crnk.core.exception.InternalServerErrorException;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import io.crnk.core.queryspec.QuerySpec;
import io.crnk.core.queryspec.SortSpec;
import io.crnk.core.queryspec.mapper.DefaultQuerySpecUrlMapper;
import io.crnk.core.queryspec.mapper.QuerySpecUrlMapper;
import io.crnk.core.repository.RelationshipRepositoryV2;
import io.crnk.core.repository.ResourceRepositoryV2;
import io.crnk.test.mock.models.Project;
Expand All @@ -36,6 +37,9 @@ public class QuerySpecUnknownAttributeClientTest extends AbstractClientTest {
public void setup() {
super.setup();

DefaultQuerySpecUrlMapper urlMapper = (DefaultQuerySpecUrlMapper) client.getUrlMapper();
urlMapper.setAllowUnknownAttributes(true);

taskRepo = client.getRepositoryForType(Task.class);
projectRepo = client.getRepositoryForType(Project.class);
relRepo = client.getRepositoryForType(Task.class, Project.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.Map;

import com.fasterxml.jackson.annotation.JsonProperty;
import io.crnk.core.utils.Optional;

public class BeanAttributeInformation {
Expand All @@ -21,6 +23,8 @@ public class BeanAttributeInformation {

private BeanInformation beanInformation;

private String jsonName;

protected BeanAttributeInformation(BeanInformation beanInformation) {
this.beanInformation = beanInformation;
}
Expand Down Expand Up @@ -116,15 +120,12 @@ public Object getValue(Object bean) {
try {
if (getter != null) {
return getter.invoke(bean);
}
else {
} else {
return field.get(bean);
}
}
catch (IllegalAccessException e) {
} catch (IllegalAccessException e) {
throw new IllegalStateException(e);
}
catch (InvocationTargetException e) {
} catch (InvocationTargetException e) {
throw new IllegalStateException(e);
}
}
Expand All @@ -133,16 +134,32 @@ public void setValue(Object bean, Object value) {
try {
if (setter != null) {
setter.invoke(bean, value);
}
else {
} else {
field.set(bean, value);
}
}
catch (IllegalAccessException e) {
} catch (IllegalAccessException e) {
throw new IllegalStateException(e);
}
catch (InvocationTargetException e) {
} catch (InvocationTargetException e) {
throw new IllegalStateException(e);
}
}

public String getJsonName() {
if (jsonName == null) {
Optional<JsonProperty> annotation = getAnnotation(JsonProperty.class);
if (annotation.isPresent()) {
jsonName = annotation.get().value();
} else {
jsonName = name;
}
}
return jsonName;
}

public Type getImplementationType() {
if (field != null) {
return field.getGenericType();
}
return getter.getGenericReturnType();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ public class BeanInformation {

private Map<String, BeanAttributeInformation> attributeMap = new HashMap<>();

private Map<String, BeanAttributeInformation> jsonAttributeMap = new HashMap<>();

private List<String> attributeNames = new ArrayList<>();

private BeanInformation superType;
Expand Down Expand Up @@ -76,6 +78,7 @@ public BeanInformation(Class implementationClass) {
String name = attrDesc.getName();
Class<?> attrType = attrDesc.getImplementationClass();
attrDesc.setSetter(ClassUtils.findSetter(implementationClass, name, attrType));
jsonAttributeMap.put(attrDesc.getJsonName(), attrDesc);
}

if (implementationClass.getSuperclass() != null && implementationClass.getSuperclass() != Object.class) {
Expand All @@ -99,6 +102,11 @@ public BeanAttributeInformation getAttribute(String name) {
return attributeMap.get(name);
}

public BeanAttributeInformation getAttributeByJsonName(String jsonName) {
return jsonAttributeMap.get(jsonName);
}


public List<String> getAttributeNames() {
return attributeNames;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package io.crnk.core.engine.registry;

import io.crnk.core.engine.http.HttpRequestContext;
import io.crnk.core.engine.information.resource.ResourceInformation;
import io.crnk.core.engine.query.QueryContext;
import io.crnk.core.engine.url.ServiceUrlProvider;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
package io.crnk.core.queryspec.internal;

import io.crnk.core.engine.document.ErrorData;
import io.crnk.core.engine.http.HttpStatus;
import io.crnk.core.engine.information.bean.BeanAttributeInformation;
import io.crnk.core.engine.information.bean.BeanInformation;
import io.crnk.core.engine.information.resource.ResourceField;
import io.crnk.core.engine.information.resource.ResourceFieldType;
import io.crnk.core.engine.information.resource.ResourceInformation;
import io.crnk.core.engine.internal.utils.ClassUtils;
import io.crnk.core.engine.internal.utils.MultivaluedMap;
import io.crnk.core.engine.internal.utils.StringUtils;
import io.crnk.core.engine.registry.RegistryEntry;
import io.crnk.core.engine.registry.ResourceRegistry;
import io.crnk.core.exception.BadRequestException;
import io.crnk.core.queryspec.mapper.QueryPathResolver;
import io.crnk.core.queryspec.mapper.QueryPathSpec;
import io.crnk.core.queryspec.mapper.QuerySpecUrlContext;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;

// TODO eventually ResourceInformation should alo provide information abaout nested non-resource types to make use of reflection here unncessary
public class DefaultQueryPathResolver implements QueryPathResolver {

private QuerySpecUrlContext ctx;

private boolean allowUnknownAttributes = false;

private boolean mapJsonNames = true;

@Override
public void init(QuerySpecUrlContext ctx) {
this.ctx = ctx;
}

@Override
public QueryPathSpec resolve(ResourceInformation resourceInformation, List<String> attributePath, NamingType sourceNamingType, String sourceParameter) {
if (attributePath == null) {
// no attribute specified, query string expected, use String
return new QueryPathSpec(String.class, null);
}

ResourceRegistry resourceRegistry = ctx.getResourceRegistry();
Type valueType = resourceInformation.getResourceClass();

SubTypeMap subTypeMap = null;

List<String> targetPath = new ArrayList<>();

for (String sourceAttributePath : attributePath) {
if (resourceInformation != null) {
ResourceField field = sourceNamingType == NamingType.JSON ? resourceInformation.findFieldByName(sourceAttributePath) : resourceInformation.findFieldByUnderlyingName(sourceAttributePath);
if (field == null) {
// search subtypes for field if not available on main resource
if (subTypeMap == null) {
subTypeMap = new SubTypeMap(resourceRegistry);
}
List<ResourceInformation> subTypes = subTypeMap.findSubTypes(resourceInformation.getResourceType());
for (ResourceInformation subType : subTypes) {
field = sourceNamingType == NamingType.JSON ? subType.findFieldByName(sourceAttributePath) : subType.findFieldByUnderlyingName(sourceAttributePath);
if (field != null) {
break;
}
}
}

if (field != null) {
if (field.getResourceFieldType() == ResourceFieldType.RELATIONSHIP) {
RegistryEntry entry = resourceRegistry.getEntry(field.getOppositeResourceType());
resourceInformation = entry.getResourceInformation();
valueType = resourceInformation.getResourceClass();
} else {
resourceInformation = null;
valueType = field.getElementType();
}
targetPath.add(sourceNamingType == NamingType.JSON ? field.getUnderlyingName() : field.getJsonName());
continue;
}
} else {
resourceInformation = null;
if (valueType == Object.class) {
targetPath.add(sourceAttributePath);
continue;
} else if (Map.class.isAssignableFrom(ClassUtils.getRawType(valueType))) {
if (valueType instanceof ParameterizedType) {
valueType = ((ParameterizedType) valueType).getActualTypeArguments()[1];
} else {
valueType = Object.class;
}
targetPath.add(sourceAttributePath);
continue;
} else {
BeanInformation beanInformation = BeanInformation.get(ClassUtils.getRawType(valueType));
BeanAttributeInformation attribute = sourceNamingType == NamingType.JSON ? beanInformation.getAttributeByJsonName(sourceAttributePath) : beanInformation.getAttribute(sourceAttributePath);
if (attribute != null) {
valueType = attribute.getImplementationType();
targetPath.add(sourceNamingType == NamingType.JSON ? attribute.getName() : attribute.getJsonName());
continue;
}
}
}

if (allowUnknownAttributes) {
targetPath.add(sourceAttributePath);
valueType = Object.class;
} else {
ErrorData errorData = ErrorData.builder()
.setCode("UNKNOWN_PARAMETER")
.setTitle("unknown parameter")
.setDetail("Failed to resolve path to field '" + StringUtils.join(".", attributePath) + "'")
.setSourceParameter(sourceParameter)
.setStatus(String.valueOf(HttpStatus.BAD_REQUEST_400)).build();
throw new BadRequestException(HttpStatus.BAD_REQUEST_400, errorData);
}
}

List<String> path = mapJsonNames ? targetPath : attributePath;
return new QueryPathSpec(valueType, path);
}

@Override
public boolean getAllowUnknownAttributes() {
return allowUnknownAttributes;
}

@Override
public void setAllowUnknownAttributes(boolean allowUnknownAttributes) {
this.allowUnknownAttributes = allowUnknownAttributes;
}

@Override
public boolean getMapJsonNames() {
return mapJsonNames;
}

@Override
public void setMapJsonNames(boolean mapJsonNames) {
this.mapJsonNames = mapJsonNames;
}

class SubTypeMap {
private MultivaluedMap<String, ResourceInformation> mapping = new MultivaluedMap();

public SubTypeMap(ResourceRegistry resourceRegistry) {

Collection<RegistryEntry> entries = resourceRegistry.getResources();
for (RegistryEntry entry : entries) {
ResourceInformation resourceInformation = entry.getResourceInformation();
String superResourceType = resourceInformation.getSuperResourceType();
if (superResourceType != null) {
mapping.add(superResourceType, resourceInformation);
}
}
}

public List<ResourceInformation> findSubTypes(String resourceType) {
List<ResourceInformation> results = new ArrayList<>();
findSubTypes(results, resourceType);
return results;
}

private void findSubTypes(List<ResourceInformation> results, String resourceType) {
if (mapping.containsKey(resourceType)) {
List<ResourceInformation> children = mapping.getList(resourceType);
// add direct descendants first
for (ResourceInformation child : children) {
results.add(child);
}
// only after add transitive children
for (ResourceInformation child : children) {
findSubTypes(results, child.getResourceType());
}
}
}
}


}
Loading

0 comments on commit 3fd16b0

Please sign in to comment.