NamedConfigs.java
package swingtree.style;
import com.google.errorprone.annotations.Immutable;
import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import swingtree.api.Configurator;
import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* An immutable value container that stores {@link NamedConf} instances
* representing a mapping of unique string names to styles of type {@link S}.
* The {@link NamedConf} instances are stored in an array and can be accessed
* by their unique name.
* Yes, this class could have been a linked hashmap or treemap
* however, we do not expect the existence of more than a handful
* of named styles in a {@link StyleConf} instance which is why we chose
* to use an array instead as it is more memory as well as CPU efficient
* to just iterate over a few array elements than to use a hashmap or treemap.
*
* @param <S> The type of the style.
*/
@Immutable(containerOf = "S")
@SuppressWarnings("Immutable")
final class NamedConfigs<S> implements Simplifiable<NamedConfigs<S>>
{
private static final NamedConfigs<?> EMPTY = new NamedConfigs<>();
private static final Logger log = LoggerFactory.getLogger(NamedConfigs.class);
static <S> NamedConfigs<S> of(NamedConf<S> defaultStyle ) {
return new NamedConfigs<>( defaultStyle );
}
static <S> NamedConfigs<S> empty() { return (NamedConfigs<S>) EMPTY; }
private final NamedConf<S>[] _styles;
@SafeVarargs
private NamedConfigs(NamedConf<S>... styles ) {
_styles = Objects.requireNonNull(styles);
// No nll entries:
for ( NamedConf<S> style : styles )
Objects.requireNonNull(style);
// No duplicate names:
Set<String> names = new HashSet<>(styles.length * 2);
for ( NamedConf<S> style : styles )
if ( !names.add(style.name()) )
throw new IllegalArgumentException("Duplicate style name: " + style.name());
}
public int size() { return _styles.length; }
public List<NamedConf<S>> namedStyles() { return Collections.unmodifiableList(Arrays.asList(_styles)); }
public Stream<S> stylesStream() {
return namedStyles()
.stream()
.map(NamedConf::style);
}
public NamedConfigs<S> withNamedStyle(String name, S style ) {
Objects.requireNonNull(name);
Objects.requireNonNull(style);
int foundIndex = _findNamedStyle(name);
if ( foundIndex == -1 ) {
NamedConf<S>[] styles = Arrays.copyOf(_styles, _styles.length + 1);
styles[styles.length - 1] = NamedConf.of(name, style);
return new NamedConfigs<>(styles);
}
NamedConf<S>[] styles = Arrays.copyOf(_styles, _styles.length);
styles[foundIndex] = NamedConf.of(name, style);
return new NamedConfigs<>(styles);
}
public NamedConfigs<S> mapStyles( Configurator<S> f ) {
Objects.requireNonNull(f);
return mapNamedStyles( ns -> NamedConf.of(ns.name(), f.configure(ns.style())) );
}
public NamedConfigs<S> mapNamedStyles( Configurator<NamedConf<S>> f ) {
Objects.requireNonNull(f);
NamedConf<S>[] newStyles = null;
for ( int i = 0; i < _styles.length; i++ ) {
NamedConf<S> mapped = _styles[i];
try {
mapped = f.configure(_styles[i]);
} catch ( Exception e ) {
log.error(
"Failed to map named style '" + _styles[i] + "' using " +
"the provided function '" + f + "'.",
e
);
}
if ( newStyles == null && !mapped.equals(_styles[i]) ) {
newStyles = Arrays.copyOf(_styles, _styles.length);
// We avoid heap allocation if possible!
}
if ( newStyles != null )
newStyles[i] = mapped;
}
if ( newStyles == null )
return this;
return new NamedConfigs<>(newStyles);
}
private int _findNamedStyle( String name ) {
for ( int i = 0; i < _styles.length; i++ ) {
if ( _styles[i].name().equals(name) )
return i;
}
return -1;
}
public @Nullable S get(String name ) {
Objects.requireNonNull(name);
int foundIndex = _findNamedStyle(name);
if ( foundIndex == -1 )
return null;
return _styles[foundIndex].style();
}
public Optional<S> find( String name ) {
Objects.requireNonNull(name);
return Optional.ofNullable(get(name));
}
public List<S> sortedByNames() {
return Collections.unmodifiableList(
namedStyles()
.stream()
.sorted(Comparator.comparing(NamedConf::name))
.map(NamedConf::style)
.collect(Collectors.toList())
);
}
/**
* Returns true if at least one of the named styles in this instance passes the test.
* The test is performed by the provided predicate.
*
* @param namedStyleTester The predicate to test the named styles against.
* @return True if at least one of the named styles in this instance passes the test.
*/
public boolean any( Predicate<NamedConf<S>> namedStyleTester ) {
return Arrays.stream(_styles).anyMatch(namedStyleTester);
}
public String toString( String defaultName, String styleType ) {
if ( styleType.isEmpty() )
styleType = this.getClass().getSimpleName();
else
styleType += "=";
if ( this.size() == 1 )
return String.valueOf(this.get(defaultName));
else
return this.namedStyles()
.stream()
.map(e -> e.name() + "=" + e.style())
.collect(Collectors.joining(", ", styleType+"[", "]"));
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(getClass().getSimpleName()).append("[");
for ( int i = 0; i < _styles.length; i++ ) {
sb.append(_styles[i].name()).append("=").append(_styles[i].style());
if ( i < _styles.length - 1 )
sb.append(", ");
}
sb.append("]");
return sb.toString();
}
@Override
public int hashCode() {
return Arrays.hashCode(_styles);
}
@Override
public boolean equals( Object obj ) {
if ( obj == null ) return false;
if ( obj == this ) return true;
if ( obj.getClass() != getClass() ) return false;
NamedConfigs<?> rhs = (NamedConfigs<?>) obj;
return Arrays.equals(_styles, rhs._styles);
}
@Override
public NamedConfigs<S> simplified() {
return mapNamedStyles(NamedConf::simplified);
}
}