All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.fge.jsonschema.ref.JsonPointer Maven / Gradle / Ivy

There is a newer version: 2.2.6
Show newest version
/*
 * Copyright (c) 2013, Francis Galiegue 
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the Lesser GNU General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * Lesser GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see .
 */

package com.github.fge.jsonschema.ref;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.MissingNode;
import com.github.fge.jsonschema.exceptions.JsonReferenceException;
import com.github.fge.jsonschema.report.ProcessingMessage;
import com.google.common.base.CharMatcher;
import com.google.common.collect.BiMap;
import com.google.common.collect.ImmutableBiMap;
import com.google.common.collect.ImmutableList;

import java.util.Collections;
import java.util.List;

import static com.github.fge.jsonschema.messages.JsonRefMessages.*;

/**
 * Implementation of IETF JSON Pointer draft, version 9
 *
 * 

* JSON Pointer is an IETF draft defining a way to address paths within JSON * values (including non container values).

* *

An individual entry of a JSON Pointer is called a reference token. For * JSON Objects, a reference token is a member name. For arrays, it is an index. * Indices start at 0. Note that array indices written with a leading 0 are * considered to be failing (ie, {@code 0} is OK but {@code 00} is not).

* *

The general syntax is {@code /reference/tokens/here}. A JSON Pointer * may be empty, in which case this refers to the JSON value itself.

* *

The difficulty solved by JSON Pointer is that any JSON String is valid as * an object member name. These are all valid object member names, and all of * them can be addressed by using JSON Pointer:

* *
    *
  • {@code ""} -- the empty string;
  • *
  • {@code "/"};
  • *
  • {@code "0"};
  • *
  • {@code "-1"};
  • *
  • {@code "."}, {@code ".."}, {@code "../.."}.
  • *
* *

The latter example is the reason why a JSON Pointer is always * absolute.

* *

All instances of this class are immutable (and therefore thread safe).

*/ public final class JsonPointer extends JsonFragment { /** * The empty pointer * *

This is what will be returned by {@link * JsonFragment#fromFragment(String)} if the submitted fragment is empty. *

*/ private static final JsonPointer EMPTY = new JsonPointer("", ImmutableList.of()); /** * Reference token separator */ private static final CharMatcher SLASH = CharMatcher.is('/'); /** * Escape character in a "cooked" element */ private static final CharMatcher ESCAPE_CHAR = CharMatcher.is('~'); /** * "0": for array index reference token needs */ private static final CharMatcher ZERO = CharMatcher.is('0'); /** * Replacement map for getting a raw reference token out of a cooked one * *

{@code ~0} becomes {@code ~} and {@code ~1} becomes {@code /}.

* *

This is a {@link BiMap} so that it can also be used in the reverse * situation.

* * @see #refTokenEncode(String) * @see #refTokenDecode(String) */ private static final BiMap ESCAPE_REPLACEMENT_MAP = new ImmutableBiMap.Builder() .put('0', '~') .put('1', '/') .build(); /** * Character matcher for an escaped reference token * *

This is built from {@link #ESCAPE_REPLACEMENT_MAP}'s keys.

* * @see #refTokenDecode(String) */ private static final CharMatcher ESCAPED; /** * Character matcher for a raw reference token * *

This is built from {@link #ESCAPE_REPLACEMENT_MAP}'s values.

* * @see #refTokenEncode(String) */ private static final CharMatcher SPECIAL; static { CharMatcher escaped = CharMatcher.NONE, special = CharMatcher.NONE; for (final Character c1: ESCAPE_REPLACEMENT_MAP.keySet()) escaped = escaped.or(CharMatcher.is(c1)); for (final Character c2: ESCAPE_REPLACEMENT_MAP.values()) special = special.or(CharMatcher.is(c2)); ESCAPED = escaped.precomputed(); SPECIAL = special.precomputed(); } /** * The list of individual reference tokens, in order. */ private final List refTokens; /** * Return an empty pointer * * @return a statically created empty JSON Pointer */ public static JsonPointer empty() { return EMPTY; } /** * Constructor * * @param input The input string, guaranteed not to be JSON encoded * @throws JsonReferenceException illegal JSON Pointer */ public JsonPointer(final String input) throws JsonReferenceException { super(input); final ImmutableList.Builder builder = ImmutableList.builder(); decode(input, builder); refTokens = builder.build(); } /** * Private constructor for building a pointer with all pointer elements * (reference tokens, full string representation) already computed * * @param fullPointer the pointer as a string * @param refTokens the reference tokens */ private JsonPointer(final String fullPointer, final List refTokens) { super(fullPointer); this.refTokens = refTokens; } /** * Static private constructor to build a pointer out of a list of reference * tokens * * @param refTokens the list of reference tokens * @return a newly constructed JSON Pointer */ private static JsonPointer fromElements(final List refTokens) { if (refTokens.isEmpty()) return empty(); final StringBuilder sb = new StringBuilder(); for (final String raw: refTokens) sb.append('/').append(refTokenEncode(raw)); return new JsonPointer(sb.toString(), refTokens); } /** * Append a pointer to the current pointer * * @param other the other pointer * @return a new instance with the pointer appended */ public JsonPointer append(final JsonPointer other) { final List newElements = ImmutableList.builder() .addAll(refTokens).addAll(other.refTokens).build(); if (newElements.isEmpty()) return empty(); final String newPath = asString + other.asString; return new JsonPointer(newPath, newElements); } /** * Append a reference token as a string to this pointer. * * @param element the element to append * @return a new instance with the element appended */ public JsonPointer append(final String element) { final List newElements = ImmutableList.builder() .addAll(refTokens).add(element).build(); return new JsonPointer(asString + '/' + refTokenEncode(element), newElements); } /** * Append an array index to this pointer. * *

Note that the index validity is NOT checked for (ie, you can append * {@code -1} if you want to -- don't do that)

* * @param index the index to add * @return a new instance with the index appended */ public JsonPointer append(final int index) { return append(Integer.toString(index)); } /** * Apply the pointer to a JSON value and return the result * *

If the pointer fails to look up a value, a {@link MissingNode} is * returned.

* * @param node the node to apply the pointer to * @return the resulting node */ @Override public JsonNode resolve(final JsonNode node) { JsonNode ret = node; for (final String pathElement : refTokens) { if (!ret.isContainerNode()) return MissingNode.getInstance(); ret = ret.isObject() ? ret.path(pathElement) : ret.path(arrayIndexFor(pathElement)); if (ret.isMissingNode()) break; } return ret; } @Override public boolean isEmpty() { return asString.isEmpty(); } @Override public boolean isPointer() { return true; } /** * Return this pointer as a series of JSON Pointers starting from the * beginning * * @return an unmodifiable list of JSON Pointers */ public List asElements() { final ImmutableList.Builder builder = ImmutableList.builder(); for (final String raw: refTokens) builder.add(fromElements(ImmutableList.of(raw))); return builder.build(); } /** * Return true if this JSON Pointer is "parent" of another one * *

That is, its number of reference tokens is less than, or equal to, * the other pointer's, and its first reference tokens are the same.

* *

This means that this will also return true if the pointers are equal. *

* * @param other the other pointer * @return true if this pointer is the parent of the other */ public boolean isParentOf(final JsonPointer other) { return Collections.indexOfSubList(other.refTokens, refTokens) == 0; } /** * Relativize a pointer to the current pointer * *

If {@link #isParentOf(JsonPointer)} returns false, this will return * the other pointer.

* *

Otherwise, it will return a pointer containing all reference tokens * following this pointer's reference tokens. For instance, relativizing * {@code /a/b} against {@code /a/b/c} gives {@code /c}.

* *

If the pointers are the same, it will return an empty pointer.

* * @param other the pointer to relativize this pointer to * @return a relativized pointer */ public JsonPointer relativize(final JsonPointer other) { if (!isParentOf(other)) return other; final List list = other.refTokens.subList(refTokens.size(), other.refTokens.size()); return fromElements(list); } /** * Initialize the object * *

We read the string sequentially, a slash, then a reference token, * then a slash, etc. Bail out if the string is malformed.

* * @param input Input string, guaranteed not to be JSON/URI encoded * @param builder the list builder * @throws JsonReferenceException the input is not a valid JSON Pointer */ private static void decode(final String input, final ImmutableList.Builder builder) throws JsonReferenceException { String cooked, raw; String victim = input; while (!victim.isEmpty()) { /* * Skip the / */ if (!victim.startsWith("/")) { final ProcessingMessage message = new ProcessingMessage() .message("illegal JSON Pointer") .put("reason", "reference token not preceeded by '/'"); throw new JsonReferenceException(message); } victim = victim.substring(1); /* * Grab the "cooked" reference token */ cooked = getNextRefToken(victim); victim = victim.substring(cooked.length()); /* * Decode it, push it in the refTokens list */ raw = refTokenDecode(cooked); builder.add(raw); } } /** * Grab a (cooked) reference token from an input string * *

This method is only called from {@link #decode(String, * ImmutableList.Builder)}, after a delimiter ({@code /}) has been swallowed * up. The input string is therefore guaranteed to start with a reference * token, which may be empty.

* * @param input the input string * @return the cooked reference token * @throws JsonReferenceException the string is malformed */ private static String getNextRefToken(final String input) throws JsonReferenceException { final StringBuilder sb = new StringBuilder(); final char[] array = input.toCharArray(); /* * If we encounter a /, this is the end of the current token. * * If we encounter a ~, ensure that what follows is either 0 or 1. * * If we encounter any other character, append it. */ boolean inEscape = false; for (final char c: array) { if (inEscape) { if (!ESCAPED.matches(c)) { final ProcessingMessage message = new ProcessingMessage().message(ILLEGAL_POINTER) .put("reason", ILLEGAL_ESCAPE) .put("allowed", ESCAPE_REPLACEMENT_MAP.keySet()) .put("found", Character.valueOf(c)); throw new JsonReferenceException(message); } sb.append(c); inEscape = false; continue; } if (SLASH.matches(c)) break; if (ESCAPE_CHAR.matches(c)) inEscape = true; sb.append(c); } if (inEscape) { final ProcessingMessage message = new ProcessingMessage().message(ILLEGAL_POINTER) .put("reason", EMPTY_ESCAPE); throw new JsonReferenceException(message); } return sb.toString(); } /** * Turn a cooked reference token into a raw reference token * *

This means we replace all occurrences of {@code ~0} with {@code ~}, * and all occurrences of {@code ~1} with {@code /}.

* *

It is called from {@link #decode}, in order to push a reference token * into {@link #refTokens}.

* * @param cooked the cooked token * @return the raw token */ private static String refTokenDecode(final String cooked) { final StringBuilder sb = new StringBuilder(cooked.length()); /* * Replace all occurrences of "~0" with "~", and all occurrences of "~1" * with "/". * * The input is guaranteed to be well formed. */ final char[] array = cooked.toCharArray(); boolean inEscape = false; for (final char c: array) { if (ESCAPE_CHAR.matches(c)) { inEscape = true; continue; } if (inEscape) { sb.append(ESCAPE_REPLACEMENT_MAP.get(c)); inEscape = false; } else sb.append(c); } return sb.toString(); } /** * Make a cooked reference token out of a raw reference token * * @param raw the raw token * @return the cooked token */ private static String refTokenEncode(final String raw) { final StringBuilder sb = new StringBuilder(raw.length()); /* * Replace all occurrences of "~" with "~0" and all occurrences of "/" * with "~1". */ final char[] array = raw.toCharArray(); for (final char c: array) if (SPECIAL.matches(c)) sb.append('~').append(ESCAPE_REPLACEMENT_MAP.inverse().get(c)); else sb.append(c); return sb.toString(); } /** * Return an array index corresponding to the given reference token * *

If no array index can be found, -1 is returned. As the result is used * with {@link JsonNode#path(int)}, we are guaranteed correct results, since * this will return a {@link MissingNode} in this case.

* * @param pathElement the path element as a string * @return the index, or -1 if the index is invalid */ private static int arrayIndexFor(final String pathElement) { /* * Empty? No dice. */ if (pathElement.isEmpty()) return -1; /* * Leading zeroes are not allowed in number-only refTokens for arrays. * But then, 0 followed by anything else than a number is invalid as * well. So, if the string starts with '0', return 0 if the token length * is 1 or -1 otherwise. */ if (ZERO.matches(pathElement.charAt(0))) return pathElement.length() == 1 ? 0 : -1; /* * Otherwise, parse as an int. If we can't, -1. */ try { return Integer.parseInt(pathElement); } catch (NumberFormatException ignored) { return -1; } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy