BsonPatch.java

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 io.github.bsonpatch;

import org.bson.BsonArray;
import org.bson.BsonNull;
import org.bson.BsonValue;

import java.util.EnumSet;
import java.util.Iterator;

import static io.github.bsonpatch.InPlaceApplyProcessor.cloneBsonValue;

public final class BsonPatch {

    private BsonPatch() {}

    private static BsonValue getPatchStringAttr(BsonValue bsonNode, String attr) {
        BsonValue child = getPatchAttr(bsonNode, attr);

        if (!child.isString())
            throw new InvalidBsonPatchException("Invalid JSON Patch payload (non-text '" + attr + "' field)");

        return child;
    }

    private static BsonValue getPatchAttr(BsonValue bsonNode, String attr) {
    	BsonValue child = bsonNode.asDocument().get(attr);
        if (child == null)
            throw new InvalidBsonPatchException("Invalid BSON Patch payload (missing '" + attr + "' field)");

        return child;
    }

    private static BsonValue getPatchAttrWithDefault(BsonValue bsonNode, String attr, BsonValue defaultValue) {
    	BsonValue child = bsonNode.asDocument().get(attr);
        if (child == null)
            return defaultValue;
        else
            return child;
    }

    private static void process(BsonArray patch, BsonPatchProcessor processor, EnumSet<CompatibilityFlags> flags)
            throws InvalidBsonPatchException {

        Iterator<BsonValue> operations = patch.iterator();
        while (operations.hasNext()) {
        	BsonValue bsonNode = operations.next();
            if (!bsonNode.isDocument()) throw new InvalidBsonPatchException("Invalid BSON Patch payload (not an object)");
            Operation operation = Operation.fromRfcName(getPatchStringAttr(bsonNode, Constants.OP).asString().getValue().replaceAll("\"", ""));
            JsonPointer path = JsonPointer.parse(getPatchStringAttr(bsonNode, Constants.PATH).asString().getValue());

            try {
	            switch (operation) {
		            case REMOVE: {
		                processor.remove(path);
		                break;
		            }
		
		            case ADD: {
		            	BsonValue value;
		                if (!flags.contains(CompatibilityFlags.MISSING_VALUES_AS_NULLS))
		                    value = getPatchAttr(bsonNode, Constants.VALUE);
		                else
		                    value = getPatchAttrWithDefault(bsonNode, Constants.VALUE, BsonNull.VALUE);
		                processor.add(path, cloneBsonValue(value));
		                break;
		            }
		
		            case REPLACE: {
		            	BsonValue value;
		                if (!flags.contains(CompatibilityFlags.MISSING_VALUES_AS_NULLS))
		                    value = getPatchAttr(bsonNode, Constants.VALUE);
		                else
		                    value = getPatchAttrWithDefault(bsonNode, Constants.VALUE, BsonNull.VALUE);
		                processor.replace(path, cloneBsonValue(value));
		                break;
		            }
		
		            case MOVE: {
		                JsonPointer fromPath = JsonPointer.parse(getPatchStringAttr(bsonNode, Constants.FROM).asString().getValue());
		                processor.move(fromPath, path);
		                break;
		            }
		
		            case COPY: {
		                JsonPointer fromPath = JsonPointer.parse(getPatchStringAttr(bsonNode, Constants.FROM).asString().getValue());
		                processor.copy(fromPath, path);
		                break;
		            }
		
		            case TEST: {
		            	BsonValue value;
		                if (!flags.contains(CompatibilityFlags.MISSING_VALUES_AS_NULLS))
		                    value = getPatchAttr(bsonNode, Constants.VALUE);
		                else
		                    value = getPatchAttrWithDefault(bsonNode, Constants.VALUE, BsonNull.VALUE);
		                processor.test(path, cloneBsonValue(value));
		                break;
		            }
	            }
            }
            catch (JsonPointerEvaluationException e) {
                throw new BsonPatchApplicationException(e.getMessage(), operation, e.getPath());
            }
         }
    }

    public static void validate(BsonArray patch, EnumSet<CompatibilityFlags> flags) throws InvalidBsonPatchException {
        process(patch, NoopProcessor.INSTANCE, flags);
    }

    public static void validate(BsonArray patch) throws InvalidBsonPatchException {
        validate(patch, CompatibilityFlags.defaults());
    }

    public static BsonValue apply(BsonArray patch, BsonValue source, EnumSet<CompatibilityFlags> flags) throws BsonPatchApplicationException {
        CopyingApplyProcessor processor = new CopyingApplyProcessor(source, flags);
        process(patch, processor, flags);
        return processor.result();
    }

    public static BsonValue apply(BsonArray patch, BsonValue source) throws BsonPatchApplicationException {
        return apply(patch, source, CompatibilityFlags.defaults());
    }

    public static void applyInPlace(BsonArray patch, BsonValue source) {
        applyInPlace(patch, source, CompatibilityFlags.defaults());
    }

    public static void applyInPlace(BsonArray patch, BsonValue source, EnumSet<CompatibilityFlags> flags) {
        InPlaceApplyProcessor processor = new InPlaceApplyProcessor(source, flags);
        process(patch, processor, flags);
    }

}