/*
* 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 org.apache.accumulo.core.Constants;
import org.apache.accumulo.core.util.Pair;
import org.apache.commons.lang.math.IntRange;
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'.\n"
+ "Unless 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'"),
@SuppressWarnings("unchecked")
PORT("port", or(new Bounds(1024, 65535), in(true, "0"), new PortRange("\\d{4,5}-\\d{4,5}")),
"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. The "
+ "following environment variables will be substituted: " + Constants.PATH_PROPERTY_ENV_VARS),
ABSOLUTEPATH("absolute path", x -> x == null || x.trim().isEmpty() || new Path(x.trim()).isAbsolute(),
"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(true, null, "none", "log", "flush", "sync"), "One of 'none', 'log', 'flush' or 'sync'."),
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;
// made this transient because findbugs was complaining
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) {
Preconditions.checkState(predicate != null, "Predicate was null, maybe this enum was serialized????");
return predicate.test(value);
}
@SuppressWarnings("unchecked")
private static Predicate<String> or(final Predicate<String>... others) {
return (x) -> Arrays.stream(others).anyMatch(y -> y.test(x));
}
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.length() > 0 && 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;
}
if (number > upperBound || (!upperInclusive && number == upperBound)) {
return false;
}
return true;
}
}
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 {
private static final IntRange VALID_RANGE = new IntRange(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 Pair<Integer,Integer> 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.containsInteger(low) || !VALID_RANGE.containsInteger(high) || !(low <= high)) {
throw new IllegalArgumentException("Invalid port range specified, only 1024 to 65535 supported.");
}
return new Pair<>(low, high);
}
throw new IllegalArgumentException("Invalid port range specification, must use M-N notation.");
}
}
}