summaryrefslogtreecommitdiffstats
path: root/vespajlib
diff options
context:
space:
mode:
authorJon Marius Venstad <venstad@gmail.com>2022-03-30 13:16:29 +0200
committerJon Marius Venstad <venstad@gmail.com>2022-03-30 13:16:29 +0200
commit2bc3dac20f6ec32f319bc850b6c1a8f28ecdfeed (patch)
tree8028a04fce5d9d6534bee9e72d68b0210bceb4f1 /vespajlib
parent9ab03c6161d2dd4469cfa5bdf3f81e3ca4dd71c5 (diff)
draft
Diffstat (limited to 'vespajlib')
-rw-r--r--vespajlib/src/main/java/ai/vespa/validation/Hostname.java31
-rw-r--r--vespajlib/src/main/java/ai/vespa/validation/Name.java33
-rw-r--r--vespajlib/src/main/java/ai/vespa/validation/StringWrapper.java45
-rw-r--r--vespajlib/src/main/java/ai/vespa/validation/Validation.java88
-rw-r--r--vespajlib/src/test/java/ai/vespa/validation/ValidationTest.java74
5 files changed, 271 insertions, 0 deletions
diff --git a/vespajlib/src/main/java/ai/vespa/validation/Hostname.java b/vespajlib/src/main/java/ai/vespa/validation/Hostname.java
new file mode 100644
index 00000000000..87ef837492c
--- /dev/null
+++ b/vespajlib/src/main/java/ai/vespa/validation/Hostname.java
@@ -0,0 +1,31 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package ai.vespa.validation;
+
+import java.util.regex.Pattern;
+
+import static ai.vespa.validation.Validation.requireMatch;
+import static ai.vespa.validation.Validation.validate;
+
+/**
+ * A valid hostname.
+ *
+ * @author jonmv
+ */
+public class Hostname extends StringWrapper<Hostname> {
+
+ public static final Pattern hostnameLabel = Pattern.compile("([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]{0,61}[A-Za-z0-9])");
+ public static final Pattern hostnamePattern = Pattern.compile("(?=.{1,255})(" + hostnameLabel + "\\.)*" + hostnameLabel);
+
+ private Hostname(String value) {
+ super(value);
+ }
+
+ public static Hostname of(String hostname) {
+ return new Hostname(validate(hostname, "hostname", requireMatch(hostnamePattern)));
+ }
+
+ public static String requireLabel(String label) {
+ return validate(label, "hostname label", requireMatch(hostnameLabel));
+ }
+
+}
diff --git a/vespajlib/src/main/java/ai/vespa/validation/Name.java b/vespajlib/src/main/java/ai/vespa/validation/Name.java
new file mode 100644
index 00000000000..1869ba734b3
--- /dev/null
+++ b/vespajlib/src/main/java/ai/vespa/validation/Name.java
@@ -0,0 +1,33 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package ai.vespa.validation;
+
+import java.util.regex.Pattern;
+
+import static ai.vespa.validation.Validation.requireMatch;
+import static ai.vespa.validation.Validation.validate;
+
+/**
+ * A name is a non-null, non-blank {@link String} which starts with a letter, and has up to
+ * 254 more characters which may be letters, numbers, dashes or underscores.
+ *
+ * Prefer domain-specific wrappers over this class, but prefer this over raw strings when possible. gT
+ *
+ * @author jonmv
+ */
+public class Name extends StringWrapper<Name> {
+
+ public static final Pattern namePattern = Pattern.compile("[A-Za-z][A-Za-z0-9_-]{0,254}");
+
+ private Name(String value) {
+ super(value);
+ }
+
+ public static Name of(String value) {
+ return of(value, "name");
+ };
+
+ public static Name of(String value, String description) {
+ return new Name(validate(value, description, requireMatch(namePattern)));
+ };
+
+}
diff --git a/vespajlib/src/main/java/ai/vespa/validation/StringWrapper.java b/vespajlib/src/main/java/ai/vespa/validation/StringWrapper.java
new file mode 100644
index 00000000000..258be824ef5
--- /dev/null
+++ b/vespajlib/src/main/java/ai/vespa/validation/StringWrapper.java
@@ -0,0 +1,45 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package ai.vespa.validation;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * Abstract wrapper for glorified strings, to ease adding new such wrappers.
+ *
+ * @param <T> child type
+ *
+ * @author jonmv
+ */
+public abstract class StringWrapper<T extends StringWrapper<T>> implements Comparable<T> {
+
+ private final String value;
+
+ protected StringWrapper(String value) {
+ this.value = requireNonNull(value);
+ }
+
+ public final String value() { return value; }
+
+ @Override
+ public int compareTo(T other) {
+ return value().compareTo(other.value());
+ }
+
+ @Override
+ public final boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ return value.equals(((StringWrapper<?>) o).value);
+ }
+
+ @Override
+ public final int hashCode() {
+ return value.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return value;
+ }
+
+}
diff --git a/vespajlib/src/main/java/ai/vespa/validation/Validation.java b/vespajlib/src/main/java/ai/vespa/validation/Validation.java
new file mode 100644
index 00000000000..f02096c478b
--- /dev/null
+++ b/vespajlib/src/main/java/ai/vespa/validation/Validation.java
@@ -0,0 +1,88 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package ai.vespa.validation;
+
+import com.yahoo.yolean.Exceptions;
+
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.regex.Pattern;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * Every {@link String} is a security risk!
+ * This class has utility methods for validating strings, which are often user input.
+ *
+ * @author jonmv
+ */
+public class Validation {
+
+ private Validation() { }
+
+ /** Parses then given string, and then validates that each of the requirements are true for the parsed value. */
+ @SafeVarargs
+ @SuppressWarnings("varargs")
+ public static <T> T validate(String value, Function<String, T> parser, String description, Predicate<? super T>... requirements) {
+ T parsed;
+ try {
+ parsed = parser.apply(requireNonNull(value, description + " cannot be null"));
+ }
+ catch (RuntimeException e) {
+ throw new IllegalArgumentException("failed parsing " + description +
+ " '" + value + "': " + Exceptions.toMessageString(e));
+ }
+ return validate(parsed, description, requirements);
+ }
+
+ /** Validates that each of the requirements are true for the given argument. */
+ @SafeVarargs
+ @SuppressWarnings("varargs")
+ public static <T> T validate(T value, String description, Predicate<? super T>... requirements) {
+ for (Predicate<? super T> requirement : requirements)
+ if ( ! requirement.test(value))
+ throw new IllegalArgumentException(description + " " + requirement + ", but got: '" + value + "'");
+
+ return value;
+ }
+
+ /** Requires arguments to match the given pattern. */
+ public static Predicate<String> requireMatch(Pattern pattern) {
+ return require(s -> pattern.matcher(s).matches(), "must match '" + pattern + "'");
+ }
+
+ /** Requires arguments to be non-blank. */
+ public static Predicate<String> requireNonBlank() {
+ return require(s -> ! s.isBlank(), "cannot be blank");
+ }
+
+ /** Requires arguments to be at least the lower bound. */
+ public static <T extends Comparable<? super T>> Predicate<T> requireAtLeast(T lower) {
+ requireNonNull(lower, "lower bound cannot be null");
+ return require(c -> lower.compareTo(c) <= 0, "must be at least '" + lower + "'");
+ }
+
+ /** Requires arguments to be at most the upper bound. */
+ public static <T extends Comparable<? super T>> Predicate<T> requireAtMost(T upper) {
+ requireNonNull(upper, "upper bound cannot be null");
+ return require(c -> upper.compareTo(c) >= 0, "must be at most '" + upper + "'");
+ }
+
+ /** Requires arguments to be at least the lower bound, and at most the upper bound. */
+ public static <T extends Comparable<? super T>> Predicate<T> requireInRange(T lower, T upper) {
+ requireNonNull(lower, "lower bound cannot be null");
+ requireNonNull(upper, "upper bound cannot be null");
+ if (lower.compareTo(upper) > 0) throw new IllegalArgumentException("lower bound cannot be greater than upper bound, " +
+ "but got '" + lower + "' > '" + upper + "'");
+ return require(c -> lower.compareTo(c) <= 0 && upper.compareTo(c) >= 0,
+ "must be at least '" + lower + "' and at most '" + upper + "'");
+ }
+
+ /** Wraps a predicate with a message describing it. */
+ public static <T> Predicate<T> require(Predicate<T> predicate, String message) {
+ return new Predicate<T>() {
+ @Override public boolean test(T t) { return predicate.test(t); }
+ @Override public String toString() { return message; }
+ };
+ }
+
+} \ No newline at end of file
diff --git a/vespajlib/src/test/java/ai/vespa/validation/ValidationTest.java b/vespajlib/src/test/java/ai/vespa/validation/ValidationTest.java
new file mode 100644
index 00000000000..c3b0730a5ee
--- /dev/null
+++ b/vespajlib/src/test/java/ai/vespa/validation/ValidationTest.java
@@ -0,0 +1,74 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package ai.vespa.validation;
+
+import com.google.common.base.Preconditions;
+import org.junit.jupiter.api.Test;
+
+import static ai.vespa.validation.Validation.require;
+import static ai.vespa.validation.Validation.requireAtLeast;
+import static ai.vespa.validation.Validation.requireAtMost;
+import static ai.vespa.validation.Validation.requireInRange;
+import static ai.vespa.validation.Validation.requireNonBlank;
+import static ai.vespa.validation.Validation.validate;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * @author jonmv
+ */
+class ValidationTest {
+
+ @Test
+ void testNumberComparisons() {
+ assertEquals(3.14, validate("3.14", Double::parseDouble, "pi",
+ requireInRange(3.14, 3.14),
+ requireInRange(0.0, Double.POSITIVE_INFINITY),
+ requireInRange(Double.NEGATIVE_INFINITY, 3.14),
+ requireAtLeast(3.14),
+ requireAtMost(3.14)));
+
+ assertEquals("lower bound cannot be greater than upper bound, but got '1.0' > '0.1'",
+ assertThrows(IllegalArgumentException.class,
+ () -> validate(3.14, "pi", requireInRange(1.0, 0.1)))
+ .getMessage());
+
+ assertEquals("pi must be at least '0.0' and at most '0.0', but got: '3.14'",
+ assertThrows(IllegalArgumentException.class,
+ () -> validate(3.14, "pi", requireInRange(0.0, 0.0)))
+ .getMessage());
+
+ assertEquals("pi must be at least '4.0' and at most '4.0', but got: '3.14'",
+ assertThrows(IllegalArgumentException.class,
+ () -> validate(3.14, "pi", requireInRange(4.0, 4.0)))
+ .getMessage());
+
+ assertEquals("pi must be at least '4.0', but got: '3.14'",
+ assertThrows(IllegalArgumentException.class,
+ () -> validate(3.14, "pi", requireAtLeast(4.0)))
+ .getMessage());
+
+ assertEquals("pi must be at most '3.0', but got: '3.14'",
+ assertThrows(IllegalArgumentException.class,
+ () -> validate(3.14, "pi", requireAtMost(3.0)))
+ .getMessage());
+ }
+
+ @Test
+ void testStringComparisons() {
+ assertEquals("hei", validate("hei", "word",
+ requireNonBlank(),
+ require(__ -> true, "nothing"),
+ requireInRange("hai", "hoi")));
+
+ assertEquals("word cannot be blank, but got: ''",
+ assertThrows(IllegalArgumentException.class,
+ () -> validate("", "word", requireNonBlank()))
+ .getMessage());
+
+ assertEquals("lower bound cannot be greater than upper bound, but got 'hoi' > 'hai'",
+ assertThrows(IllegalArgumentException.class,
+ () -> validate("hei", "word", requireInRange("hoi", "hai")))
+ .getMessage());
+ }
+
+}