net.sourceforge.pmd.properties.PropertyBuilder Maven / Gradle / Ivy
Show all versions of pmd-core Show documentation
/**
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
*/
package net.sourceforge.pmd.properties;
import static java.util.Collections.emptyList;
import static net.sourceforge.pmd.util.CollectionUtil.listOf;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.regex.Pattern;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import net.sourceforge.pmd.properties.internal.PropertyParsingUtil;
import net.sourceforge.pmd.properties.internal.PropertyTypeId;
import net.sourceforge.pmd.util.AssertionUtil;
import net.sourceforge.pmd.util.CollectionUtil;
import net.sourceforge.pmd.util.IteratorUtil;
// @formatter:off
/**
* Base class for generic property builders.
* Property builders are obtained from the {@link PropertyFactory},
* and are used to build {@link PropertyDescriptor}s.
*
* All properties must specify the following attributes to build
* properly:
*
* - A name: filled-in when obtaining the builder
*
- A description: see {@link #desc(String)}
*
- A default value: see {@link #defaultValue(Object)}
*
*
* The {@link PropertyDescriptor} may be built after those required steps by
* calling {@link #build()}.
*
*
A property builder may throw {@link IllegalArgumentException} at any
* stage during the build process to indicate invalid input. It usually tries
* to do so as early as possible, rather than waiting for the call to {@link #build()}.
*
* @param Concrete type of this builder instance
* @param Type of values the property handles
*
* @author Clément Fournier
* @since 6.10.0
*/
// @formatter:on
public abstract class PropertyBuilder, T> {
private static final Pattern NAME_PATTERN = Pattern.compile("[a-zA-Z][\\w-]*");
private final String name;
private String description;
private T defaultValue;
/**
* Non-null if declared in XML.
*/
protected @Nullable PropertyTypeId typeId;
protected boolean isXPathAvailable = false;
PropertyBuilder(String name) {
if (StringUtils.isBlank(name)) {
throw new IllegalArgumentException("Name must be provided");
} else if (!NAME_PATTERN.matcher(name).matches()) {
throw new IllegalArgumentException("Invalid name '" + name + "'");
}
this.name = name;
}
String getDescription() {
if (!isDescriptionSet()) {
throw new IllegalArgumentException("Description must be provided");
}
return description;
}
boolean isDescriptionSet() {
return StringUtils.isNotBlank(description);
}
/** Returns the value, asserting it has been set. */
T getDefaultValue() {
if (!isDefaultValueSet()) {
throw new IllegalArgumentException("A default value must be provided");
}
return defaultValue;
}
boolean isDefaultValueSet() {
return defaultValue != null;
}
/**
* Specify the description of the property. This is used for documentation.
* Please describe precisely how the property may change the behaviour of the
* rule. Providing complete information should be preferred over being concise.
*
* Calling this method is required for {@link #build()} to succeed.
*
* @param desc The description
*
* @return The same builder
*
* @throws IllegalArgumentException If the description is null or whitespace
*/
@SuppressWarnings("unchecked")
public B desc(String desc) {
if (StringUtils.isBlank(desc)) {
throw new IllegalArgumentException("Description must be provided");
}
this.description = desc;
return (B) this;
}
@SuppressWarnings("unchecked")
B typeId(PropertyTypeId typeId) {
this.typeId = typeId;
return (B) this;
}
/**
* If true, the property will be made available to XPath queries as
* an XPath variable. The default is false (except for properties
* of XPath rules that were defined in XML).
*
* @param b Whether to enable or not
*
* @return This builder
*/
public B availableInXPath(boolean b) {
this.isXPathAvailable = b;
return (B) this;
}
/**
* Add a constraint on the values that this property may take.
* The validity of values will be checked when parsing the XML,
* and invalid values will be reported. A rule will never be run
* if some of its properties violate some constraints.
*
*
Constraints should be independent from each other, and should
* perform no side effects. PMD doesn't specify how many times a
* constraint predicate will be executed, or in what order.
*
* @param constraint The constraint
*
* @return The same builder
*
* @see NumericConstraints
*/
@SuppressWarnings("unchecked")
public abstract B require(PropertyConstraint super T> constraint);
/**
* Specify a default value. Some subclasses provide convenient
* related methods, see e.g. {@link GenericCollectionPropertyBuilder#defaultValues(Object, Object[])}.
* Using the null value is prohibited.
*
*
Calling this method is required for {@link #build()} to succeed.
*
* @param val Default value
*
* @return The same builder
*
* @throws IllegalArgumentException If the argument is null
*/
@SuppressWarnings("unchecked")
public B defaultValue(@NonNull T val) {
//noinspection ConstantConditions
if (val == null) {
throw new IllegalArgumentException("Property values may not be null.");
}
this.defaultValue = val;
return (B) this;
}
/**
* Builds the descriptor and returns it.
*
* @return The built descriptor
*
* @throws IllegalArgumentException if the description or default value were not provided
* @throws IllegalArgumentException if the default value does not satisfy the given constraints
*/
public abstract PropertyDescriptor build();
/**
* Returns the name of the property to be built.
*/
public String getName() {
return name;
}
// Technically this may very well be merged into PropertyBuilder
// We'd have all properties (even collection properties) enjoy a ValueParser,
// which means they could be parsed in a tag (collections would use delimiters) if they opt in.
// The delimiters wouldn't be configurable (we'd take our current defaults). If they could ambiguous
// then the syntax should be the only one available.
// This would allow specifying eg lists of numbers as 1,2,3 , for which the syntax would look clumsy
abstract static class BaseSinglePropertyBuilder, T> extends PropertyBuilder {
private PropertySerializer parser;
// Class is not final but a package-private constructor restricts inheritance
BaseSinglePropertyBuilder(String name, PropertySerializer parser) {
super(name);
this.parser = parser;
}
protected PropertySerializer getParser() {
return parser;
}
@SuppressWarnings("unchecked")
@Override
public B require(PropertyConstraint super T> constraint) {
parser = parser.withConstraint(constraint);
return (B) this;
}
/**
* Returns a new builder that can be used to build a property
* handling lists of Ts. The validators already added are
* converted to list validators. The default value cannot have
* previously been set.
*
* @return A new list property builder
*
* @throws IllegalStateException if the default value has already been set
*
* @see #map(Collector)
*/
public GenericCollectionPropertyBuilder> toList() {
return map(Collectors.toList());
}
/**
* Returns a new builder that can be used to build a property
* with value type {@code }. The validators already added are
* converted to collection validators. The default value cannot
* have previously been set. The returned builder will support
* conversion to and from a delimited string.
*
* Example usage:
*
{@code
* // this can be set with
* // a,b,c
* PropertyDescriptor> whitelistSet =
* PropertyFactory.stringProperty("whitelist")
* .desc(...)
* .map(Collectors.toSet())
* .emptyDefaultValue()
* .build();
* }
*
* @return A new list property builder
*
* @throws IllegalStateException if the default value has already been set
*/
public > GenericCollectionPropertyBuilder map(Collector super T, ?, ? extends C> collector) {
if (isDefaultValueSet()) {
throw new IllegalStateException("The default value is already set!");
}
GenericCollectionPropertyBuilder result = new GenericCollectionPropertyBuilder<>(getName(), getParser(), collector);
if (isDescriptionSet()) {
result.desc(getDescription());
}
return result;
}
/**
* Returns a new builder that can be used to build a property
* handling {@code Optional}. The validators already added
* are used on the validator property. If the default value was
* previously set, it is converted to an optional with {@link Optional#of(Object)}.
*
* @param missingValue The string representation of the empty optional.
*
* @return A new property builder for an optional.
*/
public GenericPropertyBuilder> toOptional(String missingValue) {
AssertionUtil.requireParamNotNull("missingValue", missingValue);
PropertySerializer> serializer =
PropertyParsingUtil.toOptional(getParser(), missingValue);
GenericPropertyBuilder> result =
new GenericPropertyBuilder<>(this.getName(), serializer);
if (isDefaultValueSet()) {
result.defaultValue(Optional.of(getDefaultValue()));
}
if (isDescriptionSet()) {
result.desc(getDescription());
}
return result;
}
@Override
public PropertyDescriptor build() {
return new PropertyDescriptor<>(
getName(),
getDescription(),
getDefaultValue(),
parser,
typeId,
isXPathAvailable);
}
}
/**
* Generic builder for a single-value property.
*
* @param Type of values the property handles
*
* @author Clément Fournier
* @since 6.10.0
*/
// Note: This type is used to fix the first type parameter for classes that don't need more API.
public static class GenericPropertyBuilder extends BaseSinglePropertyBuilder, T> {
GenericPropertyBuilder(String name, PropertySerializer parser) {
super(name, parser);
}
}
/**
* Specialized builder for regex properties. Allows specifying the pattern as a
* string, with {@link #defaultValue(String)}.
*
* @author Clément Fournier
* @since 6.10.0
*/
public static final class RegexPropertyBuilder extends BaseSinglePropertyBuilder {
RegexPropertyBuilder(String name) {
super(name, PropertyParsingUtil.REGEX);
}
/**
* Specify a default value using a string pattern. The argument is
* compiled to a pattern using {@link Pattern#compile(String)}.
*
* @param pattern String pattern
*
* @return The same builder
*
* @throws java.util.regex.PatternSyntaxException If the argument is not a valid pattern
*/
public RegexPropertyBuilder defaultValue(String pattern) {
super.defaultValue(Pattern.compile(pattern));
return this;
}
/**
* Specify a default value using a string pattern. The argument is
* compiled to a pattern using {@link Pattern#compile(String, int)}.
*
* @param pattern String pattern
* @param flags Regex compilation flags, the same as for {@link Pattern#compile(String, int)}
*
* @return The same builder
*
* @throws java.util.regex.PatternSyntaxException If the argument is not a valid pattern
* @throws IllegalArgumentException If bit values other than those corresponding to the defined
* match flags are set in {@code flags}
*/
public RegexPropertyBuilder defaultValue(String pattern, int flags) {
super.defaultValue(Pattern.compile(pattern, flags));
return this;
}
}
/**
* Generic builder for a collection-valued property.
* This class adds methods related to {@link #defaultValue(Iterable)}
* to make its use more flexible. See e.g. {@link #defaultValues(Object, Object[])}.
*
* Note: this is designed to support arbitrary collections.
* Pre-7.0.0, the only collections available from the {@link PropertyFactory}
* are list types though.
*
* @param Component type of the collection
* @param Collection type for the property being built
*
* @author Clément Fournier
* @since 6.10.0
*/
public static final class GenericCollectionPropertyBuilder> extends PropertyBuilder, C> {
private PropertySerializer itemParser;
private final Collector super V, ?, ? extends C> collector;
private final List> collectionConstraints = new ArrayList<>();
/**
* Builds a new builder for a collection type. Package-private.
*/
GenericCollectionPropertyBuilder(String name,
PropertySerializer itemParser,
Collector super V, ?, ? extends C> collector) {
super(name);
this.itemParser = itemParser;
this.collector = collector;
}
private C getDefaultValue(Iterable extends V> list) {
return IteratorUtil.toStream(list).collect(collector);
}
@Override
public GenericCollectionPropertyBuilder require(PropertyConstraint super C> constraint) {
collectionConstraints.add(constraint);
return this;
}
/**
* Specify a default value. This will be converted to type
* {@code } with the supplied collector.
*
* @param val List of values
*
* @return The same builder
*/
public GenericCollectionPropertyBuilder defaultValue(Iterable extends V> val) {
super.defaultValue(getDefaultValue(val));
return this;
}
/**
* Specify default values. To specify an empty
* default value, use {@link #emptyDefaultValue()}.
*
* @param head First value
* @param tail Rest of the values
*
* @return The same builder
*/
@SuppressWarnings("unchecked")
public GenericCollectionPropertyBuilder defaultValues(V head, V... tail) {
return this.defaultValue(listOf(head, tail));
}
/**
* Specify that the default value is an empty collection.
*
* @return The same builder
*/
public GenericCollectionPropertyBuilder emptyDefaultValue() {
return this.defaultValue(emptyList());
}
/**
* Require that the given constraint be fulfilled on each item of the
* value of this property. This is a convenient shorthand for
* {@code require(constraint.toCollectionConstraint())}.
*
* @param constraint Constraint to impose on the items of the collection value
*
* @return The same builder
*/
public GenericCollectionPropertyBuilder requireEach(PropertyConstraint super V> constraint) {
this.itemParser = itemParser.withConstraint(constraint);
return this;
}
@Override
public PropertyDescriptor build() {
PropertySerializer syntax = PropertyParsingUtil.delimitedString(itemParser, collector);
syntax = PropertyParsingUtil.withAllConstraints(syntax, CollectionUtil.map(itemParser.getConstraints(), PropertyConstraint::toCollectionConstraint));
syntax = PropertyParsingUtil.withAllConstraints(syntax, collectionConstraints);
return new PropertyDescriptor<>(
getName(),
getDescription(),
getDefaultValue(),
syntax,
typeId,
isXPathAvailable);
}
}
}