package org.restler.spring.data.methods;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.collect.ImmutableMultimap;
import org.restler.client.Call;
import org.restler.client.RestlerException;
import org.restler.http.HttpCall;
import org.restler.http.HttpMethod;
import org.restler.spring.data.calls.ChainCall;
import org.restler.spring.data.methods.associations.*;
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.UriBuilder;
import org.springframework.data.repository.CrudRepository;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BinaryOperator;
import java.util.stream.Collectors;
/**
* CrudRepository save method implementation.
*/
public class SaveRepositoryMethod extends DefaultRepositoryMethod {
private static final Method saveMethod;
private static final ObjectMapper objectMapper = new ObjectMapper();
static {
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
try {
saveMethod = CrudRepository.class.getMethod("save", Object.class);
} catch (NoSuchMethodException e) {
throw new RestlerException("Can't find CrudRepository.save method.", e);
}
}
private final String baseUri;
private final Repositories repositories;
public SaveRepositoryMethod(String baseUri, Repositories repositories) {
this.baseUri = baseUri;
this.repositories = repositories;
}
@Override
public boolean isRepositoryMethod(Method method) {
return saveMethod.equals(method);
}
@Override
public Call getCall(URI uri, Class<?> declaringClass, Object[] args) {
Object resource = args[0];
SaveCalls saveCalls = new SaveCalls(resource);
Class<?> returnType = (resource instanceof ResourceProxy) ? ((ResourceProxy) resource).getObject().getClass() : resource.getClass();
return new ChainCall(new ResultAnalyzer(saveCalls.getCurrentCallNumber(), saveCalls.getCallNumberToPlaceholder()), saveCalls.getCalls(), returnType);
}
@Override
public String getPathPart(Object[] args) {
Object arg = args[0];
if(arg instanceof ResourceProxy) {
ResourceProxy resourceProxy = (ResourceProxy)arg;
return resourceProxy.getResourceId().toString();
}
return "";
}
private class ResultAnalyzer implements BinaryOperator<Object> {
private long targetCallNumber;
private final Map<Integer, List<Placeholder<Object>>> callNumberToPlaceholder;
private int currentCallNumber = 0;
public ResultAnalyzer(long targetCallNumber, Map<Integer, List<Placeholder<Object>>> callNumberToPlaceholder) {
this.targetCallNumber = targetCallNumber;
this.callNumberToPlaceholder = callNumberToPlaceholder;
}
@Override
public Object apply(Object prevResult, Object result) {
Object finalResult;
List<Placeholder<Object>> placeholders = callNumberToPlaceholder.get(currentCallNumber);
if(placeholders != null) {
placeholders.forEach(p -> {
if(p != null) {
p.setValue(ResourceHelper.getId(result));
}
});
}
if(currentCallNumber == targetCallNumber) {
finalResult = result;
} else {
finalResult = prevResult;
}
currentCallNumber++;
return finalResult;
}
}
private LazyBody getRequestBody(ObjectNode node) {
return new LazyBody(node);
}
private static class LazyBody {
private final ObjectNode objectNode;
public LazyBody(ObjectNode objectNode) {
this.objectNode = objectNode;
}
@Override
public String toString() {
String json;
try {
json = objectMapper.writeValueAsString(objectNode);
} catch (JsonProcessingException e) {
throw new RestlerException("Can't create json from object", e);
}
return json;
}
}
static public class LazyCall implements Call {
private final URI url;
private final HttpMethod method;
private final LazyBody requestBody;
private final ImmutableMultimap<String, String> headers;
private final Type returnType;
public LazyCall(URI url, HttpMethod method, LazyBody requestBody,
ImmutableMultimap<String, String> headers, Type returnType) {
this.url = url;
this.method = method;
this.requestBody = requestBody;
this.headers = headers;
this.returnType = returnType;
}
public LazyCall(URI url, HttpMethod method, LazyBody requestBody) {
this(url, method, requestBody, ImmutableMultimap.of(), Object.class);
}
@Override
public Type getReturnType() {
return returnType;
}
@Override
public Call withReturnType(Type type) {
return new LazyCall(url, method, requestBody, headers, type);
}
public HttpCall getCall() {
return new HttpCall(url, method, requestBody.toString(), headers, returnType);
}
}
private class SaveCalls {
private final Object currentResource;
private final List<Call> calls = new ArrayList<>();
private final Map<Integer, List<Placeholder<Object>>> callNumberToPlaceholder = new HashMap<>();
private Integer currentCallNumber;
public SaveCalls(Object resource) {
currentResource = resource;
ResourcesAndAssociations resourcesAndAssociations = new ResourcesAndAssociations(repositories, baseUri, resource);
createResources(resourcesAndAssociations).forEach(calls::add);
updateResources(resourcesAndAssociations).forEach(calls::add);
}
public List<Call> getCalls() {
return calls;
}
public Map<Integer, List<Placeholder<Object>>> getCallNumberToPlaceholder() {
return callNumberToPlaceholder;
}
public Integer getCurrentCallNumber() {
return currentCallNumber;
}
//create calls for creating resources
private List<Call> createResources(ResourcesAndAssociations resourcesAndAssociations) {
List<Call> calls = new ArrayList<>();
List<Call> associateCalls = new ArrayList<>();
//get resources that must be created
List<AssociatedResource> resources = resourcesAndAssociations.getResources().stream().
filter(r -> r.getState().equals(AssociatedResourceState.Create)).
collect(Collectors.toList());
//get all associations
List<Association> associations = resourcesAndAssociations.getAssociations();
Integer callNumber = callNumberToPlaceholder.size();
//while unresolved associations for creating resources not empty
while(!resources.isEmpty() && associations.stream().
filter(a->a.getFirstResource().getState().equals(AssociatedResourceState.Create)
&& !a.isResolved()).count() > 0) {
long resourcesCreated = 0;
for (AssociatedResource resource : resources) {
List<Association> associationsForCurrentResource = resourcesAndAssociations.getAssociationsByResource(resource);
//resolve associations for current resource
associationsForCurrentResource.stream().
forEach(this::resolveAssociationCreate);
//get resolved manyToOne associations
List<Association> resolvedManyToOne = associationsForCurrentResource.stream().
filter(a -> a.getAssociationType().equals(AssociationType.ManyToOne) && a.isResolved()).
collect(Collectors.toList());
//get count of manyToOne unresolved associations
long manyToOneCount =
associationsForCurrentResource.stream().
filter(a -> a.getAssociationType().equals(AssociationType.ManyToOne) &&
a.getSecondResource().getState().equals(AssociatedResourceState.Create) && !a.isResolved()).
count();
//if count of manyToOne unresolved associations is zero then add call for creating current resource
if (manyToOneCount == 0) {
resolvedManyToOne.forEach(a -> resource.getObjectNode().putPOJO(a.getJsonField().getFirstValue(), a.getJsonField().getSecondValue()));
calls.add(add(resource.getResource(), resource.getObjectNode()));
if(resource.getResource() == currentResource) {
currentCallNumber = callNumber;
}
callNumberToPlaceholder.put(callNumber++, resource.getIdPlaceholders());
resource.changeState(AssociatedResourceState.Done);
resourcesCreated++;
}
}
//if cant resolve associations and create resources
if(resourcesCreated == 0) {
Association firstManyToOneAssociation = associations.stream().
filter(a->!a.isResolved()&&a.getAssociationType().equals(AssociationType.ManyToOne)).
findFirst().orElse(null);
if(firstManyToOneAssociation != null) {
URI uri = new UriBuilder(ResourceHelper.getUri(repositories, baseUri,
firstManyToOneAssociation.getFirstResource())+"/"+firstManyToOneAssociation.getJsonField().getFirstValue()).build();
ImmutableMultimap<String, String> header = ImmutableMultimap.of("Content-Type", "text/uri-list");
/**
* The call creates request for associating parent and child.
* PUT uses for adding new associations between resources
* {@link http://docs.spring.io/spring-data/rest/docs/current/reference/html/#_put_2}
* */
associateCalls.add(new HttpCall(uri, HttpMethod.PUT,
firstManyToOneAssociation.getJsonField().getSecondValue().toString(), header, void.class));
firstManyToOneAssociation.markAsResolved();
} else {
throw new RestlerException("Can't resolve associations.");
}
}
resources = resources.stream().
filter(r -> r.getState().equals(AssociatedResourceState.Create)).
collect(Collectors.toList());
}
class IntegerHandler {
private Integer value;
}
IntegerHandler callNumberHandler = new IntegerHandler();
callNumberHandler.value = callNumber;
//if resources have not created yet
resources.stream().
filter(r -> r.getState().equals(AssociatedResourceState.Create)).
forEach(r -> {
calls.add(add(r.getResource(), r.getObjectNode()));
r.changeState(AssociatedResourceState.Done);
if(r.getResource() == currentResource) {
currentCallNumber = callNumberHandler.value;
}
callNumberToPlaceholder.put(callNumberHandler.value++, r.getIdPlaceholders());
}
);
callNumber = callNumberHandler.value;
associateCalls.forEach(calls::add);
for(int i = 0; i < associateCalls.size(); ++i) {
callNumberToPlaceholder.put(callNumber++, null);
}
return calls;
}
//resolve association for creating resource
private boolean resolveAssociationCreate(Association association) {
if(association == null) {
return false;
}
//if second resource of association manyToOne is created then association cant be resolved yet
if(association.getAssociationType().equals(AssociationType.ManyToOne)) {
boolean resolved = !association.getSecondResource().getState().equals(AssociatedResourceState.Create);
if(resolved) {
association.markAsResolved();
}
return resolved;
}
return resolveAssociation(association);
}
//resolve associations
private boolean resolveAssociation(Association association) {
ObjectNode objectNode = association.getFirstResource().getObjectNode();
String fieldName = association.getJsonField().getFirstValue();
Object fieldValue = association.getJsonField().getSecondValue();
AssociationType associationType = association.getAssociationType();
JsonNode jsonNode = objectNode.get(fieldName);
switch(associationType) {
case OneToMany:
case ManyToMany:
//add to json associations
if(jsonNode != null && jsonNode instanceof ArrayNode) {
ArrayNode arrayNode = (ArrayNode)jsonNode;
arrayNode.addPOJO(fieldValue);
} else {
objectNode.putArray(fieldName).addPOJO(fieldValue);
}
association.markAsResolved();
return true;
case ManyToOne:
//add to json association
objectNode.putPOJO(fieldName, fieldValue);
association.markAsResolved();
return true;
case OneToOne:
throw new RestlerException("Unsupported association oneToOne.");
}
return false;
}
//create calls for updating resources
private List<Call> updateResources(ResourcesAndAssociations resourcesAndAssociations) {
List<Call> calls = new ArrayList<>();
//get resources that must be created
List<AssociatedResource> resources = resourcesAndAssociations.getResources().stream().
filter(r -> r.getState().equals(AssociatedResourceState.Update)).
collect(Collectors.toList());
Integer callNumber = callNumberToPlaceholder.size();
for(AssociatedResource resource : resources) {
List<Association> associationsForCurrentResource = resourcesAndAssociations.getAssociationsByResource(resource);
//resolve associations for updating resource
associationsForCurrentResource.stream().
forEach(this::resolveAssociation);
calls.add(update((ResourceProxy) resource.getResource(), resource.getObjectNode()));
if(resource.getResource() == currentResource) {
currentCallNumber = callNumber;
}
callNumberToPlaceholder.put(callNumber++, resource.getIdPlaceholders());
resource.changeState(AssociatedResourceState.Done);
}
return calls;
}
//create call for adding new resource to repository
private Call add(Object object, ObjectNode node) {
LazyBody body = getRequestBody(node);
ImmutableMultimap<String, String> header = ImmutableMultimap.of("Content-Type", "application/json");
//POST uses for creating new entity
return new LazyCall(new UriBuilder(ResourceHelper.getRepositoryUri(repositories, baseUri, object)).build(),
HttpMethod.POST, body, header, object.getClass());
}
//create call for updating resource in repository
private Call update(ResourceProxy resource, ObjectNode node) {
LazyBody body = getRequestBody(node);
ImmutableMultimap<String, String> header = ImmutableMultimap.of("Content-Type", "application/json");
//PUT uses for replacing values
return new LazyCall(new UriBuilder(resource.getSelfUri()).build(), HttpMethod.PUT, body,
header, resource.getObject().getClass());
}
}
}