package org.restler.spring.data.methods.associations; import com.fasterxml.jackson.annotation.JsonFilter; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.JsonSerializable; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectWriter; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.jsontype.TypeSerializer; import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.ser.FilterProvider; import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter; import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider; import org.restler.client.RestlerException; import org.restler.spring.data.proxy.ResourceProxy; import org.restler.spring.data.util.Placeholder; import org.restler.spring.data.util.Repositories; import org.restler.spring.data.util.ResourceHelper; import org.restler.util.Pair; import javax.persistence.Entity; import javax.persistence.ManyToMany; import javax.persistence.ManyToOne; import javax.persistence.OneToMany; import java.io.IOException; import java.lang.reflect.Field; import java.util.*; import java.util.stream.Collectors; public class ResourcesAndAssociations { //class for creating filter @JsonFilter("filter properties by name") private class PropertyFilterMixIn {} private static final ObjectMapper objectMapper = new ObjectMapper(); static { objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); objectMapper.addMixIn(Object.class, PropertyFilterMixIn.class); } private final Repositories repositories; private final String baseUri; private final List<AssociatedResource> resources = new ArrayList<>(); private final List<Association> associations = new ArrayList<>(); private final Map<AssociatedResource, List<Association>> associationsByResource = new HashMap<>(); public ResourcesAndAssociations(Repositories repositories, String baseUri, Object resource) { this.repositories = repositories; this.baseUri = baseUri; fillResourcesAndAssociations(resource, new HashSet<>()); } public List<AssociatedResource> getResources() { return resources; } public List<Association> getAssociations() { return associations; } public List<Association> getAssociationsByResource(AssociatedResource resource) { List<Association> associations = associationsByResource.get(resource); if(associations != null) { return associations; } return new ArrayList<>(); } //recursive method, get child resources for some resource and associations between them private AssociatedResource fillResourcesAndAssociations(Object object, Set<Object> set) { Object objectAtStart = object; set.add(object); if(object instanceof ResourceProxy) { object = ((ResourceProxy) object).getObject(); } List<Field> associateFields = new ArrayList<>(); List<Pair<Field, Object>> children = getChildren(object). stream(). filter(o -> takeNullFields(associateFields, o)). filter(this::isResourceOrCollection). collect(Collectors.toList()); List<String> ignorableFields = new ArrayList<>(); AssociatedResourceState resourceState = (objectAtStart instanceof ResourceProxy) ? AssociatedResourceState.Update : AssociatedResourceState.Create; AssociatedResource currentResource = new AssociatedResource(objectAtStart, new ObjectNode(JsonNodeFactory.instance), associateFields, resourceState); resources.add(currentResource); for(Pair<Field, Object> child : children) { child.getFirstValue().setAccessible(true); if(!set.contains(child.getSecondValue())) { if (child.getSecondValue() instanceof Collection) { ((Collection) child.getSecondValue()).stream().filter(item -> !set.contains(item)).forEach(item -> { AssociatedResource childResource = fillResourcesAndAssociations(item, set); List<Association> associateResult = associate(currentResource, child.getFirstValue(), childResource); associateResult.forEach(associations::add); associateResult.forEach(this::addAssociationByResource); }); } else { AssociatedResource childResource = fillResourcesAndAssociations(child.getSecondValue(), set); List<Association> associateResult = associate(currentResource, child.getFirstValue(), childResource); associateResult.forEach(associations::add); associateResult.forEach(this::addAssociationByResource); } } ignorableFields.add(child.getFirstValue().getName()); associateFields.add(child.getFirstValue()); child.getFirstValue().setAccessible(false); } //filtering associations fields FilterProvider filters = new SimpleFilterProvider() .addFilter("filter properties by name", SimpleBeanPropertyFilter.serializeAllExcept(ignorableFields.toArray(new String[ignorableFields.size()]))); ObjectWriter writer = objectMapper.writer(filters); try { //creates base json body without associations ObjectNode node = (ObjectNode) objectMapper.readTree(writer.writeValueAsString(object)); currentResource.getObjectNode().setAll(node); } catch (IOException e) { throw new RestlerException("Can't convert object to json", e); } associations.forEach(a -> a.getSecondResource().addIdPlaceholder(a.getIdPlaceholder())); return currentResource; } //create associations for parent and child resources private List<Association> associate(AssociatedResource parent, Field childField, AssociatedResource resource) { Object parentResource = parent.getResource(); if(parentResource instanceof ResourceProxy) { parentResource = ((ResourceProxy)parentResource).getObject(); } Object childResource = resource.getResource(); if(childResource instanceof ResourceProxy) { childResource = ((ResourceProxy) childResource).getObject(); } List<Association> result = new ArrayList<>(); for(Field nullField : resource.getAssociateFields()) { //OneToOne oneToOneChild = nullField.getAnnotation(OneToOne.class); ManyToOne manyToOneChild = nullField.getAnnotation(ManyToOne.class); OneToMany oneToManyChild = nullField.getAnnotation(OneToMany.class); ManyToMany manyToManyChild = nullField.getAnnotation(ManyToMany.class); //OneToOne oneToOneParent = childField.getAnnotation(OneToOne.class); ManyToOne manyToOneParent = childField.getAnnotation(ManyToOne.class); OneToMany oneToManyParent = childField.getAnnotation(OneToMany.class); ManyToMany manyToManyParent = childField.getAnnotation(ManyToMany.class); if(manyToOneChild != null) { if(oneToManyParent != null && (oneToManyParent.mappedBy().equals(nullField.getName()) || nullField.getName().equals(parentResource.getClass().getSimpleName().toLowerCase()))) { Optional<Object> id = Optional.ofNullable(ResourceHelper.getId(parent.getResource())); Placeholder<Object> idPlaceholder = new Placeholder<>(id.orElse("{missing id}").toString()); result.add(new Association(resource, parent, new Pair<>(nullField.getName(), ResourceHelper.getUri(repositories, baseUri, parent.getResource(), idPlaceholder)), AssociationType.ManyToOne, idPlaceholder)); if(ResourceHelper.getId(resource.getResource()) != null) { result.add(new Association(parent, resource, new Pair<>(childField.getName(), ResourceHelper.getUri(repositories, baseUri, resource.getResource())), AssociationType.OneToMany)); } return result; } } if(oneToManyChild != null) { if(oneToManyChild.mappedBy().equals(childField.getName()) || childField.getName().equals(resource.getClass().getSimpleName().toLowerCase())) { if(manyToOneParent != null) { Optional<Object> id = Optional.ofNullable(ResourceHelper.getId(parent.getResource())); Placeholder<Object> idPlaceholder = new Placeholder<>(id.orElse("{missing id}").toString()); result.add(new Association(parent, resource, new Pair<>(childField.getName(), ResourceHelper.getUri(repositories, baseUri, resource.getResource(), idPlaceholder)), AssociationType.ManyToOne, idPlaceholder)); } if(ResourceHelper.getId(parent.getResource()) != null) { result.add(new Association(resource, parent, new Pair<>(childField.getName(), ResourceHelper.getUri(repositories, baseUri, parent.getResource())), AssociationType.OneToMany)); } return result; } } if(manyToManyChild != null) { if(manyToManyChild.mappedBy().equals(childField.getName()) || (childResource.getClass().getSimpleName().toLowerCase() + "s").equals(childField.getName()) || (manyToManyParent != null && manyToManyParent.mappedBy().equals(nullField.getName())) || (parentResource.getClass().getSimpleName().toLowerCase() + "s").equals(nullField.getName())) { if(manyToManyParent != null) { Optional<Object> id = Optional.ofNullable(ResourceHelper.getId(parent.getResource())); Placeholder<Object> idPlaceholder = new Placeholder<>(id.orElse("{missing id}").toString()); result.add(new Association(parent, resource, new Pair<>(childField.getName(), ResourceHelper.getUri(repositories, baseUri, resource.getResource(), idPlaceholder)), AssociationType.ManyToMany, idPlaceholder)); } Optional<Object> id = Optional.ofNullable(ResourceHelper.getId(parent.getResource())); Placeholder<Object> idPlaceholder = new Placeholder<>(id.orElse("{missing id}").toString()); result.add(new Association(resource, parent, new Pair<>(nullField.getName(), ResourceHelper.getUri(repositories, baseUri, parent.getResource(), idPlaceholder)), AssociationType.ManyToMany, idPlaceholder)); return result; } } } throw new RestlerException("Can't make association."); } private void addAssociationByResource(Association association) { List<Association> resourceAssociations = associationsByResource.get(association.getFirstResource()); if(resourceAssociations != null) { resourceAssociations.add(association); } else { resourceAssociations = new ArrayList<>(); resourceAssociations.add(association); associationsByResource.put(association.getFirstResource(), resourceAssociations); } } private List<Pair<Field, Object>> getChildren(Object object) { List<Pair<Field, Object>> result = new ArrayList<>(); if(object instanceof ResourceProxy) { object = ((ResourceProxy) object).getObject(); } Class<?> argClass = object.getClass(); Field[] fields = argClass.getDeclaredFields(); for(Field field : fields) { try { field.setAccessible(true); Object fieldValue = field.get(object); result.add(new Pair<>(field, fieldValue)); field.setAccessible(false); } catch (IllegalAccessException e) { throw new RestlerException("Can't get value from field", e); } } return result; } private boolean isResourceOrCollection(Pair<Field, Object> item) { Object value = item.getSecondValue(); if(value instanceof ResourceProxy) { value = ((ResourceProxy)item.getSecondValue()).getObject(); } return value.getClass().isAnnotationPresent(Entity.class) || value instanceof Collection; } private boolean takeNullFields(List<Field> nullFields, Pair<Field, Object> object) { if(object.getSecondValue() == null) { nullFields.add(object.getFirstValue()); return false; } return true; } }