Structure.java

/*
 * Copyright (c) 2020, 2022 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0, which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the
 * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
 * version 2 with the GNU Classpath Exception, which is available at
 * https://www.gnu.org/software/classpath/license.html.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 */

package ee.jakarta.tck.jsonp.api.jsonvaluetests;

import ee.jakarta.tck.jsonp.api.common.JsonAssert;
import ee.jakarta.tck.jsonp.api.common.PointerRFCObject;
import ee.jakarta.tck.jsonp.api.common.SimpleValues;
import ee.jakarta.tck.jsonp.api.common.TestResult;
import jakarta.json.Json;
import jakarta.json.JsonArray;
import jakarta.json.JsonException;
import jakarta.json.JsonObject;
import jakarta.json.JsonStructure;
import jakarta.json.JsonValue;

import java.util.logging.Logger;

// $Id$
/**
 * JavaScript Object Notation (JSON) compatibility tests for
 * {@link JsonStructure}. RFC 6901 JSON Pointer is being passed to
 * {@code JsonValue getValue(String)} method so whole JSON Pointer resolving
 * sample is being used to test this method.
 */
public class Structure {

  private static final Logger LOGGER = Logger.getLogger(Structure.class.getName());

  /**
   * Creates an instance of JavaScript Object Notation (JSON) compatibility
   * tests for {@link JsonStructure}.
   */
  Structure() {
    super();
  }

  /**
   * {@link JsonStructure} API methods added in JSON-P 1.1.
   * 
   * @return Result of all tests in this suite.
   */
  TestResult test() {
    final TestResult result = new TestResult(
        "JsonStructure API methods added in JSON-P 1.1.");
    LOGGER.info("JsonStructure API methods added in JSON-P 1.1.");
    testResolveWholeDocument(result);
    testResolveEmptyName(result);
    testResolveSimpleArray(result);
    testResolveSimpleArrayItems(result);
    testResolvePathWithEncodedSlash(result);
    testResolvePathWithSlash(result);
    testResolvePathWithPercent(result);
    testResolvePathWithCaret(result);
    testResolvePathWithVerticalBar(result);
    testResolvePathWithBackSlash(result);
    testResolvePathWithDoubleQuotes(result);
    testResolvePathWithSpace(result);
    testResolvePathWithTilde(result);
    testResolvePathWithEncodedTilde(result);
    testResolvePathWithEncodedTildeOne(result);
    testResolveValidNumericIndexInArray(result);
    testResolveMemberAfterLastInArray(result);
    testResolveNonNumericIndexInArray(result);
    return result;
  }

  /**
   * Test RFC 6901 JSON Pointer resolving for the whole document path using
   * {@code JsonValue getValue(String)}.
   * 
   * @param result
   *          Tests result record.
   */
  private void testResolveWholeDocument(final TestResult result) {
    final JsonStructure value = PointerRFCObject.createRFC6901Object();
    final JsonValue check = value;
    verifyGetValue(result, check, value, PointerRFCObject.RFC_KEY_WHOLE);
  }

  /**
   * Test RFC 6901 JSON Pointer resolver for {@code "": 0} using
   * {@code JsonValue getValue(String)}.
   * 
   * @param result
   *          Tests result record.
   */
  private void testResolveEmptyName(final TestResult result) {
    final JsonStructure value = PointerRFCObject.createRFC6901Object();
    final JsonValue check = Json.createValue(PointerRFCObject.RFC_VAL2);
    verifyGetValue(result, check, value, PointerRFCObject.RFC_PTR2);
  }

  /**
   * Test RFC 6901 JSON Pointer resolver for {@code "foo": ["bar", "baz"]} using
   * {@code JsonValue getValue(String)}.
   * 
   * @param result
   *          Tests result record.
   */
  private void testResolveSimpleArray(final TestResult result) {
    final JsonStructure value = PointerRFCObject.createRFC6901Object();
    final JsonValue check = PointerRFCObject.RFC_VAL1;
    verifyGetValue(result, check, value, PointerRFCObject.RFC_PTR1);
  }

  /**
   * Test RFC 6901 JSON Pointer resolver for {@code "foo": ["bar", "baz"]} array
   * elements using {@code JsonValue getValue(String)}.
   * 
   * @param result
   *          Tests result record.
   */
  private void testResolveSimpleArrayItems(final TestResult result) {
    final String[] itemPtrs = new String[] { PointerRFCObject.RFC_PTR1_ITEM1, PointerRFCObject.RFC_PTR1_ITEM2 };
    final String[] itemVals = new String[] { PointerRFCObject.RFC_VAL1_ITEM1, PointerRFCObject.RFC_VAL1_ITEM2 };
    final JsonObject value = PointerRFCObject.createRFC6901Object();
    for (int i = 0; i < itemPtrs.length; i++) {
      final JsonValue check = Json.createValue(itemVals[i]);
      verifyGetValue(result, check, value, itemPtrs[i]);
    }
  }

  /**
   * Test RFC 6901 JSON Pointer resolver for {@code "a/b": 1} using
   * {@code JsonValue getValue(String)}. Character {@code '/'} is encoded as
   * {@code "~1"} string.
   * 
   * @param result
   *          Tests result record.
   */
  private void testResolvePathWithEncodedSlash(final TestResult result) {
    final JsonStructure value = PointerRFCObject.createRFC6901Object();
    final JsonValue check = Json.createValue(PointerRFCObject.RFC_VAL3);
    verifyGetValue(result, check, value, PointerRFCObject.RFC_PTR3_ENC);
  }

  /**
   * Test RFC 6901 JSON Pointer resolver for {@code "a/b": 1} using
   * {@code JsonValue getValue(String)}. Character {@code '/'} is not encoded as
   * {@code "~1"} string. This results in invalid {@code "/a/b"} path and
   * resolving such path must throw an exception.
   * 
   * @param result
   *          Tests result record.
   */
  private void testResolvePathWithSlash(final TestResult result) {
    final JsonStructure value = PointerRFCObject.createRFC6901Object();
    verifyGetValueFail(result, value, PointerRFCObject.RFC_PTR3);
  }

  /**
   * Test RFC 6901 JSON Pointer resolver for {@code "c%d": 2} using
   * {@code JsonValue getValue(String)}.
   * 
   * @param result
   *          Tests result record.
   */
  private void testResolvePathWithPercent(final TestResult result) {
    final JsonStructure value = PointerRFCObject.createRFC6901Object();
    final JsonValue check = Json.createValue(PointerRFCObject.RFC_VAL4);
    verifyGetValue(result, check, value, PointerRFCObject.RFC_PTR4);
  }

  /**
   * Test RFC 6901 JSON Pointer resolver for {@code "e^f": 3} using
   * {@code JsonValue getValue(String)}.
   * 
   * @param result
   *          Tests result record.
   */
  private void testResolvePathWithCaret(final TestResult result) {
    final JsonStructure value = PointerRFCObject.createRFC6901Object();
    final JsonValue check = Json.createValue(PointerRFCObject.RFC_VAL5);
    verifyGetValue(result, check, value, PointerRFCObject.RFC_PTR5);
  }

  /**
   * Test RFC 6901 JSON Pointer resolver for {@code "g|h": 4} using
   * {@code JsonValue getValue(String)}.
   * 
   * @param result
   *          Tests result record.
   */
  private void testResolvePathWithVerticalBar(final TestResult result) {
    final JsonStructure value = PointerRFCObject.createRFC6901Object();
    final JsonValue check = Json.createValue(PointerRFCObject.RFC_VAL6);
    verifyGetValue(result, check, value, PointerRFCObject.RFC_PTR6);
  }

  /**
   * Test RFC 6901 JSON Pointer resolver for {@code "i\\j": 5} using
   * {@code JsonValue getValue(String)}.
   * 
   * @param result
   *          Tests result record.
   */
  private void testResolvePathWithBackSlash(final TestResult result) {
    final JsonStructure value = PointerRFCObject.createRFC6901Object();
    final JsonValue check = Json.createValue(PointerRFCObject.RFC_VAL7);
    verifyGetValue(result, check, value, PointerRFCObject.RFC_PTR7);
  }

  /**
   * Test RFC 6901 JSON Pointer resolver for {@code "k\"l": 6} using
   * {@code JsonValue getValue(String)}.
   * 
   * @param result
   *          Tests result record.
   */
  private void testResolvePathWithDoubleQuotes(final TestResult result) {
    final JsonStructure value = PointerRFCObject.createRFC6901Object();
    final JsonValue check = Json.createValue(PointerRFCObject.RFC_VAL8);
    verifyGetValue(result, check, value, PointerRFCObject.RFC_PTR8);
  }

  /**
   * Test RFC 6901 JSON Pointer resolver for {@code " ": 7} using
   * {@code JsonValue getValue(String)}.
   * 
   * @param result
   *          Tests result record.
   */
  private void testResolvePathWithSpace(final TestResult result) {
    final JsonStructure value = PointerRFCObject.createRFC6901Object();
    final JsonValue check = Json.createValue(PointerRFCObject.RFC_VAL9);
    verifyGetValue(result, check, value, PointerRFCObject.RFC_PTR9);
  }

  /**
   * Test RFC 6901 JSON Pointer resolver for {@code "m~n": 8} without encoding
   * using {@code JsonValue getValue(String)}. Passing this test is not
   * mandatory.
   * {@see <a href="https://tools.ietf.org/html/rfc6901#section-3">RFC 6901: 3.
   * Syntax</a>} defines JSON pointer grammar as:<br>
   * {@code json-pointer    = *( "/" reference-token )}<br>
   * {@code reference-token = *( unescaped / escaped )}<br>
   * {@code unescaped       = %x00-2E / %x30-7D / %x7F-10FFFF}<br>
   * {@code escaped         = "~" ( "0" / "1" )}<br>
   * Characters {@code '/'} and {@code '~'} are excluded from {@code unescaped}.
   * But having {@code '~'} outside escape sequence may be acceptable.
   * 
   * @param result
   *          Tests result record.
   */
  private void testResolvePathWithTilde(final TestResult result) {
    LOGGER.info(" - resolving of \"" + PointerRFCObject.RFC_PTR10 + "\" pointer (optional)");
    final JsonStructure value = PointerRFCObject.createRFC6901Object();
    final JsonValue check = Json.createValue(PointerRFCObject.RFC_VAL10);
    boolean noError = true;
    try {
      final JsonValue out = value.getValue(PointerRFCObject.RFC_PTR10);
      if (operationFailed(check, out)) {
        noError = false;
        LOGGER.info("    - Pointer \"" + PointerRFCObject.RFC_KEY10
            + "\" did not return expected value");
      }
    } catch (JsonException e) {
      noError = false;
      LOGGER.info("    - Expected exception: " + e.getMessage());
    }
    if (noError) {
      LOGGER.info(
          "    - Pointer resolving accepts '~' outside escape sequence");
    }
  }

  /**
   * Test RFC 6901 JSON Pointer resolver for {@code "m~n": 8} using
   * {@code JsonValue getValue(String)}. Character {@code '~'} is encoded as
   * {@code "~0"} string.
   * 
   * @param result
   *          Tests result record.
   */
  private void testResolvePathWithEncodedTilde(final TestResult result) {
    final JsonStructure value = PointerRFCObject.createRFC6901Object();
    final JsonValue check = Json.createValue(PointerRFCObject.RFC_VAL10);
    verifyGetValue(result, check, value, PointerRFCObject.RFC_KEY10_ENC);
  }

  /**
   * Test RFC 6901 JSON Pointer resolver for {@code "o~1p": 9} using
   * {@code JsonValue getValue(String)}. String {@code "~1"} is encoded as
   * {@code "~01"} String. Proper encoded sequences transformation is described
   * in chapter:
   * {@code "the string '~01' correctly becomes '~1' after transformation"}.
   * 
   * @param result
   *          Tests result record.
   */
  private void testResolvePathWithEncodedTildeOne(final TestResult result) {
    final JsonStructure value = PointerRFCObject.createRFC6901Object();
    final JsonValue check = Json.createValue(PointerRFCObject.RFC_VAL11);
    verifyGetValue(result, check, value, PointerRFCObject.RFC_PTR11_ENC);
  }

  /**
   * Test RFC 6901 JSON Pointer resolver for existing numeric indexes of an
   * array. {@see <a href="https://tools.ietf.org/html/rfc6901#section-4">RFC
   * 6901: 4. Evaluation</a>} chapter:<br>
   * If the currently referenced value is a JSON array, the reference token MUST
   * contain either:
   * <ul>
   * <li>characters comprised of digits (see ABNF below; note that leading zeros
   * are not allowed) that represent an unsigned base-10 integer value, making
   * the new referenced value the array element with the zero-based index
   * identified by the token</li>
   * </ul>
   */
  private void testResolveValidNumericIndexInArray(final TestResult result) {
    LOGGER.info(
        " - getValue(String) resolving of pointer containing existing numeric array index");
    final JsonArray[] arraysIn = new JsonArray[] { SimpleValues.createSimpleStringArray5(),
        SimpleValues.createSimpleIntArray5(), SimpleValues.createSimpleBoolArray5(),
        SimpleValues.createSimpleObjectArray5() };
    final JsonValue[] strings = new JsonValue[] { SimpleValues.toJsonValue(SimpleValues.STR_VALUE_1),
        SimpleValues.toJsonValue(SimpleValues.STR_VALUE_2), SimpleValues.toJsonValue(SimpleValues.STR_VALUE_3),
        SimpleValues.toJsonValue(SimpleValues.STR_VALUE_4), SimpleValues.toJsonValue(SimpleValues.STR_VALUE_5) };
    final JsonValue[] ints = new JsonValue[] { SimpleValues.toJsonValue(SimpleValues.INT_VALUE_1),
        SimpleValues.toJsonValue(SimpleValues.INT_VALUE_2), SimpleValues.toJsonValue(SimpleValues.INT_VALUE_3),
        SimpleValues.toJsonValue(SimpleValues.INT_VALUE_4), SimpleValues.toJsonValue(SimpleValues.INT_VALUE_5) };
    final JsonValue[] bools = new JsonValue[] { SimpleValues.toJsonValue(SimpleValues.BOOL_FALSE),
        SimpleValues.toJsonValue(SimpleValues.BOOL_TRUE), SimpleValues.toJsonValue(SimpleValues.BOOL_TRUE), SimpleValues.toJsonValue(SimpleValues.BOOL_FALSE),
        SimpleValues.toJsonValue(SimpleValues.BOOL_TRUE) };
    final JsonValue[] objs = new JsonValue[] { SimpleValues.OBJ_VALUE_1, SimpleValues.OBJ_VALUE_2,
        SimpleValues.OBJ_VALUE_3, SimpleValues.OBJ_VALUE_4, SimpleValues.OBJ_VALUE_5 };
    final JsonValue[][] checks = new JsonValue[][] { strings, ints, bools,
        objs };
    // Go trough all array types
    for (int i = 0; i < arraysIn.length; i++) {
      // Go trough all valid indexes in arrays
      for (int j = 0; j < 5; j++) {
        final String path = "/" + Integer.toString(j);
        try {
          final JsonValue out = arraysIn[i].getValue(path);
          if (operationFailed(checks[i][j], out)) {
            result.fail("getValue(String)", "Failed for \"" + path + "\" path");
          }
        } catch (JsonException e) {
          result.fail("getValue(String)", "Exception: " + e.getMessage());
        }
      }
    }
  }

  /**
   * Test RFC 6901 JSON Pointer resolver for character {@code '-'} marking the
   * end of an array.
   * {@see <a href="https://tools.ietf.org/html/rfc6901#section-4">RFC 6901: 4.
   * Evaluation</a>} chapter:<br>
   * If the currently referenced value is a JSON array, the reference token MUST
   * contain either:
   * <ul>
   * <li>exactly the single character "-", making the new referenced value the
   * (nonexistent) member after the last array element</li>
   * </ul>
   * Note that the use of the "-" character to index an array will always result
   * in such an error condition because by definition it refers to a nonexistent
   * array element. Thus, applications of JSON Pointer need to specify how that
   * character is to be handled, if it is to be useful.
   */
  private void testResolveMemberAfterLastInArray(final TestResult result) {
    LOGGER.info(" - getValue(String) resolving of array \"/-\" pointer");
    final JsonArray[] arraysIn = new JsonArray[] { SimpleValues.createEmptyArray(),
        SimpleValues.createStringArray(), SimpleValues.createSimpleIntArray5(), SimpleValues.createBoolArray2(),
        SimpleValues.createSimpleObjectArray5() };
    for (int i = 0; i < arraysIn.length; i++) {
      try {
        arraysIn[i].getValue("/-");
        result.fail("getValue(String)", "Call of getValue(String) on \"" + "/-"
            + "\" shall throw JsonException");
      } catch (JsonException e) {
        LOGGER.info("    - Expected exception: " + e.getMessage());
      }
    }
  }

  /**
   * Test RFC 6901 JSON Pointer resolver for invalid index containing non
   * numeric characters on array.
   * {@see <a href="https://tools.ietf.org/html/rfc6901#section-4">RFC 6901: 4.
   * Evaluation</a>} chapter:<br>
   * {@code array-index = %x30 / ( %x31-39 *(%x30-39) )} grammar rule prohibits
   * indexes with anything else than sequence of digits. Index {@code '-'} is
   * being checked in another tests. The only exception is path for whole
   * document ({@code ""}) which must return the whole array.
   */
  private void testResolveNonNumericIndexInArray(final TestResult result) {
    LOGGER.info(
        " - getValue(String) resolving of pointer containing non numeric array index");
    final JsonArray[] arraysIn = new JsonArray[] { SimpleValues.createEmptyArray(),
        SimpleValues.createStringArray(), SimpleValues.createSimpleIntArray5(), SimpleValues.createBoolArray2(),
        SimpleValues.createSimpleObjectArray5() };
    final String[] typeNames = new String[] { "empty", "String", "int",
        "boolean", "JsonObject" };
    final String wholeDocument = "";
    final String[] paths = new String[] { "/", "/1a", "/b4", "/name" };
    // Go trough all array types
    for (int i = 0; i < arraysIn.length; i++) {
      try {
        final JsonValue wholeOut = arraysIn[i].getValue(wholeDocument);
        if (operationFailed(wholeOut, arraysIn[i])) {
          result.fail("getValue(String)", "Failed for \"" + wholeDocument
              + "\" path on " + typeNames[i] + " array");
        }
      } catch (JsonException e) {
        result.fail("getValue(String)", "Failed for \"" + wholeDocument
            + "\" path on " + typeNames[i] + " array: " + e.getMessage());
      }
      for (int j = 0; j < paths.length; j++) {
        try {
          final JsonValue out = arraysIn[i].getValue(paths[j]);
          result.fail("getValue(String)", "Succeeded for \"" + paths[j]
              + "\" path on " + typeNames[i] + " array");
        } catch (JsonException e) {
          // There are too many combinations to log them.
        }
      }
    }
  }

  /**
   * Test helper: Verify {@code JsonValue getValue(String)} for given JSON path.
   * 
   * @param result
   *          Tests result record.
   */
  private void verifyGetValue(final TestResult result, final JsonValue check,
      final JsonStructure value, final String path) {
    LOGGER.info(" - getValue(String) resolving of \"" + path + "\" pointer");
    try {
      final JsonValue out = value.getValue(path);
      if (operationFailed(check, out)) {
        result.fail("getValue(String)", "Failed for \"" + path + "\" path");
      }
    } catch (JsonException e) {
      result.fail("getValue(String)", "Exception: " + e.getMessage());
    }
  }

  /**
   * Test helper: Verify {@code JsonValue getValue(String)} for given JSON path.
   * 
   * @param result
   *          Tests result record.
   */
  private void verifyGetValueFail(final TestResult result,
      final JsonStructure value, final String path) {
    LOGGER.info(
        " - getValue(String) resolving of invalid \"" + path + "\" pointer");
    try {
      value.getValue(path);
      result.fail("getValue(String)", "Call of getValue(String) on \"" + path
          + "\" shall throw JsonException");
    } catch (JsonException e) {
      LOGGER.info("    - Expected exception: " + e.getMessage());
    }
  }

  /**
   * Operation result check.
   * 
   * @param check
   *          Expected modified JSON value.
   * @param out
   *          Operation output.
   * @return Value of {@code true} if operation passed or {@code false}
   *         otherwise.
   */
  protected boolean operationFailed(final JsonValue check,
      final JsonValue out) {
    return out == null || !JsonAssert.assertEquals(check, out);
  }

}