blob: 6b4de25a1c273934f6bb7faf61d3ab42f5e8042d [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.accumulo.core.conf;
import static java.util.Objects.requireNonNull;
import java.util.Arrays;
import java.util.Objects;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import org.apache.commons.lang3.Range;
import org.apache.hadoop.fs.Path;
import com.google.common.base.Preconditions;
/**
* Types of {@link Property} values. Each type has a short name, a description, and a regex which
* valid values match. All of these fields are optional.
*/
public enum PropertyType {
PREFIX(null, x -> false, null),
TIMEDURATION("duration", boundedUnits(0, Long.MAX_VALUE, true, "", "ms", "s", "m", "h", "d"),
"A non-negative integer optionally followed by a unit of time (whitespace"
+ " disallowed), as in 30s.\n"
+ "If no unit of time is specified, seconds are assumed. Valid units"
+ " are 'ms', 's', 'm', 'h' for milliseconds, seconds," + " minutes, and" + " hours.\n"
+ "Examples of valid durations are '600', '30s', '45m', '30000ms'," + " '3d', and '1h'.\n"
+ "Examples of invalid durations are '1w', '1h30m', '1s 200ms', 'ms', '',"
+ " and 'a'.\nUnless otherwise stated, the max value for the duration"
+ " represented in milliseconds is " + Long.MAX_VALUE),
BYTES("bytes", boundedUnits(0, Long.MAX_VALUE, false, "", "B", "K", "M", "G"),
"A positive integer optionally followed by a unit of memory (whitespace disallowed).\n"
+ "If no unit is specified, bytes are assumed. Valid units are 'B',"
+ " 'K', 'M' or 'G' for bytes, kilobytes, megabytes, gigabytes.\n"
+ "Examples of valid memories are '1024', '20B', '100K', '1500M', '2G', '20%'.\n"
+ "Examples of invalid memories are '1M500K', '1M 2K', '1MB', '1.5G',"
+ " '1,024K', '', and 'a'.\n"
+ "Unless otherwise stated, the max value for the memory represented in bytes is "
+ Long.MAX_VALUE),
MEMORY("memory", boundedUnits(0, Long.MAX_VALUE, false, "", "B", "K", "M", "G", "%"),
"A positive integer optionally followed by a unit of memory or a"
+ " percentage (whitespace disallowed).\n"
+ "If a percentage is specified, memory will be a percentage of the"
+ " max memory allocated to a Java process (set by the JVM option -Xmx).\n"
+ "If no unit is specified, bytes are assumed. Valid units are 'B',"
+ " 'K', 'M', 'G', '%' for bytes, kilobytes, megabytes, gigabytes, and" + " percentage.\n"
+ "Examples of valid memories are '1024', '20B', '100K', '1500M', '2G', '20%'.\n"
+ "Examples of invalid memories are '1M500K', '1M 2K', '1MB', '1.5G',"
+ " '1,024K', '', and 'a'.\n"
+ "Unless otherwise stated, the max value for the memory represented in bytes is "
+ Long.MAX_VALUE),
HOSTLIST("host list",
new Matches(
"[\\w-]+(?:\\.[\\w-]+)*(?:\\:\\d{1,5})?(?:,[\\w-]+(?:\\.[\\w-]+)*(?:\\:\\d{1,5})?)*"),
"A comma-separated list of hostnames or ip addresses, with optional port numbers.\n"
+ "Examples of valid host lists are"
+ " 'localhost:2000,www.example.com,10.10.1.1:500' and 'localhost'.\n"
+ "Examples of invalid host lists are '', ':1000', and 'localhost:80000'"),
PORT("port",
x -> Stream.of(new Bounds(1024, 65535), in(true, "0"), new PortRange("\\d{4,5}-\\d{4,5}"))
.anyMatch(y -> y.test(x)),
"An positive integer in the range 1024-65535 (not already in use or"
+ " specified elsewhere in the configuration),\n"
+ "zero to indicate any open ephemeral port, or a range of positive"
+ " integers specified as M-N"),
COUNT("count", new Bounds(0, Integer.MAX_VALUE),
"A non-negative integer in the range of 0-" + Integer.MAX_VALUE),
FRACTION("fraction/percentage", new FractionPredicate(),
"A floating point number that represents either a fraction or, if"
+ " suffixed with the '%' character, a percentage.\n"
+ "Examples of valid fractions/percentages are '10', '1000%', '0.05',"
+ " '5%', '0.2%', '0.0005'.\n"
+ "Examples of invalid fractions/percentages are '', '10 percent'," + " 'Hulk Hogan'"),
PATH("path", x -> true,
"A string that represents a filesystem path, which can be either relative"
+ " or absolute to some directory. The filesystem depends on the property. "
+ "Substitutions of the ACCUMULO_HOME environment variable can be done in the system "
+ "config file using '${env:ACCUMULO_HOME}' or similar."),
// VFS_CLASSLOADER_CACHE_DIR's default value is a special case, for documentation purposes
@SuppressWarnings("removal")
ABSOLUTEPATH("absolute path",
x -> x == null || x.trim().isEmpty() || new Path(x.trim()).isAbsolute()
|| x.equals(Property.VFS_CLASSLOADER_CACHE_DIR.getDefaultValue()),
"An absolute filesystem path. The filesystem depends on the property."
+ " This is the same as path, but enforces that its root is explicitly" + " specified."),
CLASSNAME("java class", new Matches("[\\w$.]*"),
"A fully qualified java class name representing a class on the classpath.\n"
+ "An example is 'java.lang.String', rather than 'String'"),
CLASSNAMELIST("java class list", new Matches("[\\w$.,]*"),
"A list of fully qualified java class names representing classes on the classpath.\n"
+ "An example is 'java.lang.String', rather than 'String'"),
DURABILITY("durability", in(false, null, "default", "none", "log", "flush", "sync"),
"One of 'none', 'log', 'flush' or 'sync'."),
GC_POST_ACTION("gc_post_action", in(true, null, "none", "flush", "compact"),
"One of 'none', 'flush', or 'compact'."),
STRING("string", x -> true,
"An arbitrary string of characters whose format is unspecified and"
+ " interpreted based on the context of the property to which it applies."),
BOOLEAN("boolean", in(false, null, "true", "false"),
"Has a value of either 'true' or 'false' (case-insensitive)"),
URI("uri", x -> true, "A valid URI");
private String shortname, format;
// Field is transient because enums are Serializable, but Predicates aren't necessarily,
// and our lambdas certainly aren't; This shouldn't matter because enum serialization doesn't
// store fields, so this is a false positive in our spotbugs version
// see https://github.com/spotbugs/spotbugs/issues/740
private transient Predicate<String> predicate;
private PropertyType(String shortname, Predicate<String> predicate, String formatDescription) {
this.shortname = shortname;
this.predicate = Objects.requireNonNull(predicate);
this.format = formatDescription;
}
@Override
public String toString() {
return shortname;
}
/**
* Gets the description of this type.
*
* @return description
*/
String getFormatDescription() {
return format;
}
/**
* Checks if the given value is valid for this type.
*
* @return true if value is valid or null, or if this type has no regex
*/
public boolean isValidFormat(String value) {
// this can't happen because enum fields aren't serialized, so it doesn't matter if the
// predicate was transient or not, but it's probably not hurting anything to check and provide
// the helpful error message for troubleshooting, just in case
Preconditions.checkState(predicate != null,
"Predicate was null, maybe this enum was serialized????");
return predicate.test(value);
}
private static Predicate<String> in(final boolean caseSensitive, final String... allowedSet) {
if (caseSensitive) {
return x -> Arrays.stream(allowedSet)
.anyMatch(y -> (x == null && y == null) || (x != null && x.equals(y)));
} else {
Function<String,String> toLower = x -> x == null ? null : x.toLowerCase();
return x -> Arrays.stream(allowedSet).map(toLower)
.anyMatch(y -> (x == null && y == null) || (x != null && toLower.apply(x).equals(y)));
}
}
private static Predicate<String> boundedUnits(final long lowerBound, final long upperBound,
final boolean caseSensitive, final String... suffixes) {
Predicate<String> suffixCheck = new HasSuffix(caseSensitive, suffixes);
return x -> x == null
|| (suffixCheck.test(x) && new Bounds(lowerBound, upperBound).test(stripUnits.apply(x)));
}
private static final Pattern SUFFIX_REGEX = Pattern.compile("[^\\d]*$");
private static final Function<String,String> stripUnits =
x -> x == null ? null : SUFFIX_REGEX.matcher(x.trim()).replaceAll("");
private static class HasSuffix implements Predicate<String> {
private final Predicate<String> p;
public HasSuffix(final boolean caseSensitive, final String... suffixes) {
p = in(caseSensitive, suffixes);
}
@Override
public boolean test(final String input) {
requireNonNull(input);
Matcher m = SUFFIX_REGEX.matcher(input);
if (m.find()) {
if (m.groupCount() != 0) {
throw new AssertionError(m.groupCount());
}
return p.test(m.group());
} else {
return true;
}
}
}
private static class FractionPredicate implements Predicate<String> {
@Override
public boolean test(final String input) {
if (input == null) {
return true;
}
try {
double d;
if (!input.isEmpty() && input.charAt(input.length() - 1) == '%') {
d = Double.parseDouble(input.substring(0, input.length() - 1));
} else {
d = Double.parseDouble(input);
}
return d >= 0;
} catch (NumberFormatException e) {
return false;
}
}
}
private static class Bounds implements Predicate<String> {
private final long lowerBound, upperBound;
private final boolean lowerInclusive, upperInclusive;
public Bounds(final long lowerBound, final long upperBound) {
this(lowerBound, true, upperBound, true);
}
public Bounds(final long lowerBound, final boolean lowerInclusive, final long upperBound,
final boolean upperInclusive) {
this.lowerBound = lowerBound;
this.lowerInclusive = lowerInclusive;
this.upperBound = upperBound;
this.upperInclusive = upperInclusive;
}
@Override
public boolean test(final String input) {
if (input == null) {
return true;
}
long number;
try {
number = Long.parseLong(input);
} catch (NumberFormatException e) {
return false;
}
if (number < lowerBound || (!lowerInclusive && number == lowerBound)) {
return false;
}
return number <= upperBound && (upperInclusive || number != upperBound);
}
}
private static class Matches implements Predicate<String> {
protected final Pattern pattern;
public Matches(final String pattern) {
this(pattern, Pattern.DOTALL);
}
public Matches(final String pattern, int flags) {
this(Pattern.compile(requireNonNull(pattern), flags));
}
public Matches(final Pattern pattern) {
requireNonNull(pattern);
this.pattern = pattern;
}
@Override
public boolean test(final String input) {
// TODO when the input is null, it just means that the property wasn't set
// we can add checks for not null for required properties with
// Predicates.and(Predicates.notNull(), ...),
// or we can stop assuming that null is always okay for a Matches predicate, and do that
// explicitly with Predicates.or(Predicates.isNull(), ...)
return input == null || pattern.matcher(input).matches();
}
}
public static class PortRange extends Matches {
public static final Range<Integer> VALID_RANGE = Range.between(1024, 65535);
public PortRange(final String pattern) {
super(pattern);
}
@Override
public boolean test(final String input) {
if (super.test(input)) {
try {
PortRange.parse(input);
return true;
} catch (IllegalArgumentException e) {
return false;
}
} else {
return false;
}
}
public static IntStream parse(String portRange) {
int idx = portRange.indexOf('-');
if (idx != -1) {
int low = Integer.parseInt(portRange.substring(0, idx));
int high = Integer.parseInt(portRange.substring(idx + 1));
if (!VALID_RANGE.contains(low) || !VALID_RANGE.contains(high) || low > high) {
throw new IllegalArgumentException(
"Invalid port range specified, only 1024 to 65535 supported.");
}
return IntStream.rangeClosed(low, high);
}
throw new IllegalArgumentException(
"Invalid port range specification, must use M-N notation.");
}
}
}