/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright (c) 2013-2015 ForgeRock AS. All Rights Reserved
*
* The contents of this file are subject to the terms
* of the Common Development and Distribution License
* (the License). You may not use this file except in
* compliance with the License.
*
* You can obtain a copy of the License at
* http://forgerock.org/license/CDDLv1.0.html
* See the License for the specific language governing
* permission and limitations under the License.
*
* When distributing Covered Code, include this CDDL
* Header Notice in each file and include the License file
* at http://forgerock.org/license/CDDLv1.0.html
* If applicable, add the following below the CDDL Header,
* with the fields enclosed by brackets [] replaced by
* your own identifying information:
* "Portions Copyrighted [year] [name of copyright owner]"
*/
package org.forgerock.openidm.patch;
import org.forgerock.json.JsonPointer;
import org.forgerock.json.JsonValue;
import org.forgerock.json.JsonValueException;
import org.forgerock.json.resource.BadRequestException;
import org.forgerock.json.resource.PatchOperation;
import org.forgerock.json.resource.ResourceException;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.Scriptable;
import java.util.List;
/**
*/
public class JsonValuePatch {
/** Apply an "add" PatchOperation. */
private static boolean add(JsonValue subject, PatchOperation operation) throws BadRequestException {
if (!operation.isAdd()) {
throw new BadRequestException("Operation is an " + operation.getOperation() + ", not an add!");
}
subject.putPermissive(operation.getField(), operation.getValue().getObject());
return true;
}
/** Apply a "remove" PatchOperation */
private static boolean remove(JsonValue subject, PatchOperation operation) throws BadRequestException {
if (!operation.isRemove()) {
throw new BadRequestException("Operation is an " + operation.getOperation() + ", not a remove!");
}
final JsonValue current = subject.get(operation.getField());
if (current == null || current.isNull()) {
return false;
}
if (operation.getValue() == null || operation.getValue().isNull()) {
// remove the field
subject.remove(operation.getField());
return true;
}
else {
// remove the value
if (current.isList()) {
// remove each instance of operation.getValue() from the list of values
while (current.asList().remove(operation.getValue().getObject())) {
// "iterate" until no operation.getValue() objects are in the list
}
return true;
} else {
if (operation.getValue().getObject().equals(current.getObject())) {
subject.remove(operation.getField());
return true;
}
}
return false;
}
}
/** Apply a "replace" PatchOperation */
private static boolean replace(JsonValue subject, PatchOperation operation) throws BadRequestException {
if (!operation.isReplace()) {
throw new BadRequestException("Operation is an " + operation.getOperation() + ", not a replace!");
}
subject.remove(operation.getField());
if (!operation.getValue().isNull()) {
subject.putPermissive(operation.getField(), operation.getValue().getObject());
}
return true;
}
/** Apply a "increment" PatchOperation */
private static boolean increment(JsonValue subject, PatchOperation operation) throws BadRequestException {
if (!operation.isIncrement()) {
throw new BadRequestException("Operation is an " + operation.getOperation() + ", not an increment!");
}
final JsonValue current = subject.get(operation.getField());
if (current == null) {
throw new BadRequestException("The field '" + operation.getField() + "' does not exist");
} else if (current.isList()) {
final List<Object> elements = current.asList();
for (int i = 0; i < elements.size(); i++) {
elements.set(i, increment(elements.get(i), operation.getValue().asNumber(), operation.getField()));
}
} else {
subject.put(operation.getField(), increment(current.getObject(), operation.getValue().asNumber(), operation.getField()));
}
return true;
}
/** Helper function to deal with typecasting and incrementing of Object to appropriate Number object). */
private static Object increment(final Object object, final Number amount, final JsonPointer field) throws BadRequestException {
if (object instanceof Long) {
return ((Long) object) + amount.longValue();
} else if (object instanceof Integer) {
return ((Integer) object) + amount.intValue();
} else if (object instanceof Float) {
return ((Float) object) + amount.floatValue();
} else if (object instanceof Double) {
return ((Double) object) + amount.doubleValue();
} else {
throw new BadRequestException("The field '" + field + "' is not a number");
}
}
/** Apply a move patch operation */
private static boolean move(JsonValue subject, PatchOperation operation) throws BadRequestException {
if (!operation.isMove()) {
throw new BadRequestException("Operation is a " + operation.getOperation() + ", not a move!");
}
JsonValue value = subject.get(operation.getFrom());
if (value == null || value.isNull()) {
return false;
}
subject.remove(operation.getFrom());
subject.add(operation.getField(), value.getObject());
return true;
}
/** Apply a copy patch operation */
private static boolean copy(JsonValue subject, PatchOperation operation) throws BadRequestException {
if (!operation.isCopy()) {
throw new BadRequestException("Operation is a " + operation.getOperation() + ", not a copy!");
}
JsonValue value = subject.get(operation.getFrom());
if (value == null || value.isNull()) {
return false;
}
subject.add(operation.getField(), value.getObject());
return true;
}
/** Apply a transform patch operation */
private static boolean transform(JsonValue subject, PatchOperation operation, PatchValueTransformer transformer) throws BadRequestException {
if (!operation.isTransform()) {
throw new BadRequestException("Operation is a " + operation.getOperation() + ", not a transform!");
}
Object value = transformer.getTransformedValue(operation, subject);
if (value == null) {
subject.remove(operation.getField());
} else {
subject.put(operation.getField(), value);
}
return true;
}
/** An "unknown", or bad operation, implementation of patch application */
private static boolean unknown(JsonValue subject, PatchOperation operation) throws BadRequestException {
throw new BadRequestException("Operation " + operation.getOperation() + " is not supported");
}
private static final PatchValueTransformer DEFAULT_TRANSFORMER = new PatchValueTransformer() {
@Override
public Object getTransformedValue(PatchOperation patch, JsonValue subject) throws JsonValueException {
if (patch.getValue() != null) {
return evalScript(subject, patch.getValue());
}
throw new JsonValueException(patch.toJsonValue(), "expecting a value member");
}
private String evalScript(JsonValue content, JsonValue script) {
if (script == null || script.getObject() == null || !script.isString()) {
return null;
}
Context cx = Context.enter();
try {
Scriptable scope = cx.initStandardObjects();
String finalScript = "var content = " + content.toString() + "; " + script.getObject();
Object result = cx.evaluateString(scope, finalScript, "script", 1, null);
return Context.toString(result);
} catch (Exception e) {
throw new JsonValueException(script, "failed to eval script", e);
} finally {
Context.exit();
}
}
};
/**
* Apply a list of PatchOperations.
*
* @param subject the JsonValue to which to apply the patch operation(s).
* @return whether the subject was modified.
* @throws ResourceException on failure to apply PatchOperation.
*/
public static boolean apply(JsonValue subject, List<PatchOperation> operations) throws BadRequestException {
return apply(subject, operations, DEFAULT_TRANSFORMER);
}
/**
* Apply a list of PatchOperations.
*
* @param subject the JsonValue to which to apply the patch operation(s).
* @param transformer the value transformer used to compute the value to use for the operation.
* @return whether the subject was modified.
* @throws ResourceException on failure to apply PatchOperation.
*/
public static boolean apply(JsonValue subject, List<PatchOperation> operations, PatchValueTransformer transformer)
throws BadRequestException {
boolean isModified = false;
if (operations != null) {
for (final PatchOperation operation : operations) {
isModified |=
operation.isAdd() ? add(subject, operation)
: operation.isRemove() ? remove(subject, operation)
: operation.isReplace() ? replace(subject, operation)
: operation.isIncrement() ? increment(subject, operation)
: operation.isMove() ? move(subject, operation)
: operation.isCopy() ? copy(subject, operation)
: operation.isTransform() ? transform(subject, operation, transformer)
: unknown(subject, operation);
}
}
return isModified;
}
}