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

org.klojang.path.Path Maven / Gradle / Ivy

Go to download

Klojang Invoke is a Java module focused on path-based object access and dynamic invocation. Its central classes are the Path class and the PathWalker class. The Path class captures a path through an object graph. For example "employee.address.city". The PathWalker class lets you read from and write to a wide variety of types using Path objects.

There is a newer version: 21.1.0
Show newest version
package org.klojang.path;

import org.klojang.check.Check;
import org.klojang.check.Tag;
import org.klojang.check.aux.Emptyable;
import org.klojang.util.ArrayMethods;
import org.klojang.util.ObjectMethods;
import org.klojang.util.StringMethods;

import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.stream.Stream;

import static java.lang.System.arraycopy;
import static java.util.Arrays.copyOfRange;
import static org.klojang.check.CommonChecks.indexOf;
import static org.klojang.check.CommonChecks.lt;
import static org.klojang.check.CommonExceptions.INDEX;
import static org.klojang.util.ArrayMethods.EMPTY_STRING_ARRAY;
import static org.klojang.util.ArrayMethods.implode;

/**
 * Specifies a path to a value within an object. For example:
 * {@code employee.address.city}. A path string consists of path segments separated by the
 * dot character ('.'). Array indices are specified as separate path segments. For
 * example: {@code employees.3.address.city} — the city component of the address of
 * the fourth employee in a list or array of {@code Employee} instances. Non-numeric
 * segments can be either bean properties or map keys. Therefore the {@code Path} class
 * does not impose any constraints on what constitutes a valid path segment. A map key,
 * after all, can be anything — including {@code null} and the empty string. Of
 * course, if the path segment represents a JavaBean property, it must be a valid Java
 * identifier.
 *
 * 

Escaping

*

These are the escaping rules when specifying path strings: *

    *
  • If a map key happens to contain the segment separator ('.'), it must be * escaped using the circumflex character ('^'). So key {@code "my.awkward.map.key"} * should be escaped to {@code "my^.awkward^.map^.key"}. *
  • For a map key with value {@code null}, use this escape sequence: {@code "^0"}. So * {@code "lookups.^0"} represents the {@code null} key in the {@code lookups} map. *
  • If a segment needs to represent a map key whose value is the empty string, * simply make it a zero-length segment: {@code "lookups..name"}. This implies that a * path string that ends with a dot in fact ends with an empty (zero-length) segment. *
  • The escape character ('^') itself may be escaped. Thus, key * {@code "super^awkward"} can be represented either as {@code "super^awkward"} or as * {@code "super^^awkward"}. If the escape character is not followed by a dot or * another escape character, it is just that character. You must escape the * escape character, however, if the entire path segment happens to be the * escape sequence for {@code null} ({@code "^0"}). Thus, in the odd case you have a * key with value {@code "^0"}, escape it to {@code "^^0"}. *
* *

You can let the {@link #escape(String) escape} method do the escaping for you. Do * not escape path segments when passing them individually (as a {@code String} array) to * the constructor. Only escape them when passing a complete path string. * * @author Ayco Holleman */ public final class Path implements Comparable, Iterable, Emptyable { private static final Path EMPTY_PATH = new Path(); // segment separator private static final char SEP = '.'; // escape character private static final char ESC = '^'; // escape sequence to use for null keys private static final String NULL_SEGMENT = "^0"; /** * Returns a new {@code Path} instance for the specified path string. * * @param path the path string from which to create a {@code Path} * @return a new {@code Path} instance for the specified path string */ public static Path from(String path) { Check.notNull(path, Tag.PATH); if (path.isEmpty()) { return EMPTY_PATH; } return new Path(path); } /** * Returns a {@code Path} consisting of the specified segments. Do not escape the * segments. The array may contain {@code null} values as well as empty strings. * * @param segments the path segments * @return a {@code Path} consisting of the specified segments */ public static Path from(String[] segments) { Check.notNull(segments); return segments.length == 0 ? EMPTY_PATH : new Path(segments); } /** * Returns an empty {@code Path} instance, consisting of zero path segments. * * @return an empty {@code Path} instance, consisting of zero path segments */ public static Path empty() { return EMPTY_PATH; } /** * Returns a {@code Path} consisting of a single segment. Do not escape the * segment. * * @param segment the one and only segment of the {@code Path} * @return a {@code Path} consisting of a single segment */ public static Path of(String segment) { return new Path(new String[]{segment}); } /** * Returns a {@code Path} consisting of the specified segments. Do not escape the * segments. * * @param segment0 the 1st segment * @param segment1 the 2nd segment * @return a {@code Path} consisting of the specified segments */ public static Path of(String segment0, String segment1) { return new Path(new String[]{segment0, segment1}); } /** * Returns a {@code Path} consisting of the specified segments. Do not escape the * segments. * * @param segment0 the 1st segment * @param segment1 the 2nd segment * @param segment2 the 3rd segment * @return a {@code Path} consisting of the specified segments */ public static Path of(String segment0, String segment1, String segment2) { return new Path(new String[]{segment0, segment1, segment2}); } /** * Returns a {@code Path} consisting of the specified segments. Do not escape the * segments. * * @param segment0 the 1st segment * @param segment1 the 2nd segment * @param segment2 the 3rd segment * @param segment3 the 4th segment * @return a {@code Path} consisting of the specified segments */ public static Path of( String segment0, String segment1, String segment2, String segment3) { return new Path(new String[]{segment0, segment1, segment2, segment3}); } /** * Returns a {@code Path} consisting of the specified segments. Do not escape the * segments. * * @param segment0 the 1st segment * @param segment1 the 2nd segment * @param segment2 the 3rd segment * @param segment3 the 4th segment * @param segment4 the 5th segment * @return a {@code Path} consisting of the specified segments */ public static Path of( String segment0, String segment1, String segment2, String segment3, String segment4) { return new Path(new String[]{segment0, segment1, segment2, segment3, segment4}); } /** * Returns a {@code Path} consisting of the specified segments. Do not escape the * segments. * * @param segments the path segments * @return a {@code Path} consisting of the specified segments */ public static Path of(String... segments) { return from(segments); } /** * Returns a copy of the specified path. * * @param other the {@code Path} to copy. * @return a copy of the specified path */ public static Path copyOf(Path other) { return other == EMPTY_PATH ? other : Check.notNull(other).ok(Path::new); } /** * Escapes the specified path segment. Do not escape path segments when passing them * individually to one of the static factory methods. Only use this method to assemble * complete path strings from individual path segments. Generally you don't need this * method when specifying path strings, unless one or more path segments contain a dot * ('.') or the escape character ('^') itself. The argument may be {@code null}, in * which case the escape sequence for {@code null} ({@code "^0"}) is returned. * * @param segment the path segment to escape * @return the escaped version of the segment */ public static String escape(String segment) { if (segment == null) { return NULL_SEGMENT; } else if (segment.equals(NULL_SEGMENT)) { return ESC + NULL_SEGMENT; } int x = segment.indexOf(SEP); if (x == -1) { return segment; } StringBuilder sb = new StringBuilder(segment.length() + 3) .append(segment.substring(0, x)) .append(ESC) .append(SEP); for (int i = x + 1; i < segment.length(); ++i) { char c = segment.charAt(i); switch (c) { case SEP -> sb.append(ESC).append(SEP); default -> sb.append(c); } } return sb.toString(); } private final String[] elems; private String str; // Caches toString() private int hash; // Caches hashCode() // Reserved for EMPTY_PATH private Path() { elems = EMPTY_STRING_ARRAY; } private Path(String path) { elems = parse(str = path); } private Path(String[] segments) { elems = new String[segments.length]; arraycopy(segments, 0, elems, 0, segments.length); } private Path(Path other) { // Since we are immutable we can happily share state this.elems = other.elems; this.str = other.str; this.hash = other.hash; } /** * Returns the path segment at the specified index. Specify a negative index to retrieve * a segment relative to end of the {@code Path} (-1 would return the last path * segment). * * @param index the array index of the path segment * @return the path segment at the specified index. */ public String segment(int index) { if (index < 0) { return Check.that(elems.length + index) .is(indexOf(), elems) .mapToObj(x -> elems[x]); } return Check.that(index).is(indexOf(), elems).mapToObj(x -> elems[x]); } /** * Returns a new {@code Path} starting with the segment at the specified array index. * Specify a negative index to count back from the last segment of the {@code Path} (-1 * returns the last path segment). * * @param offset the index of the first segment of the new {@code Path} * @return a new {@code Path} starting with the segment at the specified array index */ public Path subPath(int offset) { int from = offset < 0 ? elems.length + offset : offset; Check.that(from).is(lt(), elems.length); return new Path(copyOfRange(elems, from, elems.length)); } /** * Returns a new {@code Path} consisting of {@code length} segments starting with * segment {@code offset}. The {@code offset} argument may be negative to specify a * segment relative to the end of the {@code Path}. Thus, -1 specifies the last segment * of the {@code Path}. * * @param offset the index of the first segment to extract * @param length the number of segments to extract * @return a new {@code Path} consisting of {@code len} segments starting with segment * {@code from}. */ public Path subPath(int offset, int length) { if (offset < 0) { offset = elems.length + offset; } Check.offsetLength(elems.length, offset, length); return new Path(copyOfRange(elems, offset, offset + length)); } /** * Returns a {@code Path} with all segments of this {@code Path} except the first * segment. If the path is empty, this method returns {@code null}. If it consists of a * single segment, this method returns {@link #EMPTY_PATH}. * * @return a {@code Path} with all segments of this {@code Path} except the first * segment */ public Path shift() { return switch (elems.length) { case 0 -> null; case 1 -> EMPTY_PATH; default -> new Path(copyOfRange(elems, 1, elems.length)); }; } /** * Returns a {@code Path} with all segments of this {@code Path} except the last * segment. If the path is empty, this method returns {@code null}. If it consists of a * single segment, this method returns {@link #EMPTY_PATH}. * * @return the parent of this {@code Path} */ public Path parent() { return switch (elems.length) { case 0 -> null; case 1 -> EMPTY_PATH; default -> new Path(copyOfRange(elems, 0, elems.length - 1)); }; } /** * Returns a new {@code Path} containing only the segments of this {@code Path} that are * not array indices. * * @return a new {@code Path} without any array indices */ public Path getCanonicalPath() { String[] canonical = stream() .filter(s -> !s.chars().allMatch(Character::isDigit) || new BigInteger(s).intValueExact() < 0) .toArray(String[]::new); return canonical.length == 0 ? EMPTY_PATH : new Path(canonical); } /** * Returns a new {@code Path} representing the concatenation of this {@code Path} and * the specified {@code Path}. * * @param path the path to append to this {@code Path} * @return a new {@code Path} representing the concatenation of this {@code Path} and * the specified {@code Path} */ public Path append(String path) { Check.notNull(path); return append(new Path(parse(path))); } /** * Returns a new {@code Path} consisting of the segments of this {@code Path} plus the * segments of the specified {@code Path}. * * @param other the {@code Path} to append to this {@code Path}. * @return a new {@code Path} consisting of the segments of this {@code Path} plus the * segments of the specified {@code Path} */ public Path append(Path other) { Check.notNull(other); return new Path(ArrayMethods.concat(elems, other.elems)); } /** * Returns a new {@code Path} with the path segment at the specified array index set to * the new value. * * @param index the array index of the segment to replace * @param newValue the new segment * @return a new {@code Path} with the path segment at the specified array index set to * the new value */ public Path replace(int index, String newValue) { Check.on(INDEX, index, Tag.INDEX).is(indexOf(), elems); String[] copy = Arrays.copyOf(elems, elems.length); copy[index] = newValue; return new Path(copy); } /** * Returns a {@code Path} in which the order of the segments is reversed. * * @return a {@code Path} in which the order of the segments is reversed */ public Path reverse() { String[] elems; if ((elems = this.elems).length > 1) { String[] segments = new String[elems.length]; int x = elems.length; for (int i = 0; i < elems.length; ++i) { segments[i] = elems[--x]; } return new Path(segments); } return this; } /** * Returns an {@code Iterator} over the path segments. * * @return an {@code Iterator} over the path segments */ @Override public Iterator iterator() { return new Iterator<>() { private int i; public boolean hasNext() { return i < elems.length; } public String next() { if (i < elems.length) { return elems[i++]; } throw new IndexOutOfBoundsException(i); } }; } /** * Returns a {@code Stream} of path segments. * * @return a {@code Stream} of path segments */ public Stream stream() { return Arrays.stream(elems); } /** * Returns the number of segments in this {@code Path}. * * @return the number of segments in this {@code Path} */ public int size() { return elems.length; } /** * Returns {@code true} if this is an empty {@code Path}, consisting of zero segments. * * @return {@code true} if this is an empty {@code Path}, consisting of zero segments */ @Override public boolean isEmpty() { return elems.length == 0; } /** * Returns {@code true} if this is a non-empty {@code Path}, consisting only of * non-null, non-empty of path segments. * * @return {@code true} if this is a non-empty {@code Path}, consisting only of * non-null, non-empty of path segments */ @Override public boolean isDeepNotEmpty() { return ObjectMethods.isDeepNotEmpty(elems); } @Override public boolean equals(Object obj) { return this == obj || (obj instanceof Path p && Arrays.equals(elems, p.elems)); } @Override public int hashCode() { if (hash == 0) { hash = Arrays.deepHashCode(elems); } return hash; } @Override public int compareTo(Path other) { Check.notNull(other); return Arrays.compare(elems, other.elems); } /** * Returns this {@code Path} as a string, properly escaped. * * @return this {@code Path} as a string, properly escaped */ @Override public String toString() { if (str == null) { str = implode(elems, Path::escape, ".", 0, elems.length); } return str; } private static String[] parse(String path) { ArrayList elems = new ArrayList<>(); StringBuilder sb = new StringBuilder(); int len = path.length(); for (int i = 0; i < len; i++) { switch (path.charAt(i)) { case SEP -> { elems.add(sb.toString()); sb.setLength(0); } case ESC -> { if (i < len - 1) { char c = path.charAt(i + 1); if (c == SEP || c == ESC) { sb.append(c); ++i; } else if (c == '0' && sb.length() == 0 && (i == len - 2 || path.charAt(i + 2) == SEP)) { elems.add(null); sb.setLength(0); i += 2; } else { sb.append(ESC); } } else { sb.append(ESC); } } default -> sb.append(path.charAt(i)); } } if (sb.length() > 0) { elems.add(sb.toString()); } else if (path.charAt(len - 1) == SEP) { elems.add(StringMethods.EMPTY_STRING); } return elems.toArray(String[]::new); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy