/* * Copyright 2014-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.rest.webmvc.json.patch; import static org.springframework.data.rest.webmvc.json.patch.PathToSpEL.*; import java.util.Collection; import java.util.List; import org.springframework.core.CollectionFactory; import org.springframework.core.convert.TypeDescriptor; import org.springframework.expression.Expression; import org.springframework.expression.ExpressionException; /** * Abstract base class representing and providing support methods for patch operations. * * @author Craig Walls * @author Mathias Düsterhöft * @author Oliver Gierke */ public abstract class PatchOperation { protected final String op; protected final String path; protected final Object value; protected final Expression spelExpression; /** * Constructs the operation. * * @param op the operation name. (e.g., 'move') * @param path the path to perform the operation on. (e.g., '/1/description') */ public PatchOperation(String op, String path) { this(op, path, null); } /** * Constructs the operation. * * @param op the operation name. (e.g., 'move') * @param path the path to perform the operation on. (e.g., '/1/description') * @param value the value to apply in the operation. Could be an actual value or an implementation of * {@link LateObjectEvaluator}. */ public PatchOperation(String op, String path, Object value) { this.op = op; this.path = path; this.value = value; this.spelExpression = pathToExpression(path); } /** * @return the operation name */ public String getOp() { return op; } /** * @return the operation path */ public String getPath() { return path; } /** * @return the operation's value (or {@link LateObjectEvaluator}) */ public Object getValue() { return value; } /** * Pops a value from the given path. * * @param target the target from which to pop a value. * @param removePath the path from which to pop a value. Must be a list. * @return the value popped from the list */ protected Object popValueAtPath(Object target, String removePath) { Integer listIndex = targetListIndex(removePath); Expression expression = pathToExpression(removePath); Object value = expression.getValue(target); if (listIndex == null) { try { expression.setValue(target, null); return value; } catch (NullPointerException e) { throw new PatchException("Path '" + removePath + "' is not nullable."); } } else { Expression parentExpression = pathToParentExpression(removePath); List<?> list = (List<?>) parentExpression.getValue(target); list.remove(listIndex >= 0 ? listIndex.intValue() : list.size() - 1); return value; } } /** * Adds a value to the operation's path. If the path references a list index, the value is added to the list at the * given index. If the path references an object property, the property is set to the value. * * @param target The target object. * @param value The value to add. */ @SuppressWarnings({ "unchecked", "null" }) protected void addValue(Object target, Object value) { Expression parentExpression = pathToParentExpression(path); Object parent = parentExpression != null ? parentExpression.getValue(target) : null; Integer listIndex = targetListIndex(path); if (parent == null || !(parent instanceof List) || listIndex == null) { TypeDescriptor descriptor = parentExpression.getValueTypeDescriptor(target); // Set as new collection if necessary if (descriptor.isCollection() && !Collection.class.isInstance(value)) { Collection<Object> collection = CollectionFactory.createCollection(descriptor.getType(), 1); collection.add(value); parentExpression.setValue(target, collection); } else { spelExpression.setValue(target, value); } } else { List<Object> list = (List<Object>) parentExpression.getValue(target); list.add(listIndex >= 0 ? listIndex.intValue() : list.size(), value); } } /** * Sets a value to the operation's path. * * @param target The target object. * @param value The value to set. */ protected void setValueOnTarget(Object target, Object value) { spelExpression.setValue(target, value); } /** * Retrieves a value from the operation's path. * * @param target the target object. * @return the value at the path on the given target object. */ protected Object getValueFromTarget(Object target) { try { return spelExpression.getValue(target); } catch (ExpressionException e) { throw new PatchException("Unable to get value from target", e); } } /** * Performs late-value evaluation on the operation value if the value is a {@link LateObjectEvaluator}. * * @param targetObject the target object, used as assistance in determining the evaluated object's type. * @param entityType the entityType * @param <T> the entity type * @return the result of late-value evaluation if the value is a {@link LateObjectEvaluator}; the value itself * otherwise. */ protected <T> Object evaluateValueFromTarget(Object targetObject, Class<T> entityType) { return value instanceof LateObjectEvaluator ? ((LateObjectEvaluator) value).evaluate(spelExpression.getValueType(targetObject)) : value; } /** * Perform the operation. * * @param target the target of the operation. */ abstract <T> void perform(Object target, Class<T> type); private Integer targetListIndex(String path) { String[] pathNodes = path.split("\\/"); String lastNode = pathNodes[pathNodes.length - 1]; if (APPEND_CHARACTERS.contains(lastNode)) { return -1; } try { return Integer.parseInt(lastNode); } catch (NumberFormatException e) { return null; } } }