package org.netbeans.gradle.project.api.config;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import javax.annotation.Nonnull;
import org.jtrim.collections.CollectionsEx;
import org.jtrim.collections.Equality;
import org.jtrim.collections.EqualityComparator;
import org.jtrim.property.PropertyFactory;
import org.jtrim.property.PropertySource;
import org.jtrim.utils.ExceptionHelper;
import org.netbeans.gradle.project.properties.standard.CommonProperties;
/**
* Defines how to store and parse the value of a particular property.
* <P>
* The values of the properties are stored in a tree where each edge is
* identified by a string and each node might store a string value. There is no
* restriction on the name of the edges and the values stored in the nodes. In
* most practical use-cases, you can think of edges as {@literal XML} tags and
* values as their {@literal CDATA} content.
* <P>
* Properties are parsed in multiple steps:
* <ol>
* <li>
* The value stored in the configuration tree is parsed into a user defined
* type. This transformation must be idempotent and therefore may not use
* information from other sources but the configuration tree. The purpose of
* this step is to cache the result of the parsed value and prevent
* unnecessary re-parsing and check if the value in the settings changed or
* not (based on a possibly custom equality comparison).
* </li>
* <li>
* The result of the previous step is transformed into the final value
* of the property. This transformation might include information from
* external sources, even from sources which might change over time
* (assuming the changes are trackable).
* </li>
* <li>
* The value of the property can optionally be merged with a parent property.
* This is usually used to merge the value of a custom profile with the
* value of the default profile.
* </li>
* </ol>
* <P>
* Instances of {@code PropertyDef} are immutable (assuming the implementation
* of its properties are immutable which is strongly recommended).
* <P>
* Instances of {@code PropertyDef} are created through its {@link Builder}.
*
* @param <ValueKey> the type of the user defined object the property parsed
* into the first step
* @param <ValueType> the type of the final value of the property
*
* @see ConfigTree
* @see ProjectSettingsProvider
*/
public final class PropertyDef<ValueKey, ValueType> {
/**
* The builder of {@code PropertyDef} used to create new {@link PropertyDef}
* instances.
*
* @param <ValueKey> the type of the user defined object the property parsed
* into the first step
* @param <ValueType> the type of the final value of the property
*/
public static final class Builder<ValueKey, ValueType> {
private final List<ConfigPath> configPaths;
private PropertyKeyEncodingDef<ValueKey> keyEncodingDef;
private PropertyValueDef<ValueKey, ValueType> valueDef;
private ValueMerger<ValueType> valueMerger;
private EqualityComparator<? super ValueKey> valueKeyEquality;
/**
* Initializes this builder with the given root path in the configuration
* tree. That is, the configuration trees in
* {@link PropertyKeyEncodingDef PropertyKeyEncodingDef}
* will be relative to this path.
*
* @param configPath the root path of the property to be created. This
* argument cannot be {@code null}.
*/
public Builder(@Nonnull ConfigPath configPath) {
this(Collections.singleton(configPath));
}
/**
* Initializes this builder with the multiple root paths in the
* configuration tree. That is, the root paths specify under which
* subtrees will the property to be parsed found.
* <P>
* The {@link PropertyKeyEncodingDef PropertyKeyEncodingDef}
* will be relative to the deepest common parent of all root paths. For
* example, if {@code /root/parent/sub1} and {@code /root/parent/sub2}
* is specified as root, the common parent will be {@code /root/parent}.
*
* @param configPaths the root paths of the property to be created. This
* argument cannot be {@code null} and cannot contain {@code null}
* elements.
*/
public Builder(@Nonnull Collection<ConfigPath> configPaths) {
this.configPaths = CollectionsEx.readOnlyCopy(configPaths);
ExceptionHelper.checkNotNullElements(this.configPaths, "configPaths");
this.keyEncodingDef = NoOpKeyEncodingDef.getInstance();
this.valueDef = NoOpValueDef.getInstance();
this.valueMerger = CommonProperties.getParentIfNullValueMerger();
this.valueKeyEquality = Equality.naturalEquality();
}
/**
* Sets the parser used to parse the value found in the configuration
* tree to a more meaningful object. This can be an identity
* transformation, however for efficency, it is usually recommended to
* use a more specific custom type.
* <P>
* The default implementation will always return {@code null} for any
* configuration tree and will transform any value to an empty to tree.
*
* @param keyEncodingDef the parser used to parse the value found in the
* configuration tree to a more meaningful object. This argument
* cannot be {@code null}.
*/
public void setKeyEncodingDef(@Nonnull PropertyKeyEncodingDef<ValueKey> keyEncodingDef) {
ExceptionHelper.checkNotNullArgument(keyEncodingDef, "keyEncodingDef");
this.keyEncodingDef = keyEncodingDef;
}
/**
* Sets the parser used to transform the value created by the
* {@link PropertyKeyEncodingDef} to the final value.
* <P>
* The default implementation always transforms everything to
* {@code null} (in both direction).
*
* @param valueDef the parser used to transform the value created by the
* {@code PropertyKeyEncodingDef} to the final value. This argument
* cannot be {@code null}.
*/
public void setValueDef(@Nonnull PropertyValueDef<ValueKey, ValueType> valueDef) {
ExceptionHelper.checkNotNullArgument(valueDef, "valueDef");
this.valueDef = valueDef;
}
/**
* Sets the {@link ValueMerger} defining the way of combining the value
* of this property with its fallback property. Which is usually, how to
* fall back to the fallback value.
* <P>
* The default implementation simply returns the parent value if the
* current value is {@code null}.
*
* @param valueMerger the {@link ValueMerger} defining the way of
* combining the value of this property with its fallback property.
* This argument cannot be {@code null}.
*/
public void setValueMerger(@Nonnull ValueMerger<ValueType> valueMerger) {
ExceptionHelper.checkNotNullArgument(valueMerger, "valueMerger");
this.valueMerger = valueMerger;
}
/**
* Sets the equality definition of keys provided by the specified
* {@link PropertyKeyEncodingDef}.
* <P>
* The default implementation compares objects by their {@code equals}
* method (which is usually appropriate).
*
* @param valueKeyEquality the equality definition of keys provided by
* the specified {@link PropertyKeyEncodingDef}. This argument cannot
* be {@code null}.
*/
public void setValueKeyEquality(@Nonnull EqualityComparator<? super ValueKey> valueKeyEquality) {
ExceptionHelper.checkNotNullArgument(valueKeyEquality, "valueKeyEquality");
this.valueKeyEquality = valueKeyEquality;
}
/**
* Creates a new immutable instance of {@code PropertyDef} based on the
* properties set before calling this method.
*
* @return a new immutable instance of {@code PropertyDef} based on the
* properties set before calling this method. This method never
* returns {@code null}.
*/
@Nonnull
public PropertyDef<ValueKey, ValueType> create() {
return new PropertyDef<>(this);
}
}
private final List<ConfigPath> configPaths;
private final PropertyKeyEncodingDef<ValueKey> keyEncodingDef;
private final PropertyValueDef<ValueKey, ValueType> valueDef;
private final ValueMerger<ValueType> valueMerger;
private final EqualityComparator<? super ValueKey> valueKeyEquality;
private PropertyDef(Builder<ValueKey, ValueType> builder) {
this.keyEncodingDef = builder.keyEncodingDef;
this.valueDef = builder.valueDef;
this.valueMerger = builder.valueMerger;
this.valueKeyEquality = builder.valueKeyEquality;
this.configPaths = builder.configPaths;
}
private PropertyDef(PropertyDef<ValueKey, ValueType> baseDef, List<ConfigPath> newConfigPaths) {
this.keyEncodingDef = baseDef.keyEncodingDef;
this.valueDef = baseDef.valueDef;
this.valueMerger = baseDef.valueMerger;
this.valueKeyEquality = baseDef.valueKeyEquality;
this.configPaths = newConfigPaths;
}
/**
* Returns a new {@code PropertyDef} which has the same properties as this
* {@code PropertyDef} except that all of its {@link #getConfigPaths() configuration roots}
* will start with the given keys (as done by
* {@link ConfigPath#withParentPath(String[]) ConfigPath.withParentPath}).
*
* @param parentKeys the keys prepended to all the configuration roots. This
* argument cannot be {@code null} and none of the keys can be {@code null}.
* @return the new {@code PropertyDef} which has the same properties as this
* {@code PropertyDef} except that all of its configuration roots
* will start with the given keys. This method never returns {@code null}.
*/
@Nonnull
public PropertyDef<ValueKey, ValueType> withParentConfigPath(@Nonnull String... parentKeys) {
List<ConfigPath> newConfigPaths = new ArrayList<>(configPaths.size());
for (ConfigPath path: configPaths) {
newConfigPaths.add(path.withParentPath(parentKeys));
}
return new PropertyDef<>(this, Collections.unmodifiableList(newConfigPaths));
}
/**
* Returns the configuration roots from which this property reads its value.
* <P>
* The {@link PropertyKeyEncodingDef PropertyKeyEncodingDef}
* will be relative to the deepest common parent of all root paths. For
* example, if {@code /root/parent/sub1} and {@code /root/parent/sub2}
* is specified as root, the common parent will be {@code /root/parent}.
*
* @return the configuration roots from which this property reads its value.
* This method never returns {@code null}.
*/
@Nonnull
public List<ConfigPath> getConfigPaths() {
return configPaths;
}
/**
* Returns the parser used to parse the value found in the configuration
* tree to a more meaningful object. The result of this parsing will be
* converted by the {@link PropertyValueDef} to its final value.
*
* @return the parser used to parse the value found in the configuration
* tree to a more meaningful object. This method never returns
* {@code null}.
*/
@Nonnull
public PropertyKeyEncodingDef<ValueKey> getKeyEncodingDef() {
return keyEncodingDef;
}
/**
* Returns the parser used to transform the value created by the
* {@link PropertyKeyEncodingDef} to the final value.
*
* @return the parser used to transform the value created by the
* {@link PropertyKeyEncodingDef} to the final value. This method
* never returns {@code null}.
*/
@Nonnull
public PropertyValueDef<ValueKey, ValueType> getValueDef() {
return valueDef;
}
/**
* Returns the {@link ValueMerger} defining the way of combining the value
* of this property with its fallback property. Which is usually, how to
* fall back to the fallback value.
*
* @return the {@link ValueMerger} defining the way of combining the value
* of this property with its fallback property. This method never returns
* {@code null}.
*/
@Nonnull
public ValueMerger<ValueType> getValueMerger() {
return valueMerger;
}
/**
* Returns the equality definition of keys provided by the specified
* {@link PropertyKeyEncodingDef}. This can be used to determine of the
* value of the property has changed or not and so, should we update the
* properties.
*
* @return the equality definition of keys provided by the specified
* {@link PropertyKeyEncodingDef}. This method never returns {@code null}.
*/
@Nonnull
public EqualityComparator<? super ValueKey> getValueKeyEquality() {
return valueKeyEquality;
}
private static final class NoOpKeyEncodingDef<ValueKey>
implements
PropertyKeyEncodingDef<ValueKey> {
private static final PropertyKeyEncodingDef<?> INSTANCE = new NoOpKeyEncodingDef<>();
@SuppressWarnings("unchecked")
public static <ValueKey> PropertyKeyEncodingDef<ValueKey> getInstance() {
return (PropertyKeyEncodingDef<ValueKey>)INSTANCE;
}
@Override
public ValueKey decode(ConfigTree config) {
return null;
}
@Override
public ConfigTree encode(ValueKey value) {
return ConfigTree.EMPTY;
}
}
private static final class NoOpValueDef<ValueKey, ValueType>
implements
PropertyValueDef<ValueKey, ValueType> {
private static final PropertyValueDef<?, ?> INSTANCE = new NoOpValueDef<>();
@SuppressWarnings("unchecked")
public static <ValueKey, ValueType> PropertyValueDef<ValueKey, ValueType> getInstance() {
return (PropertyValueDef<ValueKey, ValueType>)INSTANCE;
}
@Override
public PropertySource<ValueType> property(ValueKey valueKey) {
return PropertyFactory.constSource(null);
}
@Override
public ValueKey getKeyFromValue(ValueType value) {
return null;
}
}
}