/* * Copyright 2015 Couchbase, Inc. * * 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.couchbase.mock.subdoc; import com.google.gson.*; import com.google.gson.stream.JsonReader; import org.json.simple.JSONValue; import org.json.simple.parser.ParseException; import java.io.StringReader; import java.math.BigInteger; import java.util.ArrayList; import java.util.List; public class Executor { public final static Gson gs = new Gson(); private final Path path; private final Operation code; private final JsonElement value; private final boolean isCreate; private final boolean isMultiValue; private final Match match; private static <T> T parseStrictJson(String text, Class<T> klass) { try { JSONValue.parseWithException(text); } catch (ParseException ex) { throw new JsonSyntaxException(ex); } catch (NumberFormatException ex2) { // Ignore number formats. GSON uses BigInteger if it's too big anyway. It's perfectly valid JSON } JsonReader reader = new JsonReader(new StringReader(text)); reader.setLenient(false); return gs.fromJson(reader, klass); } private Executor(String input, Path path, Operation code, String valueFragment, boolean shouldCreateParents) throws SubdocException { this.path = path; this.code = code; this.isCreate = shouldCreateParents; if (code.requiresValue()) { if (valueFragment == null || valueFragment.isEmpty()) { throw new EmptyValueException(); } try { if (code.isArrayParent()) { valueFragment = "[" + valueFragment + "]"; JsonArray arr = parseStrictJson(valueFragment, JsonArray.class); if (arr.size() > 1) { value = arr; isMultiValue = true; } else { value = arr.get(0); isMultiValue = false; } } else { valueFragment = "{\"K\":" + valueFragment + "}"; JsonObject obj = parseStrictJson(valueFragment, JsonObject.class); if (obj.getAsJsonObject().entrySet().size() != 1) { throw new CannotInsertException("More than one value found in object!"); } value = obj.get("K"); isMultiValue = false; } } catch (JsonSyntaxException ex) { if (code == Operation.COUNTER) { throw new BadNumberException(ex); } else { throw new CannotInsertException(ex); } } } else { value = null; isMultiValue = false; } if (isMultiValue && !code.allowsMultiValue()) { throw new CannotInsertException("Multi value not allowed!"); } JsonElement root; try { root = parseStrictJson(input, JsonElement.class); } catch (JsonSyntaxException e) { throw new DocNotJsonException(e); } match = new Match(root, path); } public static JsonElement executeGet(String input, String path) throws SubdocException { return execute(input, path, Operation.GET).getMatch(); } public static Result execute(String input, String path, Operation code) throws SubdocException { return execute(input, new Path(path), code); } public static Result execute(String input, String path, Operation code, String valueFragment, boolean isMkdirP) throws SubdocException { return execute(input, new Path(path), code, valueFragment, isMkdirP); } public static Result execute(String input, String path, Operation code, String valueFragment) throws SubdocException { return execute(input, path, code, valueFragment, false); } public static Result execute(String input, Path path, Operation code) throws SubdocException { // For Get, Exists and Delete return execute(input, path, code, null, false); } public static Result execute(String input, Path path, Operation code, String valueFragment, boolean isMkdirP) throws SubdocException { Executor p = new Executor(input, path, code, valueFragment, isMkdirP); p.match.execute(); return p.operate(); } private void insertInJsonArray(JsonArray array, int index) { // Because JsonArray doesn't implement Collection or List, we need // to use a temporary list, and then reassemble the contents into // an array List<JsonElement> elements = new ArrayList<JsonElement>(); while (array.size() > 0) { elements.add(array.remove(0)); } List<JsonElement> newElements = new ArrayList<JsonElement>(); if (isMultiValue) { for (JsonElement elem : value.getAsJsonArray()) { newElements.add(elem); } } else { newElements.add(value); } elements.addAll(index, newElements); for (JsonElement elem : elements) { array.add(elem); } } enum ParentType { ARRAY, OBJECT } private void createParents(ParentType parentType, JsonElement newValue) throws SubdocException { if (match.getDeepest().isJsonArray()) { throw new PathMismatchException("Cannot create intermediate array!"); } List<JsonElement> chain = match.getChain(); // Get the first missing component index. This is the size of the chain, less two int lastIndex = chain.size() - 2; for (int i = lastIndex + 1; i < path.size() - 1; i++) { Component comp = path.get(i); if (comp.isIndex()) { // Cannot insert elements with MKDIR_P throw new PathNotFoundException(); } JsonObject nextParent = new JsonObject(); JsonObject prevParent = chain.get(chain.size()-1).getAsJsonObject(); chain.add(nextParent); prevParent.add(comp.getString(), nextParent); } Component lastComp = path.getLast(); if (lastComp.isIndex()) { throw new PathNotFoundException(); } JsonObject deepest = chain.get(chain.size()-1).getAsJsonObject(); if (parentType == ParentType.ARRAY) { // ADD_UNIQUE, ARRAY_PREPEND, ARRAY_APPEND JsonArray parentArray = new JsonArray(); parentArray.add(newValue); deepest.add(lastComp.getString(), parentArray); } else { deepest.add(lastComp.getString(), newValue); } } private void replace(JsonElement newValue) throws SubdocException { if (!match.isFound()) { throw new PathNotFoundException(); } else if (path.size() == 0) { throw new CannotInsertException("Cannot replace root element!"); } JsonElement parent = match.getMatchParent(); Component comp = path.getLast(); if (comp.isIndex()) { int index = comp.getIndex(); JsonArray array = parent.getAsJsonArray(); if (index == -1) { index = parent.getAsJsonArray().size()-1; } array.set(index, value); } else { parent.getAsJsonObject().add(comp.getString(), newValue); } } private JsonElement dictAdd(JsonElement newValue) throws SubdocException { if (match.isFound()) { throw new PathExistsException(); } if (!match.hasImmediateParent()) { if (!isCreate) { throw new PathNotFoundException(); } createParents(ParentType.OBJECT, newValue); return match.getRoot(); } Component lastComp = path.getLast(); JsonElement parent = match.getImmediateParent(); if (!parent.isJsonObject()) { throw new PathMismatchException("DICT_ADD must have dictionary parent"); } parent.getAsJsonObject().add(lastComp.getString(), newValue); return match.getRoot(); } private void ensureUnique(JsonArray array) throws SubdocException { if (!value.isJsonPrimitive() && !value.isJsonNull()) { throw new CannotInsertException("Cannot verify uniqueness with non-primitives"); } String valueString = value.toString(); for (int i = 0; i < array.size(); i++) { JsonElement e = array.get(i); if (!e.isJsonPrimitive()) { throw new PathMismatchException("Values in the array are not all primitives"); } if (e.toString().equals(valueString)) { throw new PathExistsException(); } } } private void arrayAdd() throws SubdocException { if (!match.isFound()) { if (isCreate) { createParents(ParentType.ARRAY, value); return; } else { throw new PathNotFoundException(); } } JsonElement lastElem = match.getDeepest(); if (!lastElem.isJsonArray()) { throw new PathMismatchException(); } JsonArray array = lastElem.getAsJsonArray(); if (code == Operation.ADD_UNIQUE) { ensureUnique(array); } insertInJsonArray(array, code == Operation.ARRAY_APPEND ? array.size() : 0); } private void arrayInsert() throws SubdocException { JsonArray array; int position = path.getLast().getIndex(); if (match.hasImmediateParent()) { array = match.getImmediateParent().getAsJsonArray(); } else { throw new PathNotFoundException(); } if (position == -1) { throw new InvalidPathException("Insert does not accept negative arrays"); } else if (position > array.size()) { // Index is too big! throw new PathNotFoundException(); } else { insertInJsonArray(array, position); } } private Result remove() throws SubdocException { if (!match.isFound()) { throw new PathNotFoundException(); } else if (path.size() == 0) { throw new CannotInsertException("Cannot delete root element!"); } JsonElement parent = match.getImmediateParent(); JsonElement removedElement; Component lastComp = path.getLast(); if (parent.isJsonObject()) { removedElement = parent.getAsJsonObject().remove(lastComp.getString()); } else { JsonArray array = parent.getAsJsonArray(); int index = lastComp.getIndex(); if (index == -1) { index = array.size() - 1; } removedElement = array.remove(index); } return new Result(removedElement, match.getRoot()); } static private boolean bigintIsWithinRange(BigInteger ee) { BigInteger longMax = new BigInteger(Long.toString(Long.MAX_VALUE)); return ee.compareTo(longMax) <= 0; } private static JsonElement getCount(JsonElement elem) throws SubdocException { if (elem.isJsonObject()) { return new JsonPrimitive(elem.getAsJsonObject().entrySet().size()); } else if (elem.isJsonArray()) { return new JsonPrimitive(elem.getAsJsonArray().size()); } else { throw new PathMismatchException("GET_COUNT must point to array or dictionary"); } } private JsonElement counter() throws SubdocException { Long numres; Long delta; try { BigInteger ee = value.getAsBigInteger(); if (!bigintIsWithinRange(ee)) { throw new DeltaTooBigException(); } delta = ee.longValue(); } catch (NumberFormatException ex) { throw new BadNumberException(ex); } catch (UnsupportedOperationException ex2) { throw new BadNumberException(ex2); } if (delta == 0) { throw new ZeroDeltaException(); } if (match.isFound()) { try { BigInteger tmpCombo = match.getMatch().getAsBigInteger(); if (!bigintIsWithinRange(tmpCombo)) { throw new NumberTooBigException(); } numres = tmpCombo.longValue(); } catch (UnsupportedOperationException ex) { throw new PathMismatchException(ex); } catch (NumberFormatException ex2) { throw new PathMismatchException(ex2); } /* if (delta >= 0 && numres >= 0) { if (std::numeric_limits<int64_t>::max() - delta < numres) { return Error::DELTA_E2BIG; } } else if (delta < 0 && numres < 0) { if (delta < std::numeric_limits<int64_t>::min() - numres) { return Error::DELTA_E2BIG; } } */ if (delta >= 0 && numres >= 0) { if (Long.MAX_VALUE - delta < numres) { throw new DeltaTooBigException(); } } else if (delta < 0 && numres < 0) { if (delta < Long.MIN_VALUE - numres) { throw new DeltaTooBigException(); } } JsonPrimitive p = new JsonPrimitive(numres + delta); replace(p); return p; } else { JsonPrimitive newNum = new JsonPrimitive(delta); if (match.hasImmediateParent() && match.getImmediateParent().isJsonObject()) { dictAdd(newNum); } else if (isCreate && match.getDeepest().isJsonObject()) { createParents(ParentType.OBJECT, newNum); } else { throw new PathNotFoundException(); } return newNum; } } private Result operate() throws SubdocException { switch (code) { case GET: case EXISTS: case GET_COUNT: if (!match.isFound()) { throw new PathNotFoundException(); } else { if (code == Operation.GET_COUNT) { return new Result(getCount(match.getMatch()), null); } else { return new Result(match.getMatch(), null); } } case REPLACE: replace(value); return new Result(null, match.getRoot()); case DICT_UPSERT: if (path.getLast().isIndex()) { throw new InvalidPathException("DICT_UPSERT cannot have an array index as its last component"); } if (match.isFound()) { replace(value); } else { dictAdd(value); } return new Result(null, match.getRoot()); case DICT_ADD: dictAdd(value); return new Result(null, match.getRoot()); case ARRAY_PREPEND: case ARRAY_APPEND: case ADD_UNIQUE: arrayAdd(); return new Result(null, match.getRoot()); case ARRAY_INSERT: arrayInsert(); return new Result(null, match.getRoot()); case REMOVE: return remove(); case COUNTER: return new Result(counter(), match.getRoot()); default: throw new RuntimeException("Unknown operation!"); } } public static String getRootType(String path, Operation op) { if (path.isEmpty()) { switch (op) { case ARRAY_APPEND: case ARRAY_PREPEND: case ADD_UNIQUE: return "[]"; default: return null; } } if (path.charAt(0) == '[') { return "[]"; } else { return "{}"; } } }