blob: b674ad94bb865a48b407f5da60e075b289d9343f [file] [log] [blame]
/*
* 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 org.apache.johnzon.core;
import javax.json.Json;
import javax.json.JsonArray;
import javax.json.JsonArrayBuilder;
import javax.json.JsonException;
import javax.json.JsonObject;
import javax.json.JsonObjectBuilder;
import javax.json.JsonPointer;
import javax.json.JsonStructure;
import javax.json.JsonValue;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class JsonPointerImpl implements JsonPointer {
private final String jsonPointer;
private final List<String> referenceTokens = new ArrayList<>();
private final String lastReferenceToken;
/**
* Constructs and initializes a JsonPointer.
*
* @param jsonPointer the JSON Pointer string
* @throws NullPointerException if {@code jsonPointer} is {@code null}
* @throws JsonException if {@code jsonPointer} is not a valid JSON Pointer
*/
public JsonPointerImpl(String jsonPointer) {
if (jsonPointer == null) {
throw new NullPointerException("jsonPointer must not be null");
}
if (!jsonPointer.equals("") && !jsonPointer.startsWith("/")) {
throw new JsonException("A non-empty JsonPointer string must begin with a '/'");
}
this.jsonPointer = jsonPointer;
String[] encodedReferenceTokens = jsonPointer.split("/", -1);
for (String encodedReferenceToken : encodedReferenceTokens) {
referenceTokens.add(JsonPointerUtil.decode(encodedReferenceToken));
}
lastReferenceToken = referenceTokens.get(referenceTokens.size() - 1);
}
/**
* Compares this {@code JsonPointer} with another object.
*
* @param obj the object to compare this {@code JsonPointer} against
* @return true if the given object is a {@code JsonPointer} with the same
* reference tokens as this one, false otherwise.
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
JsonPointerImpl that = (JsonPointerImpl) obj;
return jsonPointer.equals(that.jsonPointer);
}
/**
* Returns the hash code value for this {@code JsonPointer} object.
* The hash code of this object is defined by the hash codes of it's reference tokens.
*
* @return the hash code value for this {@code JsonPointer} object
*/
@Override
public int hashCode() {
return jsonPointer.hashCode();
}
/**
* Returns the value at the referenced location in the specified {@code target}
*
* @param target the target referenced by this {@code JsonPointer}
* @return the referenced value in the target.
* @throws NullPointerException if {@code target} is null
* @throws JsonException if the referenced value does not exist
*/
public JsonValue getValue(JsonStructure target) {
if (target == null) {
throw new NullPointerException("target must not be null");
}
if (isEmptyJsonPointer()) {
return target;
}
JsonValue jsonValue = target;
for (int i = 1; i < referenceTokens.size(); i++) {
jsonValue = getValue(jsonValue, referenceTokens.get(i), i, referenceTokens.size() - 1);
}
return jsonValue;
}
@Override
public boolean containsValue(JsonStructure target) {
try {
getValue(target);
return true;
} catch (JsonException je) {
return false;
}
}
/**
* Adds or replaces a value at the referenced location in the specified
* {@code target} with the specified {@code value}.
* <ol>
* <li>If the reference is the target (empty JSON Pointer string),
* the specified {@code value}, which must be the same type as
* specified {@code target}, is returned.</li>
* <li>If the reference is an array element, the specified {@code value} is inserted
* into the array, at the referenced index. The value currently at that location, and
* any subsequent values, are shifted to the right (adds one to the indices).
* Index starts with 0. If the reference is specified with a "-", or if the
* index is equal to the size of the array, the value is appended to the array.</li>
* <li>If the reference is a name/value pair of a {@code JsonObject}, and the
* referenced value exists, the value is replaced by the specified {@code value}.
* If the value does not exist, a new name/value pair is added to the object.</li>
* </ol>
*
* @param target the target referenced by this {@code JsonPointer}
* @param value the value to be added
* @return the transformed {@code target} after the value is added.
* @throws NullPointerException if {@code target} is {@code null}
* @throws JsonException if the reference is an array element and
* the index is out of range ({@code index < 0 || index > array size}),
* or if the pointer contains references to non-existing objects or arrays.
*/
public JsonStructure add(JsonStructure target, JsonValue value) {
validateAdd(target);
if (isEmptyJsonPointer()) {
if (value.getClass() != target.getClass()) {
throw new JsonException("The value must have the same type as the target");
}
return (JsonStructure) value;
}
return addInternal(target, value);
}
/**
* Adds or replaces a value at the referenced location in the specified
* {@code target} with the specified {@code value}.
*
* @param target the target referenced by this {@code JsonPointer}
* @param value the value to be added
* @return the transformed {@code target} after the value is added.
* @throws NullPointerException if {@code target} is {@code null}
* @throws JsonException if the reference is an array element and
* the index is out of range ({@code index < 0 || index > array size}),
* or if the pointer contains references to non-existing objects or arrays.
* @see #add(JsonStructure, JsonValue)
*/
public JsonObject add(JsonObject target, JsonValue value) {
validateAdd(target);
return addInternal(target, value);
}
/**
* Adds or replaces a value at the referenced location in the specified
* {@code target} with the specified {@code value}.
*
* @param target the target referenced by this {@code JsonPointer}
* @param value the value to be added
* @return the transformed {@code target} after the value is added.
* @throws NullPointerException if {@code target} is {@code null}
* @throws JsonException if the reference is an array element and
* the index is out of range ({@code index < 0 || index > array size}),
* or if the pointer contains references to non-existing objects or arrays.
* @see #add(JsonStructure, JsonValue)
*/
public JsonArray add(JsonArray target, JsonValue value) {
validateAdd(target);
return addInternal(target, value);
}
/**
* Replaces the value at the referenced location in the specified
* {@code target} with the specified {@code value}.
*
* @param target the target referenced by this {@code JsonPointer}
* @param value the value to be stored at the referenced location
* @return the transformed {@code target} after the value is replaced.
* @throws NullPointerException if {@code target} is {@code null}
* @throws JsonException if the referenced value does not exist,
* or if the reference is the target.
*/
public JsonStructure replace(JsonStructure target, JsonValue value) {
if (target instanceof JsonObject) {
return replace((JsonObject) target, value);
} else {
return replace((JsonArray) target, value);
}
}
/**
* Replaces the value at the referenced location in the specified
*
* @param target the target referenced by this {@code JsonPointer}
* @param value the value to be stored at the referenced location
* @return the transformed {@code target} after the value is replaced.
* @throws NullPointerException if {@code target} is {@code null}
* @throws JsonException if the referenced value does not exist,
* or if the reference is the target.
* @see #replace(JsonStructure, JsonValue)
*/
public JsonObject replace(JsonObject target, JsonValue value) {
return add(remove(target), value);
}
/**
* Replaces the value at the referenced location in the specified
*
* @param target the target referenced by this {@code JsonPointer}
* @param value the value to be stored at the referenced location
* @return the transformed {@code target} after the value is replaced.
* @throws NullPointerException if {@code target} is {@code null}
* @throws JsonException if the referenced value does not exist,
* or if the reference is the target.
* @see #replace(JsonStructure, JsonValue)
*/
public JsonArray replace(JsonArray target, JsonValue value) {
return add(remove(target), value);
}
/**
* Removes the value at the reference location in the specified {@code target}
*
* @param target the target referenced by this {@code JsonPointer}
* @return the transformed {@code target} after the value is removed.
* @throws NullPointerException if {@code target} is {@code null}
* @throws JsonException if the referenced value does not exist,
* or if the reference is the target.
*/
public JsonStructure remove(JsonStructure target) {
if (target instanceof JsonObject) {
return remove((JsonObject) target);
} else {
return remove((JsonArray) target);
}
}
/**
* Removes the value at the reference location in the specified {@code target}
*
* @param target the target referenced by this {@code JsonPointer}
* @return the transformed {@code target} after the value is removed.
* @throws NullPointerException if {@code target} is {@code null}
* @throws JsonException if the referenced value does not exist,
* or if the reference is the target.
* @see #remove(JsonStructure)
*/
public JsonObject remove(JsonObject target) {
validateRemove(target);
return (JsonObject) remove(target, 1, referenceTokens.size() - 1);
}
/**
* Removes the value at the reference location in the specified {@code target}
*
* @param target the target referenced by this {@code JsonPointer}
* @return the transformed {@code target} after the value is removed.
* @throws NullPointerException if {@code target} is {@code null}
* @throws JsonException if the referenced value does not exist,
* or if the reference is the target.
* @see #remove(JsonStructure)
*/
public JsonArray remove(JsonArray target) {
validateRemove(target);
return (JsonArray) remove(target, 1, referenceTokens.size() - 1);
}
String getJsonPointer() {
return jsonPointer;
}
private void validateAdd(JsonValue target) {
validateJsonPointer(target, referenceTokens.size() - 1);
}
private void validateRemove(JsonValue target) {
validateJsonPointer(target, referenceTokens.size());
if (isEmptyJsonPointer()) {
throw new JsonException("The reference must not be the target");
}
}
private boolean isEmptyJsonPointer() {
return jsonPointer.equals("");
}
private JsonValue getValue(JsonValue jsonValue, String referenceToken, int currentPosition, int referencePosition) {
if (jsonValue instanceof JsonObject) {
JsonObject jsonObject = (JsonObject) jsonValue;
jsonValue = jsonObject.get(referenceToken);
if (jsonValue != null) {
return jsonValue;
}
throw new JsonException("'" + jsonObject + "' contains no value for name '" + referenceToken + "'");
} else if (jsonValue instanceof JsonArray) {
validateArrayIndex(referenceToken);
try {
JsonArray jsonArray = (JsonArray) jsonValue;
int arrayIndex = Integer.parseInt(referenceToken);
validateArraySize(jsonArray, arrayIndex, jsonArray.size());
return jsonArray.get(arrayIndex);
} catch (NumberFormatException e) {
throw new JsonException("'" + referenceToken + "' is no valid array index", e);
}
} else {
if (currentPosition != referencePosition) {
return jsonValue;
}
throw new JsonException("'" + jsonValue + "' contains no element for '" + referenceToken + "'");
}
}
private <T extends JsonStructure> T addInternal(T jsonValue, JsonValue newValue) {
List<String> currentPath = new ArrayList<>();
currentPath.add("");
return (T) addInternal(jsonValue, newValue, currentPath);
}
private JsonValue addInternal(JsonValue jsonValue, JsonValue newValue, List<String> currentPath) {
if (jsonValue instanceof JsonObject) {
JsonObject jsonObject = (JsonObject) jsonValue;
JsonObjectBuilder objectBuilder = Json.createObjectBuilder();
if (jsonObject.isEmpty()) {
objectBuilder.add(lastReferenceToken, newValue);
} else {
for (Map.Entry<String, JsonValue> entry : jsonObject.entrySet()) {
currentPath.add(entry.getKey());
objectBuilder.add(entry.getKey(), addInternal(entry.getValue(), newValue, currentPath));
currentPath.remove(entry.getKey());
if (currentPath.size() == referenceTokens.size() - 1 &&
currentPath.get(currentPath.size() - 1).equals(referenceTokens.get(referenceTokens.size() - 2))) {
objectBuilder.add(lastReferenceToken, newValue);
}
}
}
return objectBuilder.build();
} else if (jsonValue instanceof JsonArray) {
JsonArray jsonArray = (JsonArray) jsonValue;
JsonArrayBuilder arrayBuilder = Json.createArrayBuilder();
int arrayIndex = -1;
if (currentPath.size() == referenceTokens.size() - 1 &&
currentPath.get(currentPath.size() - 1).equals(referenceTokens.get(referenceTokens.size() - 2))) {
arrayIndex = getArrayIndex(lastReferenceToken, jsonArray, true);
}
int jsonArraySize = jsonArray.size();
for (int i = 0; i <= jsonArraySize; i++) {
if (i == arrayIndex) {
arrayBuilder.add(newValue);
}
if (i == jsonArraySize) {
break;
}
String path = String.valueOf(i);
currentPath.add(path);
arrayBuilder.add(addInternal(jsonArray.get(i), newValue, currentPath));
currentPath.remove(path);
}
return arrayBuilder.build();
}
return jsonValue;
}
private JsonValue remove(JsonValue jsonValue, int currentPosition, int referencePosition) {
if (jsonValue instanceof JsonObject) {
JsonObject jsonObject = (JsonObject) jsonValue;
JsonObjectBuilder objectBuilder = Json.createObjectBuilder();
for (Map.Entry<String, JsonValue> entry : jsonObject.entrySet()) {
if (currentPosition == referencePosition
&& lastReferenceToken.equals(entry.getKey())) {
continue;
}
objectBuilder.add(entry.getKey(), remove(entry.getValue(), currentPosition + 1, referencePosition));
}
return objectBuilder.build();
} else if (jsonValue instanceof JsonArray) {
JsonArray jsonArray = (JsonArray) jsonValue;
JsonArrayBuilder arrayBuilder = Json.createArrayBuilder();
int arrayIndex = -1;
if (currentPosition == referencePosition) {
arrayIndex = getArrayIndex(lastReferenceToken, jsonArray, false);
}
int jsonArraySize = jsonArray.size();
for (int i = 0; i < jsonArraySize; i++) {
if (i == arrayIndex) {
continue;
}
arrayBuilder.add(remove(jsonArray.get(i), currentPosition + 1, referencePosition));
}
return arrayBuilder.build();
}
return jsonValue;
}
private int getArrayIndex(String referenceToken, JsonArray jsonArray, boolean addOperation) {
if (addOperation && referenceToken.equals("-")) {
return jsonArray.size();
}
validateArrayIndex(referenceToken);
try {
int arrayIndex = Integer.parseInt(referenceToken);
int arraySize = addOperation ? jsonArray.size() + 1 : jsonArray.size();
validateArraySize(jsonArray, arrayIndex, arraySize);
return arrayIndex;
} catch (NumberFormatException e) {
throw new JsonException("'" + referenceToken + "' is no valid array index", e);
}
}
private void validateJsonPointer(JsonValue target, int size) throws NullPointerException, JsonException {
if (target == null) {
throw new NullPointerException("target must not be null");
}
JsonValue jsonValue = target;
for (int i = 1; i < size; i++) {
jsonValue = getValue(jsonValue, referenceTokens.get(i), i, referenceTokens.size() - 1);
}
}
private void validateArrayIndex(String referenceToken) throws JsonException {
if (referenceToken.startsWith("+") || referenceToken.startsWith("-")) {
throw new JsonException("An array index must not start with '" + referenceToken.charAt(0) + "'");
}
if (referenceToken.startsWith("0") && referenceToken.length() > 1) {
throw new JsonException("An array index must not start with a leading '0'");
}
}
private void validateArraySize(JsonArray jsonArray, int arrayIndex, int arraySize) throws JsonException {
if (arrayIndex >= arraySize) {
throw new JsonException("'" + jsonArray + "' contains no element for index " + arrayIndex);
}
}
}