org.klojang.path.PathWalker Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of klojang-invoke Show documentation
Show all versions of klojang-invoke Show documentation
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.
The newest version!
package org.klojang.path;
import org.klojang.check.Check;
import org.klojang.check.Tag;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import static org.klojang.check.CommonChecks.*;
import static org.klojang.check.CommonProperties.length;
/**
* A {@code PathWalker} lets you read and write deeply nested values using {@link Path}
* objects. It can read almost any type of object it encounters as it walks down the path
* towards the last path segment: JavaBeans, maps, collections, arrays and scalar values.
* It can also write to most of them. A {@code PathWalker} can be useful when processing
* large batches of sparsely populated maps or objects and/or it doesn't really matter
* whether a deeply nested value is {@code null} or just not present at all. By default,
* the {@code PathWalker} will return {@code null} in either case (although you can change
* this).
*
* @author Ayco Holleman
*/
@SuppressWarnings({"unchecked"})
public final class PathWalker {
/**
* Returns the value of the specified path within the specified object. This method
* returns {@code null} if the path is invalid or if it is blocked midway by a
* {@code null} value or an out-of-range index.
*
* @param host the object to read the value from
* @param path the path to the value
* @param the type of the value
* @return the value of the specified path within the specified object
*/
public static T read(Object host, String path) {
return new PathWalker(path).read(host);
}
/**
* Returns the value of the specified path within the specified object. This method
* returns {@code null} if the path is invalid or if it is blocked midway by a
* {@code null} value or an out-of-range index.
*
* @param host the object to read the value from
* @param path the path to the value
* @param the type of the value
* @return the value of the specified path within the specified object
*/
public static T read(Object host, Path path) {
return new PathWalker(path).read(host);
}
private static final String PATHS = "paths";
private final Path[] paths;
private final boolean se;
private final KeyDeserializer kd;
/**
* Creates a {@code PathWalker} for the specified paths.
*
* @param paths one or more paths representing possibly deeply-nested properties
*/
public PathWalker(Path... paths) {
Check.that(paths, PATHS).isNot(empty()).is(deepNotNull());
this.paths = Arrays.copyOf(paths, paths.length);
this.se = true;
this.kd = null;
}
/**
* Creates a {@code PathWalker} for the specified paths.
*
* @param paths the paths to walk through the provided host objects
*/
public PathWalker(String... paths) {
Check.that(paths, PATHS).isNot(empty()).is(deepNotNull());
this.paths = Arrays.stream(paths).map(Path::from).toArray(Path[]::new);
this.se = true;
this.kd = null;
}
/**
* Creates a {@code PathWalker} for the specified paths.
*
* @param paths the paths to walk through the provided host objects
*/
public PathWalker(List paths) {
this(paths, true);
}
/**
* Creates a {@code PathWalker} for the specified paths.
*
* @param paths the action to take if a path could not be read or written
* @param suppressExceptions if {@code true}, the {@code read} methods will return
* {@code null} for paths that could not be read. The {@code write} methods will
* quietly return without having written the value. If {@code false}, a
* {@link PathWalkerException} will be thrown detailing the error.
*/
public PathWalker(List paths, boolean suppressExceptions) {
Check.that(paths, PATHS).isNot(empty()).is(deepNotNull());
this.paths = paths.toArray(Path[]::new);
this.se = suppressExceptions;
this.kd = null;
}
/**
* Creates a {@code PathWalker} for the specified paths.
*
* @param paths the paths to walk
* @param suppressExceptions if {@code true}, the {@code read} methods will return
* {@code null} for paths that could not be read. The {@code write} methods will
* quietly return without having written the value. If {@code false}, a
* {@link PathWalkerException} will be thrown detailing the error.
* @param keyDeserializer A function that converts path segments to map keys. You
* need to provide this if the host objects are, or contain, {@link Map} instances
* with non-string keys.
*/
public PathWalker(
List paths,
boolean suppressExceptions,
KeyDeserializer keyDeserializer) {
Check.that(paths, PATHS).isNot(empty()).is(deepNotNull());
Check.notNull(keyDeserializer, "keyDeserializer");
this.paths = paths.toArray(Path[]::new);
this.se = suppressExceptions;
this.kd = keyDeserializer;
}
// For internal use
PathWalker(Path path, boolean suppressExceptions, KeyDeserializer keyDeserializer) {
this.paths = new Path[]{path};
this.se = suppressExceptions;
this.kd = keyDeserializer;
}
/**
* Returns the values of all paths specified through the constructor.
*
* @param host the object to read the values from
* @return the values of all paths specified through the constructor
* @throws PathWalkerException If {@code suppressExceptions} is false and the
* {@code PathWalker} fails to retrieve the values of one or more paths.
*/
public Object[] readValues(Object host) throws PathWalkerException {
ObjectReader reader = new ObjectReader(se, kd);
return Arrays.stream(paths).map(path -> reader.read(host, path, 0)).toArray();
}
/**
* Reads the values of all paths specified through the constructor.
*
* @param host the object to read the path values from
* @param output an array into which to place the values. The length of the output
* array must be equal to, or greater than the number of paths specified through
* the constructor.
* @throws PathWalkerException If {@code suppressExceptions} is false and the
* {@code PathWalker} fails to retrieve the values of one or more paths.
*/
public void readValues(Object host, Object[] output) throws PathWalkerException {
Check.notNull(output, Tag.OUTPUT).has(length(), gte(), paths.length);
ObjectReader reader = new ObjectReader(se, kd);
for (int i = 0; i < paths.length; ++i) {
output[i] = reader.read(host, paths[i], 0);
}
}
/**
* Reads the values of all paths and inserts them into the provided path-to-value map.
*
* @param host the object from which to read the values
* @param output The {@code Map} into which to put the values
* @throws PathWalkerException If {@code suppressExceptions} is false and the
* {@code PathWalker} fails to retrieve the values of one or more paths.
*/
public void readValues(Object host, Map output) throws
PathWalkerException {
Check.notNull(output, Tag.OUTPUT);
ObjectReader reader = new ObjectReader(se, kd);
Arrays.stream(paths).forEach(p -> output.put(p, reader.read(host, p, 0)));
}
/**
* Reads the value of the first path specified through the constructor. Convenient if
* you specified just one path.
*
* @param The type of the value being returned
* @param host the object from which to read the value
* @return the value of the first path specified through the constructor
* @throws PathWalkerException If {@code suppressExceptions} is false and the
* {@code PathWalker} fails to retrieve the value of the first path.
*/
public T read(Object host) {
return (T) new ObjectReader(se, kd).read(host, paths[0], 0);
}
/**
* Sets the values of the paths specified through the constructor. The provided array of
* values must have the same length as the number of paths.
*
* @param host the object to which to write the values
* @param values The values to write
* @return the number of successfully written values
*/
public int writeValues(Object host, Object... values) {
Check.notNull(values, Tag.VALUES).has(length(), eq(), paths.length);
ObjectWriter writer = new ObjectWriter(se, kd);
int x = 0;
for (int i = 0; i < paths.length; ++i) {
if (writer.write(host, paths[i], values[i])) {
++x;
}
}
return x;
}
/**
* Sets the value of the first path specified through the constructor. Convenient if you
* specified just one path.
*
* @param host the object to write the value to
* @param value The value to write
* @return {@code true} if the value was successfully written
*/
public boolean write(Object host, Object value) {
return new ObjectWriter(se, kd).write(host, paths[0], value);
}
}