diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a05b8de --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +*.args +*.class +*~ +nbactions.xml +nb-configuration.xml +src/site/markdown/*.html +target/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..bacc917 --- /dev/null +++ b/README.md @@ -0,0 +1,36 @@ +# microBean™ Attributes + +[![Maven Central](https://img.shields.io/maven-central/v/org.microbean/microbean-attributes.svg?label=Maven%20Central)](https://search.maven.org/artifact/org.microbean/microbean-attributes) + +The microBean™ Attributes project provides classes and interfaces assisting with representing annotation-like constructs. + +# Status + +This project is currently experimental, in a pre-alpha state, and unsuitable for production use. + +# Compatibility + +**Until further notice, this project's APIs are subject to frequent backwards-incompatible signature and behavior +changes, regardless of project version and without notice.** + +# Requirements + +microBean™ Attributes requires a Java runtime of version 23 or higher. + +# Installation + +microBean™ Attributes is available on [Maven Central](https://search.maven.org/). Include microBean™ Attributes as a Maven +dependency: + +```xml + + org.microbean + microbean-attributes + + 0.0.1 + +``` + +# Documentation + +Full documentation is available at [microbean.github.io/microbean-attributes](https://microbean.github.io/microbean-attributes/). diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..1b66cf4 --- /dev/null +++ b/pom.xml @@ -0,0 +1,591 @@ + + + 4.0.0 + + org.microbean + microbean-attributes + 0.0.1-SNAPSHOT + + microBean™ Attributes + microBean™ Attributes: Utilities for representing certain kinds of annotations. + 2025 + https://microbean.github.io/microbean-attributes + + + microBean™ + http://microbean.systems/ + + + + + The Apache License, Version 2.0 + repo + Apache License 2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + + + + + + ljnelson + Laird Nelson + ljnelson@gmail.com + https://about.me/lairdnelson + + architect + developer + + -8 + + + + + ${scm.url} + ${scm.url} + https://github.com/microbean/microbean-attributes/ + HEAD + + + + Github + https://github.com/microbean/microbean-attributes/issues + + + + + sonatype-oss-repository-hosting + + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + Github Pages + ${project.name} Site + https://microbean.github.io/microbean-attributes/ + + + sonatype-oss-repository-hosting + https://oss.sonatype.org/content/repositories/snapshots + + + + + + ${project.basedir}/src/test/java/logging.properties + + + 23 + true + true + + + ${project.organization.name}. All rights reserved.]]> + <a href="${project.url}" target="_top"><span style="font-family:Lobster, cursive;">µb</span> ${project.artifactId}</a> ${project.version} + + 2 + + + + deploy,post-site,scm-publish:publish-scm + deployment + [maven-release-plugin] [skip ci] + v@{project.version} + false + + + ${project.reporting.outputDirectory} + ${project.scm.developerConnection} + gh-pages + + + true + false + + + + true + + https://oss.sonatype.org/ + 10 + + + UTF8 + UTF8 + scm:git:git@github.com:microbean/microbean-attributes.git + + + + + + + + + + + + org.junit + junit-bom + 5.11.4 + pom + import + + + + + + org.microbean + microbean-constant + 0.0.7 + + + + + + + + + + + org.microbean + microbean-constant + 0.0.7 + + + + + + org.junit.jupiter + junit-jupiter-api + test + + + + org.junit.jupiter + junit-jupiter-engine + test + + + + + + + + + maven-antrun-plugin + 3.1.0 + + + maven-assembly-plugin + 3.7.1 + + + maven-checkstyle-plugin + 3.6.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + project.basedir=${project.basedir} + project.build.sourceEncoding=${project.build.sourceEncoding} + + + + + com.puppycrawl.tools + checkstyle + 10.12.6 + + + + + maven-clean-plugin + 3.4.0 + + + + ${basedir} + + src/**/*~ + *~ + + + + + + + maven-compiler-plugin + 3.13.0 + + + -Xlint:all + -parameters + + + + + org.codehaus.plexus + plexus-java + 1.3.0 + + + + + maven-dependency-plugin + 3.8.1 + + + maven-deploy-plugin + 3.1.3 + + + maven-enforcer-plugin + 3.5.0 + + + maven-gpg-plugin + + 3.2.7 + + + maven-install-plugin + 3.1.3 + + + maven-jar-plugin + 3.4.2 + + + maven-javadoc-plugin + 3.11.2 + + true + + + microbean.idempotency + m + Idempotency: + + + microbean.nullability + cfm + Nullability: + + + microbean.threadsafety + fmpt + Thread Safety: + + + + + + maven-plugin-plugin + 3.9.0 + + + maven-project-info-reports-plugin + 3.8.0 + + + maven-release-plugin + + 3.1.1 + + + maven-resources-plugin + 3.3.1 + + + maven-scm-plugin + 2.1.0 + + + maven-scm-publish-plugin + 3.3.0 + + + maven-site-plugin + 3.21.0 + + + maven-source-plugin + 3.3.1 + + + attach-sources + + jar-no-fork + + + + + + maven-surefire-plugin + 3.5.2 + + + org.apache.maven.surefire + surefire-junit-platform + 3.5.2 + + + + + maven-toolchains-plugin + 3.2.0 + + + com.github.spotbugs + spotbugs-maven-plugin + 4.8.6.6 + + + org.codehaus.mojo + versions-maven-plugin + 2.18.0 + + + io.smallrye + jandex-maven-plugin + 3.2.3 + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.6.13 + true + + + + com.thoughtworks.xstream + xstream + 1.4.20 + + + + sonatype-oss-repository-hosting + ${nexusUrl} + ${autoReleaseAfterClose} + + + + + + + + maven-enforcer-plugin + + + enforce-maven + + enforce + + + + + 23 + + + 3.9.9 + + + + + + + + maven-surefire-plugin + + + + + junit.jupiter.execution.parallel.enabled=true + junit.jupiter.execution.parallel.mode.default=concurrent + + + + ${java.util.logging.config.file} + ${project.build.directory} + ${project.build.testOutputDirectory} + + + + + org.sonatype.plugins + nexus-staging-maven-plugin + + + + + + src/main/resources + + + + META-INF + ${project.basedir} + + LICENSE + + + + + + + + + + maven-javadoc-plugin + + + + javadoc-no-fork + + + + + + + + + + deployment + + + + maven-gpg-plugin + + + sign-artifacts + verify + + sign + + + + + + maven-javadoc-plugin + + + attach-javadocs + + jar + + + + + + maven-source-plugin + + + + + + + + diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java new file mode 100644 index 0000000..38b039e --- /dev/null +++ b/src/main/java/module-info.java @@ -0,0 +1,26 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2025 microBean™. + * + * Licensed 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. + */ + +/** + * Provides packages related to representing annotations. + * + * @author Laird Nelson + */ +module org.microbean.attributes { + + exports org.microbean.attributes; + + requires org.microbean.constant; + +} diff --git a/src/main/java/org/microbean/attributes/ArrayValue.java b/src/main/java/org/microbean/attributes/ArrayValue.java new file mode 100644 index 0000000..98f8afe --- /dev/null +++ b/src/main/java/org/microbean/attributes/ArrayValue.java @@ -0,0 +1,337 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2025 microBean™. + * + * Licensed 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.microbean.attributes; + +import java.util.Arrays; +import java.util.List; + +import java.lang.constant.ClassDesc; +import java.lang.constant.DynamicConstantDesc; +import java.lang.constant.MethodHandleDesc; +import java.lang.constant.MethodTypeDesc; + +import java.util.Optional; + +import org.microbean.constant.Constables; + +import static java.lang.constant.ConstantDescs.BSM_INVOKE; +import static java.lang.constant.ConstantDescs.CD_List; +import static java.lang.constant.ConstantDescs.CD_Object; +import static java.lang.constant.DirectMethodHandleDesc.Kind.STATIC; +import static java.lang.constant.DirectMethodHandleDesc.Kind.VIRTUAL; + +/** + * A {@link Value} holding other {@link Value}s. + * + * @param the type of the {@link Value}s + * + * @param value a non-{@code null} array of {@link Value}s + * + * @author Laird Nelson + */ +public final record ArrayValue>(T[] value) implements Value> { + + @SuppressWarnings("rawtypes") + private static final Value[] EMPTY_VALUE_ARRAY = new Value[0]; + + /** + * Creates a new {@link ArrayValue}. + * + * @param value a non-{@code null} array of {@link Value}s + * + * @exception NullPointerException if {@code value} is {@code null} + */ + public ArrayValue { + value = value.clone(); + } + + @Override // Comparable> + public final int compareTo(final ArrayValue other) { + if (other == null) { + return -1; + } else if (this.equals(other)) { + return 0; + } + return this.toString().compareTo(other.toString()); + } + + @Override // Constable + public final Optional>> describeConstable() { + final ClassDesc me = ClassDesc.of(this.getClass().getName()); + return Constables.describeConstable(Arrays.asList(this.value())) + .map(valueDesc -> DynamicConstantDesc.of(BSM_INVOKE, + MethodHandleDesc.ofMethod(STATIC, + me, + "of", + MethodTypeDesc.of(me, + CD_List)), + valueDesc)); + } + + @Override // Record + public final boolean equals(final Object other) { + return + other == this || + // Follow java.lang.annotation.Annotation requirements. + other != null && other.getClass() == this.getClass() && Arrays.equals(this.value(), ((ArrayValue)other).value()); + } + + @Override // Record + public final int hashCode() { + // Follow java.lang.annotation.Annotation requirements. + return Arrays.hashCode(this.value()); + } + + @Override // Record + public final String toString() { + return Arrays.asList(this.value()).toString(); + } + + /** + * Returns the array of suitably-typed {@link Value}s this {@link ArrayValue} holds. + * + *

No reference to the returned array is kept.

+ * + * @return a non-{@code null} array of suitably-typed {@link Value}s + */ + @Override // Record + public final T[] value() { + return this.value.clone(); + } + + /** + * Returns an {@link ArrayValue} suitable for the supplied argument. + * + * @param values a non-{@code null} array of values + * + * @return a non-{@code null} {@link ArrayValue} + * + * @exception NullPointerException if {@code values} is {@code null} + * + * @see #of(Value[]) + */ + public static final ArrayValue of(final boolean... values) { + final var va = new BooleanValue[values.length]; + for (int i = 0; i < values.length; i++) { + va[i] = BooleanValue.of(values[i]); + } + return new ArrayValue<>(va); + } + + /** + * Returns an {@link ArrayValue} suitable for the supplied argument. + * + * @param values a non-{@code null} array of values + * + * @return a non-{@code null} {@link ArrayValue} + * + * @exception NullPointerException if {@code values} is {@code null} + * + * @see #of(Value[]) + */ + public static final ArrayValue of(final byte... values) { + final var va = new ByteValue[values.length]; + for (int i = 0; i < values.length; i++) { + va[i] = ByteValue.of(values[i]); + } + return new ArrayValue<>(va); + } + + /** + * Returns an {@link ArrayValue} suitable for the supplied argument. + * + * @param values a non-{@code null} array of values + * + * @return a non-{@code null} {@link ArrayValue} + * + * @exception NullPointerException if {@code values} is {@code null} + * + * @see #of(Value[]) + */ + public static final ArrayValue of(final char... values) { + final var va = new CharValue[values.length]; + for (int i = 0; i < values.length; i++) { + va[i] = CharValue.of(values[i]); + } + return new ArrayValue<>(va); + } + + /** + * Returns an {@link ArrayValue} suitable for the supplied argument. + * + * @param values a non-{@code null} array of values + * + * @return a non-{@code null} {@link ArrayValue} + * + * @exception NullPointerException if {@code values} is {@code null} + * + * @see #of(Value[]) + */ + public static final ArrayValue of(final double... values) { + final var va = new DoubleValue[values.length]; + for (int i = 0; i < values.length; i++) { + va[i] = DoubleValue.of(values[i]); + } + return new ArrayValue<>(va); + } + + /** + * Returns an {@link ArrayValue} suitable for the supplied argument. + * + * @param values a non-{@code null} array of values + * + * @return a non-{@code null} {@link ArrayValue} + * + * @exception NullPointerException if {@code values} is {@code null} + * + * @see #of(Value[]) + */ + public static final ArrayValue of(final float... values) { + final var va = new FloatValue[values.length]; + for (int i = 0; i < values.length; i++) { + va[i] = FloatValue.of(values[i]); + } + return new ArrayValue<>(va); + } + + /** + * Returns an {@link ArrayValue} suitable for the supplied argument. + * + * @param values a non-{@code null} array of values + * + * @return a non-{@code null} {@link ArrayValue} + * + * @exception NullPointerException if {@code values} is {@code null} + * + * @see #of(Value[]) + */ + public static final ArrayValue of(final int... values) { + final var va = new IntValue[values.length]; + for (int i = 0; i < values.length; i++) { + va[i] = IntValue.of(values[i]); + } + return new ArrayValue<>(va); + } + + /** + * Returns an {@link ArrayValue} suitable for the supplied argument. + * + * @param values a non-{@code null} array of values + * + * @return a non-{@code null} {@link ArrayValue} + * + * @exception NullPointerException if {@code values} is {@code null} + * + * @see #of(Value[]) + */ + public static final ArrayValue of(final long... values) { + final var va = new LongValue[values.length]; + for (int i = 0; i < values.length; i++) { + va[i] = LongValue.of(values[i]); + } + return new ArrayValue<>(va); + } + + /** + * Returns an {@link ArrayValue} suitable for the supplied argument. + * + * @param values a non-{@code null} array of values + * + * @return a non-{@code null} {@link ArrayValue} + * + * @exception NullPointerException if {@code values} is {@code null} + * + * @see #of(Value[]) + */ + public static final ArrayValue of(final short... values) { + final var va = new ShortValue[values.length]; + for (int i = 0; i < values.length; i++) { + va[i] = ShortValue.of(values[i]); + } + return new ArrayValue<>(va); + } + + /** + * Returns an {@link ArrayValue} suitable for the supplied argument. + * + * @param values a non-{@code null} array of values + * + * @return a non-{@code null} {@link ArrayValue} + * + * @exception NullPointerException if {@code values} is {@code null} + * + * @see #of(Value[]) + */ + public static final ArrayValue of(final Class... values) { + final var va = new ClassValue[values.length]; + for (int i = 0; i < values.length; i++) { + va[i] = ClassValue.of(values[i]); + } + return new ArrayValue<>(va); + } + + /** + * Returns an {@link ArrayValue} suitable for the supplied argument. + * + * @param values a non-{@code null} array of values + * + * @return a non-{@code null} {@link ArrayValue} + * + * @exception NullPointerException if {@code values} is {@code null} + * + * @see #of(Value[]) + */ + public static final ArrayValue of(final String... values) { + final var va = new StringValue[values.length]; + for (int i = 0; i < values.length; i++) { + va[i] = StringValue.of(values[i]); + } + return new ArrayValue<>(va); + } + + /** + * Returns an {@link ArrayValue} suitable for the supplied argument. + * + * @param the type of {@link Value} held by the returned {@link ArrayValue} + * + * @param value a non-{@code null} {@link List} of suitably-typed {@link Value}s + * + * @return a non-{@code null} {@link ArrayValue} + * + * @exception NullPointerException if {@code value} is {@code null} + * + * @see #of(Value[]) + */ + @SuppressWarnings("unchecked") + // Called by #describeConstable() + public static final > ArrayValue of(final List value) { + return of(value.toArray((T[])EMPTY_VALUE_ARRAY)); + } + + /** + * Returns an {@link ArrayValue} suitable for the supplied argument. + * + * @param the type of {@link Value} held by the returned {@link ArrayValue} + * + * @param value a non-{@code null} array of suitably-typed {@link Value}s + * + * @return a non-{@code null} {@link ArrayValue} + * + * @exception NullPointerException if {@code value} is {@code null} + */ + public static final > ArrayValue of(final T[] value) { + return new ArrayValue<>(value); + } + +} diff --git a/src/main/java/org/microbean/attributes/Attributes.java b/src/main/java/org/microbean/attributes/Attributes.java new file mode 100644 index 0000000..06867c1 --- /dev/null +++ b/src/main/java/org/microbean/attributes/Attributes.java @@ -0,0 +1,375 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2025 microBean™. + * + * Licensed 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.microbean.attributes; + +import java.lang.constant.ClassDesc; +import java.lang.constant.DynamicConstantDesc; +import java.lang.constant.MethodHandleDesc; +import java.lang.constant.MethodTypeDesc; + +import java.util.Optional; + +import java.util.function.Predicate; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.TreeMap; + +import org.microbean.constant.Constables; + +import static java.lang.constant.ConstantDescs.BSM_INVOKE; +import static java.lang.constant.ConstantDescs.CD_Map; +import static java.lang.constant.ConstantDescs.CD_String; +import static java.lang.constant.DirectMethodHandleDesc.Kind.STATIC; + +import static java.util.Collections.unmodifiableSortedMap; + +/** + * A {@link Value} with a {@linkplain #name() name}, {@linkplain #values() named values}, {@linkplain #notes() + * non-normative named values}, and {@linkplain #attributes() metadata}. + * + * @param name a non-{@code null} name of this {@link Attributes} + * + * @param values a non-{@code null} {@link Map} of named {@linkplain Value values} associated with this {@link Attributes} + * + * @param notes a non-{@code null} {@link Map} of non-normative named {@linkplain Value values} associated with this + * {@link Attributes} + * + * @param attributes a non-{@code null} {@link Map} of named metadata associated with this {@link Attributes} + * + * @author Laird Nelson + */ +public final record Attributes(String name, Map> values, Map> notes, Map> attributes) + implements Value { + + /** + * Creates a new {@link Attributes}. + * + * @param name a non-{@code null} name of this {@link Attributes} + * + * @param values a non-{@code null} {@link Map} of named {@linkplain Value values} associated with this {@link Attributes} + * + * @param notes a non-{@code null} {@link Map} of non-normative named {@linkplain Value values} associated with this + * {@link Attributes} + * + * @param attributes a non-{@code null} {@link Map} of named metadata associated with this {@link Attributes} + * + * @exception NullPointerException if any argument is {@code null} + */ + public Attributes { + Objects.requireNonNull(name, "name"); + switch (values.size()) { + case 0: + values = Map.of(); + break; + case 1: + values = Map.copyOf(values); + break; + default: + final TreeMap> sortedValues = new TreeMap<>(); + sortedValues.putAll(values); + values = unmodifiableSortedMap(sortedValues); + } + if (values.containsKey("")) { + throw new IllegalArgumentException("values: " + values); + } + switch (notes.size()) { + case 0: + notes = Map.of(); + break; + case 1: + notes = Map.copyOf(notes); + break; + default: + final TreeMap> sortedNotes = new TreeMap<>(); + sortedNotes.putAll(notes); + notes = unmodifiableSortedMap(sortedNotes); + } + if (notes.containsKey("")) { + throw new IllegalArgumentException("notes: " + notes); + } + switch (attributes.size()) { + case 0: + attributes = Map.of(); + break; + case 1: + attributes = Map.copyOf(attributes); + break; + default: + final TreeMap> sortedAttributes = new TreeMap<>(); + for (final Entry> e : attributes.entrySet()) { + sortedAttributes.put(e.getKey(), List.copyOf(e.getValue())); + } + attributes = unmodifiableSortedMap(sortedAttributes); + break; + } + if (attributes.containsKey("")) { + throw new IllegalArgumentException("attributes: " + attributes); + } + } + + /** + * Returns a {@link List} of {@link Attributes} associated with the supplied {@code key}, or an {@linkplain + * List#isEmpty() empty List} if there is no such {@link List}. + * + * @param key a {@link String}; must not be {@code null} + * + * @return a non-{@code null} {@link List} of {@link Attributes} + * + * @exception NullPointerException if {@code key} is {@code null} + */ + public final List attributes(final String key) { + return this.attributes().getOrDefault(key, List.of()); + } + + @Override // Comparable + public final int compareTo(final Attributes other) { + if (other == null) { + return -1; + } else if (this.equals(other)) { + return 0; + } + final int c = (this.name() + this.values()).compareTo(other.name() + other.values()); + // (Possible? that c simply cannot be 0 here?) + return c != 0 ? c : (this.notes().toString() + this.attributes()).compareTo(other.notes().toString() + other.attributes()); + } + + @Override // Constable + public final Optional> describeConstable() { + final ClassDesc me = ClassDesc.of(this.getClass().getName()); + return Constables.describeConstable(this.values()) + .flatMap(valuesDesc -> Constables.describeConstable(this.notes()) + .flatMap(notesDesc -> Constables.describeConstable(this.attributes()) + .map(attributesDesc -> DynamicConstantDesc.of(BSM_INVOKE, + MethodHandleDesc.ofMethod(STATIC, + me, + "of", + MethodTypeDesc.of(me, + CD_String, + CD_Map, + CD_Map, + CD_Map)), + this.name(), + valuesDesc, + notesDesc, + attributesDesc)))); + } + + /** + * Returns {@code true} if this {@link Attributes} equals the supplied {@link Object}. + * + *

If the supplied {@link Object} is also an {@link Attributes} and has a {@linkplain #name() name} equal to this + * {@link Attributes}' {@linkplain #name() name} and a {@linkplain #values() values} {@link Map} {@linkplain + * Map#equals(Object) equal to} this {@link Attributes}' {@linkplain #values() values} {@link Map}, this method + * returns {@code true}.

+ * + *

This method returns {@code false} in all other cases.

+ * + * @param other an {@link Object}; may be {@code null} + * + * @return {@code true} if this {@link Attributes} equals the supplied {@link Object} + * + * @see #hashCode() + * + * @see #name() + * + * @see #values() + */ + @Override // Record + public final boolean equals(final Object other) { + return + other == this || + // Follow java.lang.annotation.Annotation requirements. + other instanceof Attributes a && this.name().equals(a.name()) && this.values().equals(a.values()); + } + + /** + * Returns a hash code value for this {@link Attributes} derived solely from its {@linkplain #values() values}. + * + * @return a hash code value + * + * @see #values() + * + * @see #equals(Object) + */ + @Override // Record + public final int hashCode() { + // Follow java.lang.annotation.Annotation requirements. + int hashCode = 0; + for (final Entry> e : this.values().entrySet()) { + hashCode += (127 * e.getKey().hashCode()) ^ e.getValue().hashCode(); + } + return hashCode; + } + + /** + * Returns {@code true} if {@code a} appears in the {@link #attributes(String) attributes} of this {@link Attributes}, + * or any of their attributes. + * + *

Notably, this method does not return {@code true} if this {@link Attributes} {@linkplain + * #equals(Object) is equal to} {@code a}.

+ * + * @param a an {@link Attributes}; must not be {@code null} + * + * @return {@code true} if {@code a} appears in the {@link #attributes(String) attributes} of this {@link Attributes}, + * or any of their attributes + * + * @exception NullPointerException if {@code a} is {@code null} + * + * @see #attributes(String) + * + * @see #equals(Object) + */ + public final boolean isa(final Attributes a) { + return this.attributesSatisfy(a::equals); + } + + /** + * Returns {@code true} if any of the {@linkplain #attributes(String) attributes} reachable from this {@link + * Attributes} satisfy the supplied {@link Predicate}. + * + * @param p a {@link Predicate}; must not be {@code null} + * + * @return {@code true} if any of the {@linkplain #attributes(String) attributes} reachable from this {@link + * Attributes} satisfy the supplied {@link Predicate}; {@code false} otherwise + * + * @exception NullPointerException if {@code p} is {@code null} + * + * @see #attributes(String) + */ + public final boolean attributesSatisfy(final Predicate p) { + for (final Attributes md : this.attributes(this.name())) { + if (p.test(md) || md.attributesSatisfy(p)) { + return true; + } + } + return false; + } + + /** + * Returns a suitably-typed {@link Value} indexed under the supplied {@code name}, or {@code null} if no such {@link + * Value} exists. + * + * @param the type of the {@link Value} + * + * @param name the name; must not be {@code null} + * + * @return a suitably-typed {@link Value} indexed under the supplied {@code name}, or {@code null} if no such {@link + * Value} exists + * + * @exception NullPointerException if {@code name} is {@code null} + * + * @exception ClassCastException if {@code } does not match the actual type of the {@link Value} indexed under the + * supplied {@code name} + */ + @SuppressWarnings("unchecked") + public final > T value(final String name) { + return (T)this.values().get(name); + } + + /** + * Returns an {@link Attributes} comprising the supplied arguments. + * + * @param name the name; must not be {@code null} + * + * @return a non-{@code null} {@link Attributes} + * + * @exception NullPointerException if {@code name} is {@code null} + * + * @see #of(String, Map, Map, Map) + */ + public static final Attributes of(final String name) { + return of(name, Map.of(), Map.of(), Map.of()); + } + + /** + * Returns an {@link Attributes} comprising the supplied arguments. + * + * @param name the name; must not be {@code null} + * + * @param valueValue a {@link String} that will be indexed under the key "{@code value}"; must not be {@code null} + * + * @return a non-{@code null} {@link Attributes} + * + * @exception NullPointerException if {@code name} or {@code valueValue} is {@code null} + * + * @see #of(String, Map, Map, Map) + */ + public static final Attributes of(final String name, final String valueValue) { + return of(name, Map.of("value", new StringValue(valueValue)), Map.of(), Map.of()); + } + + /** + * Returns an {@link Attributes} comprising the supplied arguments. + * + * @param name the name; must not be {@code null} + * + * @param attributes an array of {@link Attributes}; may be {@code null} + * + * @return a non-{@code null} {@link Attributes} + * + * @exception NullPointerException if {@code name} is {@code null} + * + * @see #of(String, List) + */ + public static final Attributes of(final String name, final Attributes... attributes) { + return of(name, attributes == null || attributes.length == 0 ? List.of() : Arrays.asList(attributes)); + } + + /** + * Returns an {@link Attributes} comprising the supplied arguments. + * + * @param name the name; must not be {@code null} + * + * @param attributes a non-{@code null} {@link List} of {@link Attributes} + * + * @return a non-{@code null} {@link Attributes} + * + * @exception NullPointerException if any argument is {@code null} + * + * @see #of(String, Map, Map, Map) + */ + public static final Attributes of(final String name, final List attributes) { + return of(name, Map.of(), Map.of(), Map.of(name, attributes)); + } + + /** + * Returns an {@link Attributes} comprising the supplied arguments. + * + * @param name the name; must not be {@code null} + * + * @param values a {@link Map} of {@link Value}s indexed by {@link String} keys; must not be {@code null} + * + * @param notes a {@link Map} of {@link Value}s indexed by {@link String} keys containing descriptive information + * only; must not be {@code null}; not incorporated into equality calculations + * + * @param attributes a {@link Map} of {@link List}s of {@link Attributes} instances denoting metadata for a given + * value in {@code values} (or for this {@link Attributes} as a whole if the key in question is equal to {@code + * name}); must not be {@code null} + * + * @return a non-{@code null} {@link Attributes} + * + * @exception NullPointerException if any argument is {@code null} + */ + public static final Attributes of(final String name, + final Map> values, + final Map> notes, + final Map> attributes) { + return new Attributes(name, values, notes, attributes); + } + +} diff --git a/src/main/java/org/microbean/attributes/BooleanValue.java b/src/main/java/org/microbean/attributes/BooleanValue.java new file mode 100644 index 0000000..628ceef --- /dev/null +++ b/src/main/java/org/microbean/attributes/BooleanValue.java @@ -0,0 +1,104 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2025 microBean™. + * + * Licensed 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.microbean.attributes; + +import java.lang.constant.ClassDesc; +import java.lang.constant.ConstantDesc; +import java.lang.constant.ConstantDescs; +import java.lang.constant.DynamicConstantDesc; +import java.lang.constant.MethodHandleDesc; +import java.lang.constant.MethodTypeDesc; + +import java.util.Optional; + +import static java.lang.constant.ConstantDescs.BSM_INVOKE; +import static java.lang.constant.ConstantDescs.CD_boolean; +import static java.lang.constant.DirectMethodHandleDesc.Kind.STATIC; + +/** + * A {@link Value} whose value is a {@code boolean}. + * + * @param value the value + * + * @author Laird Nelson + */ +public final record BooleanValue(boolean value) implements Value { + + /** + * The {@link BooleanValue} representing {@code true}. + */ + public static final BooleanValue TRUE = new BooleanValue(true); + + /** + * The {@link BooleanValue} representing {@code false}. + */ + public static final BooleanValue FALSE = new BooleanValue(false); + + @Override // Comparable + public final int compareTo(BooleanValue other) { + if (other == null) { + return -1; + } else if (this.equals(other)) { + return 0; + } else if (this.value()) { + return 1; // false comes first + } else { + return -1; + } + } + + @Override // Constable + public final Optional> describeConstable() { + final ClassDesc cd = ClassDesc.of(this.getClass().getName()); + return + Optional.of(DynamicConstantDesc.of(BSM_INVOKE, + MethodHandleDesc.ofMethod(STATIC, + cd, + "of", + MethodTypeDesc.of(cd, + CD_boolean)), + this.value() ? ConstantDescs.TRUE : ConstantDescs.FALSE)); + } + + @Override // Record + public final boolean equals(final Object other) { + return + other == this || + // Follow java.lang.annotation.Annotation requirements + other != null && other.getClass() == this.getClass() && this.value() == ((BooleanValue)other).value(); + } + + @Override // Record + public final int hashCode() { + // Follow java.lang.annotation.Annotation requirements. + return Boolean.valueOf(this.value()).hashCode(); + } + + @Override // Record + public final String toString() { + return String.valueOf(this.value()); + } + + /** + * Returns a {@link BooleanValue} suitable for the supplied arguments. + * + * @param b a {@code boolean} + * + * @return {@link TRUE} if {@code b} is {@code true}; {@link FALSE} otherwise + */ + public static final BooleanValue of(final boolean b) { + return b ? TRUE : FALSE; + } + +} diff --git a/src/main/java/org/microbean/attributes/ByteValue.java b/src/main/java/org/microbean/attributes/ByteValue.java new file mode 100644 index 0000000..ffb61e5 --- /dev/null +++ b/src/main/java/org/microbean/attributes/ByteValue.java @@ -0,0 +1,96 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2025 microBean™. + * + * Licensed 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.microbean.attributes; + +import java.lang.constant.ClassDesc; +import java.lang.constant.DynamicConstantDesc; +import java.lang.constant.MethodHandleDesc; +import java.lang.constant.MethodTypeDesc; + +import java.util.Optional; + +import static java.lang.constant.ConstantDescs.BSM_INVOKE; +import static java.lang.constant.ConstantDescs.CD_Integer; +import static java.lang.constant.ConstantDescs.CD_byte; +import static java.lang.constant.DirectMethodHandleDesc.Kind.STATIC; +import static java.lang.constant.DirectMethodHandleDesc.Kind.VIRTUAL; + +/** + * A {@link Value} whose value is a {@code byte}. + * + * @param value the value + * + * @author Laird Nelson + */ +public final record ByteValue(byte value) implements Value { + + @Override // Comparable + public final int compareTo(ByteValue other) { + if (other == null) { + return -1; + } else if (this.equals(other)) { + return 0; + } + return this.value() > other.value() ? 1 : -1; + } + + @Override // Constable + public final Optional> describeConstable() { + final ClassDesc cd = ClassDesc.of(this.getClass().getName()); + return + Optional.of(DynamicConstantDesc.of(BSM_INVOKE, + MethodHandleDesc.ofMethod(STATIC, + cd, + "of", + MethodTypeDesc.of(cd, + CD_byte)), + DynamicConstantDesc.of(BSM_INVOKE, + MethodHandleDesc.ofMethod(VIRTUAL, + CD_Integer, + "byteValue", + MethodTypeDesc.of(CD_byte)), + Integer.valueOf(this.value())))); + } + + @Override // Record + public final boolean equals(final Object other) { + return + other == this || + // Follow java.lang.annotation.Annotation requirements. + other != null && other.getClass() == this.getClass() && this.value() == ((ByteValue)other).value(); + } + + @Override // Record + public final int hashCode() { + // Follow java.lang.annotation.Annotation requirements. + return Byte.valueOf(this.value()).hashCode(); + } + + @Override // Record + public final String toString() { + return String.valueOf(this.value()); + } + + /** + * Returns a {@link ByteValue} suitable for the supplied arguments. + * + * @param b a {@code byte} + * + * @return a non-{@code null} {@link ByteValue} + */ + public static final ByteValue of(final byte b) { + return new ByteValue(b); + } + +} diff --git a/src/main/java/org/microbean/attributes/CharValue.java b/src/main/java/org/microbean/attributes/CharValue.java new file mode 100644 index 0000000..cc74ec3 --- /dev/null +++ b/src/main/java/org/microbean/attributes/CharValue.java @@ -0,0 +1,99 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2025 microBean™. + * + * Licensed 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.microbean.attributes; + +import java.lang.constant.ClassDesc; +import java.lang.constant.DynamicConstantDesc; +import java.lang.constant.MethodHandleDesc; +import java.lang.constant.MethodTypeDesc; + +import java.util.Optional; + +import static java.lang.constant.ConstantDescs.BSM_INVOKE; +import static java.lang.constant.ConstantDescs.CD_String; +import static java.lang.constant.ConstantDescs.CD_char; +import static java.lang.constant.ConstantDescs.CD_int; +import static java.lang.constant.DirectMethodHandleDesc.Kind.STATIC; +import static java.lang.constant.DirectMethodHandleDesc.Kind.VIRTUAL; + +/** + * A {@link Value} whose value is a {@code char}. + * + * @param value the value + * + * @author Laird Nelson + */ +public final record CharValue(char value) implements Value { + + @Override // Comparable + public final int compareTo(final CharValue other) { + if (other == null) { + return -1; + } else if (this.equals(other)) { + return 0; + } + return this.value() > other.value() ? 1 : -1; + } + + @Override // Constable + public final Optional> describeConstable() { + final ClassDesc cd = ClassDesc.of(this.getClass().getName()); + return + Optional.of(DynamicConstantDesc.of(BSM_INVOKE, + MethodHandleDesc.ofMethod(STATIC, + cd, + "of", + MethodTypeDesc.of(cd, + CD_char)), + DynamicConstantDesc.of(BSM_INVOKE, + MethodHandleDesc.ofMethod(VIRTUAL, + CD_String, + "charAt", + MethodTypeDesc.of(CD_char, + CD_int)), + String.valueOf(this.value()), + Integer.valueOf(0)))); + } + + @Override // Record + public final boolean equals(final Object other) { + return + other == this || + // Follow java.lang.annotation.Annotation requirements. + other != null && other.getClass() == this.getClass() && this.value() == ((CharValue)other).value(); + } + + @Override // Record + public final int hashCode() { + // Follow java.lang.annotation.Annotation requirements. + return Character.valueOf(this.value()).hashCode(); + } + + @Override // Record + public final String toString() { + return String.valueOf(this.value()); + } + + /** + * Returns a {@link CharValue} suitable for the supplied arguments. + * + * @param c a {@code char} + * + * @return a non-{@code null} {@link CharValue} + */ + public static final CharValue of(final char c) { + return new CharValue(c); + } + +} diff --git a/src/main/java/org/microbean/attributes/ClassValue.java b/src/main/java/org/microbean/attributes/ClassValue.java new file mode 100644 index 0000000..26d4cb3 --- /dev/null +++ b/src/main/java/org/microbean/attributes/ClassValue.java @@ -0,0 +1,110 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2025 microBean™. + * + * Licensed 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.microbean.attributes; + +import java.lang.constant.ClassDesc; +import java.lang.constant.DynamicConstantDesc; +import java.lang.constant.MethodHandleDesc; +import java.lang.constant.MethodTypeDesc; + +import java.util.Objects; +import java.util.Optional; + +import static java.lang.constant.ConstantDescs.BSM_INVOKE; +import static java.lang.constant.ConstantDescs.CD_Class; +import static java.lang.constant.DirectMethodHandleDesc.Kind.STATIC; + +/** + * A {@link Value} whose value is a {@code Class}. + * + * @param value the value + * + * @author Laird Nelson + */ +public final record ClassValue(Class value) implements Value { + + public static final ClassValue CLASS_JAVA_LANG_OBJECT = new ClassValue(Object.class); + + public static final ClassValue CLASS_JAVA_LANG_STRING = new ClassValue(String.class); + + /** + * Creates a new {@link ClassValue}. + * + * @param value the value; must not be {@code null} + * + * @exception NullPointerException if {@code value} is {@code null} + */ + public ClassValue { + Objects.requireNonNull(value, "value"); + } + + @Override // Comparable + public final int compareTo(final ClassValue other) { + if (other == null) { + return -1; + } else if (this.equals(other)) { + return 0; + } + return this.value().getName().compareTo(other.value().getName()); + } + + @Override // Constable + public final Optional> describeConstable() { + final ClassDesc cd = ClassDesc.of(this.getClass().getName()); + return + Optional.of(DynamicConstantDesc.of(BSM_INVOKE, + MethodHandleDesc.ofMethod(STATIC, + cd, + "of", + MethodTypeDesc.of(cd, + CD_Class)), + this.value().describeConstable().orElseThrow())); + } + + @Override // Record + public final boolean equals(final Object other) { + return + other == this || + // Follow java.lang.annotation.Annotation requirements. + other != null && other.getClass() == this.getClass() && this.value().equals(((ClassValue)other).value()); + } + + @Override // Record + public final int hashCode() { + // Follow java.lang.annotation.Annotation requirements. + return this.value().hashCode(); + } + + @Override // Record + public final String toString() { + return this.value().getName(); // binary name + } + + /** + * Returns a {@link ClassValue} suitable for the supplied arguments. + * + * @param c a {@code Class}; must not be {@code null} + * + * @return a non-{@code null} {@link ClassValue} + * + * @exception NullPointerException if {@code c} is {@code null} + */ + public static final ClassValue of(final Class c) { + return + c == Object.class ? CLASS_JAVA_LANG_OBJECT : + c == String.class ? CLASS_JAVA_LANG_STRING : + new ClassValue(c); + } + +} diff --git a/src/main/java/org/microbean/attributes/DoubleValue.java b/src/main/java/org/microbean/attributes/DoubleValue.java new file mode 100644 index 0000000..eaa0a62 --- /dev/null +++ b/src/main/java/org/microbean/attributes/DoubleValue.java @@ -0,0 +1,90 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2025 microBean™. + * + * Licensed 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.microbean.attributes; + +import java.lang.constant.ClassDesc; +import java.lang.constant.ConstantDesc; +import java.lang.constant.DynamicConstantDesc; +import java.lang.constant.MethodHandleDesc; +import java.lang.constant.MethodTypeDesc; + +import java.util.Optional; + +import static java.lang.constant.ConstantDescs.BSM_INVOKE; +import static java.lang.constant.ConstantDescs.CD_double; +import static java.lang.constant.DirectMethodHandleDesc.Kind.STATIC; + +/** + * A {@link Value} whose value is a {@code double}. + * + * @param value the value + * + * @author Laird Nelson + */ +public final record DoubleValue(double value) implements Value { + + @Override // Comparable + public final int compareTo(final DoubleValue other) { + if (other == null) { + return -1; + } else if (this.equals(other)) { + return 0; + } + return this.value() > other.value() ? 1 : -1; + } + + @Override // Constable + public final Optional> describeConstable() { + final ClassDesc cd = ClassDesc.of(this.getClass().getName()); + return + Optional.of(DynamicConstantDesc.of(BSM_INVOKE, + MethodHandleDesc.ofMethod(STATIC, + cd, + "of", + MethodTypeDesc.of(cd, + CD_double)), + Double.valueOf(this.value()))); + } + + @Override // Record + public final boolean equals(final Object other) { + return + other == this || + // Follow java.lang.annotation.Annotation requirements. + other != null && other.getClass() == this.getClass() && Double.valueOf(this.value()).equals(Double.valueOf(((DoubleValue)other).value())); + } + + @Override // Record + public final int hashCode() { + // Follow java.lang.annotation.Annotation requirements. + return Double.valueOf(this.value()).hashCode(); + } + + @Override // Record + public final String toString() { + return String.valueOf(this.value()); + } + + /** + * Returns a {@link DoubleValue} suitable for the supplied arguments. + * + * @param d a {@code double} + * + * @return a non-{@code null} {@link DoubleValue} + */ + public static final DoubleValue of(final double d) { + return new DoubleValue(d); + } + +} diff --git a/src/main/java/org/microbean/attributes/EnumValue.java b/src/main/java/org/microbean/attributes/EnumValue.java new file mode 100644 index 0000000..b9e6c54 --- /dev/null +++ b/src/main/java/org/microbean/attributes/EnumValue.java @@ -0,0 +1,105 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2025 microBean™. + * + * Licensed 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.microbean.attributes; + +import java.lang.constant.ClassDesc; +import java.lang.constant.DynamicConstantDesc; +import java.lang.constant.MethodHandleDesc; +import java.lang.constant.MethodTypeDesc; + +import java.util.Objects; +import java.util.Optional; + +import static java.lang.constant.ConstantDescs.BSM_INVOKE; +import static java.lang.constant.ConstantDescs.CD_Enum; +import static java.lang.constant.DirectMethodHandleDesc.Kind.STATIC; + +/** + * A {@link Value} whose value is an {@code Enum}. + * + * @param the type of the {@link Enum} + * + * @param value the value + * + * @author Laird Nelson + */ +public final record EnumValue>(E value) implements Value> { + + /** + * Creates a new {@link EnumValue}. + * + * @param value the value; must not be {@code null} + * + * @exception NullPointerException if {@code value} is {@code null} + */ + public EnumValue { + Objects.requireNonNull(value, "value"); + } + + @Override // Comparable + public final int compareTo(final EnumValue other) { + return + other == null ? -1 : + this.equals(other) ? 0 : + this.value().compareTo(other.value()); + } + + @Override // Constable + public final Optional>> describeConstable() { + final ClassDesc cd = ClassDesc.of(this.getClass().getName()); + return + Optional.of(DynamicConstantDesc.of(BSM_INVOKE, + MethodHandleDesc.ofMethod(STATIC, + cd, + "of", + MethodTypeDesc.of(cd, + CD_Enum)), + this.value().describeConstable().orElseThrow())); + } + + @Override // Record + public final boolean equals(final Object other) { + return + other == this || + // Follow java.lang.annotation.Annotation requirements. + other != null && other.getClass() == this.getClass() && this.value().equals(((EnumValue)other).value()); + } + + @Override // Record + public final int hashCode() { + // Follow java.lang.annotation.Annotation requirements. + return this.value().hashCode(); + } + + @Override // Record + public final String toString() { + return this.value().toString(); + } + + /** + * Returns an {@link EnumValue} suitable for the supplied arguments. + * + * @param the type of the {@link Enum} + * + * @param e a non-{@code null} {@code Enum} + * + * @return a non-{@code null} {@link EnumValue} + * + * @exception NullPointerException if {@code e} is {@code null} + */ + public static final > EnumValue of(final E e) { + return new EnumValue<>(e); + } + +} diff --git a/src/main/java/org/microbean/attributes/FloatValue.java b/src/main/java/org/microbean/attributes/FloatValue.java new file mode 100644 index 0000000..777120e --- /dev/null +++ b/src/main/java/org/microbean/attributes/FloatValue.java @@ -0,0 +1,89 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2025 microBean™. + * + * Licensed 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.microbean.attributes; + +import java.lang.constant.ClassDesc; +import java.lang.constant.DynamicConstantDesc; +import java.lang.constant.MethodHandleDesc; +import java.lang.constant.MethodTypeDesc; + +import java.util.Optional; + +import static java.lang.constant.ConstantDescs.BSM_INVOKE; +import static java.lang.constant.ConstantDescs.CD_float; +import static java.lang.constant.DirectMethodHandleDesc.Kind.STATIC; + +/** + * A {@link Value} whose value is a {@code float}. + * + * @param value the value + * + * @author Laird Nelson + */ +public final record FloatValue(float value) implements Value { + + @Override // Comparable + public final int compareTo(final FloatValue other) { + if (other == null) { + return -1; + } else if (this.equals(other)) { + return 0; + } + return this.value() > other.value() ? 1 : -1; + } + + @Override // Constable + public final Optional> describeConstable() { + final ClassDesc cd = ClassDesc.of(this.getClass().getName()); + return + Optional.of(DynamicConstantDesc.of(BSM_INVOKE, + MethodHandleDesc.ofMethod(STATIC, + cd, + "of", + MethodTypeDesc.of(cd, + CD_float)), + Float.valueOf(this.value()))); + } + + @Override // Record + public final boolean equals(final Object other) { + return + other == this || + // Follow java.lang.annotation.Annotation requirements. + other != null && other.getClass() == this.getClass() && Float.valueOf(this.value()).equals(Float.valueOf(((FloatValue)other).value())); + } + + @Override // Record + public final int hashCode() { + // Follow java.lang.annotation.Annotation requirements. + return Float.valueOf(this.value()).hashCode(); + } + + @Override // Record + public final String toString() { + return String.valueOf(this.value()); + } + + /** + * Returns a {@link FloatValue} suitable for the supplied arguments. + * + * @param f a {@code float} + * + * @return a non-{@code null} {@link FloatValue} + */ + public static final FloatValue of(final float f) { + return new FloatValue(f); + } + +} diff --git a/src/main/java/org/microbean/attributes/IntValue.java b/src/main/java/org/microbean/attributes/IntValue.java new file mode 100644 index 0000000..e84bf54 --- /dev/null +++ b/src/main/java/org/microbean/attributes/IntValue.java @@ -0,0 +1,100 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2025 microBean™. + * + * Licensed 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.microbean.attributes; + +import java.lang.constant.ClassDesc; +import java.lang.constant.DynamicConstantDesc; +import java.lang.constant.MethodHandleDesc; +import java.lang.constant.MethodTypeDesc; + +import java.util.Optional; + +import static java.lang.constant.ConstantDescs.BSM_INVOKE; +import static java.lang.constant.ConstantDescs.CD_int; +import static java.lang.constant.DirectMethodHandleDesc.Kind.STATIC; + +/** + * A {@link Value} whose value is an {@code int}. + * + * @param value the value + * + * @author Laird Nelson + */ +public final record IntValue(int value) implements Value { + + public static final IntValue NEGATIVE_ONE = new IntValue(-1); + + public static final IntValue ZERO = new IntValue(0); + + public static final IntValue ONE = new IntValue(1); + + @Override // Comparable + public final int compareTo(final IntValue other) { + if (other == null) { + return -1; + } else if (this.equals(other)) { + return 0; + } + return this.value() > other.value() ? 1 : -1; + } + + @Override // Constable + public final Optional> describeConstable() { + final ClassDesc cd = ClassDesc.of(this.getClass().getName()); + return + Optional.of(DynamicConstantDesc.of(BSM_INVOKE, + MethodHandleDesc.ofMethod(STATIC, + cd, + "of", + MethodTypeDesc.of(cd, + CD_int)), + Integer.valueOf(this.value()))); + } + + @Override // Record + public final boolean equals(final Object other) { + return + other == this || + // Follow java.lang.annotation.Annotation requirements. + other != null && other.getClass() == this.getClass() && this.value() == ((IntValue)other).value(); + } + + @Override // Record + public final int hashCode() { + // Follow java.lang.annotation.Annotation requirements. + return Integer.valueOf(this.value()).hashCode(); + } + + @Override // Record + public final String toString() { + return String.valueOf(this.value()); + } + + /** + * Returns an {@link IntValue} suitable for the supplied arguments. + * + * @param i am {@code int} + * + * @return a non-{@code null} {@link IntValue} + */ + public static final IntValue of(final int i) { + return switch (i) { + case -1 -> NEGATIVE_ONE; + case 0 -> ZERO; + case 1 -> ONE; + default -> new IntValue(i); + }; + } + +} diff --git a/src/main/java/org/microbean/attributes/LongValue.java b/src/main/java/org/microbean/attributes/LongValue.java new file mode 100644 index 0000000..ac17f3f --- /dev/null +++ b/src/main/java/org/microbean/attributes/LongValue.java @@ -0,0 +1,89 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2025 microBean™. + * + * Licensed 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.microbean.attributes; + +import java.lang.constant.ClassDesc; +import java.lang.constant.DynamicConstantDesc; +import java.lang.constant.MethodHandleDesc; +import java.lang.constant.MethodTypeDesc; + +import java.util.Optional; + +import static java.lang.constant.ConstantDescs.BSM_INVOKE; +import static java.lang.constant.ConstantDescs.CD_long; +import static java.lang.constant.DirectMethodHandleDesc.Kind.STATIC; + +/** + * A {@link Value} whose value is a {@code long}. + * + * @param value the value + * + * @author Laird Nelson + */ +public final record LongValue(long value) implements Value { + + @Override // Comparable + public final int compareTo(final LongValue other) { + if (other == null) { + return -1; + } else if (this.equals(other)) { + return 0; + } + return this.value() > other.value() ? 1 : -1; + } + + @Override // Constable + public final Optional> describeConstable() { + final ClassDesc cd = ClassDesc.of(this.getClass().getName()); + return + Optional.of(DynamicConstantDesc.of(BSM_INVOKE, + MethodHandleDesc.ofMethod(STATIC, + cd, + "of", + MethodTypeDesc.of(cd, + CD_long)), + Long.valueOf(this.value()))); + } + + @Override // Record + public final boolean equals(final Object other) { + return + other == this || + // Follow java.lang.annotation.Annotation requirements. + other != null && other.getClass() == this.getClass() && this.value() == ((LongValue)other).value(); + } + + @Override // Record + public final int hashCode() { + // Follow java.lang.annotation.Annotation requirements. + return Long.valueOf(this.value()).hashCode(); + } + + @Override // Record + public final String toString() { + return String.valueOf(this.value()); + } + + /** + * Returns a {@link LongValue} suitable for the supplied arguments. + * + * @param l a {@code long} + * + * @return a non-{@code null} {@link LongValue} + */ + public static final LongValue of(final long l) { + return new LongValue(l); + } + +} diff --git a/src/main/java/org/microbean/attributes/ShortValue.java b/src/main/java/org/microbean/attributes/ShortValue.java new file mode 100644 index 0000000..5dbd8c8 --- /dev/null +++ b/src/main/java/org/microbean/attributes/ShortValue.java @@ -0,0 +1,96 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2025 microBean™. + * + * Licensed 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.microbean.attributes; + +import java.lang.constant.ClassDesc; +import java.lang.constant.DynamicConstantDesc; +import java.lang.constant.MethodHandleDesc; +import java.lang.constant.MethodTypeDesc; + +import java.util.Optional; + +import static java.lang.constant.ConstantDescs.BSM_INVOKE; +import static java.lang.constant.ConstantDescs.CD_Integer; +import static java.lang.constant.ConstantDescs.CD_short; +import static java.lang.constant.DirectMethodHandleDesc.Kind.STATIC; +import static java.lang.constant.DirectMethodHandleDesc.Kind.VIRTUAL; + +/** + * A {@link Value} whose value is a {@code short}. + * + * @param value the value + * + * @author Laird Nelson + */ +public final record ShortValue(short value) implements Value { + + @Override // Comparable + public final int compareTo(final ShortValue other) { + if (other == null) { + return -1; + } else if (this.equals(other)) { + return 0; + } + return this.value() > other.value() ? 1 : -1; + } + + @Override // Constable + public final Optional> describeConstable() { + final ClassDesc cd = ClassDesc.of(this.getClass().getName()); + return + Optional.of(DynamicConstantDesc.of(BSM_INVOKE, + MethodHandleDesc.ofMethod(STATIC, + cd, + "of", + MethodTypeDesc.of(cd, + CD_short)), + DynamicConstantDesc.of(BSM_INVOKE, + MethodHandleDesc.ofMethod(VIRTUAL, + CD_Integer, + "shortValue", + MethodTypeDesc.of(CD_short)), + Integer.valueOf(this.value())))); + } + + @Override // Record + public final boolean equals(final Object other) { + return + other == this || + // Follow java.lang.annotation.Annotation requirements. + other != null && other.getClass() == this.getClass() && this.value() == ((ShortValue)other).value(); + } + + @Override // Record + public final int hashCode() { + // Follow java.lang.annotation.Annotation requirements. + return Short.valueOf(this.value()).hashCode(); + } + + @Override // Record + public final String toString() { + return String.valueOf(this.value()); + } + + /** + * Returns a {@link ShortValue} suitable for the supplied arguments. + * + * @param s a {@code short} + * + * @return a non-{@code null} {@link ShortValue} + */ + public static final ShortValue of(final short s) { + return new ShortValue(s); + } + +} diff --git a/src/main/java/org/microbean/attributes/StringValue.java b/src/main/java/org/microbean/attributes/StringValue.java new file mode 100644 index 0000000..adcc6cb --- /dev/null +++ b/src/main/java/org/microbean/attributes/StringValue.java @@ -0,0 +1,104 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2025 microBean™. + * + * Licensed 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.microbean.attributes; + +import java.lang.constant.ClassDesc; +import java.lang.constant.DynamicConstantDesc; +import java.lang.constant.MethodHandleDesc; +import java.lang.constant.MethodTypeDesc; + +import java.util.Objects; +import java.util.Optional; + +import static java.lang.constant.ConstantDescs.BSM_INVOKE; +import static java.lang.constant.ConstantDescs.CD_String; +import static java.lang.constant.DirectMethodHandleDesc.Kind.STATIC; + +/** + * A {@link Value} whose value is a {@code String}. + * + * @param value the value + * + * @author Laird Nelson + */ +public final record StringValue(String value) implements Value { + + public static final StringValue EMPTY = new StringValue(""); + + /** + * Creates a new {@link StringValue}. + * + * @param value the value; must not be {@code null} + * + * @exception NullPointerException if {@code value} is {@code null} + */ + public StringValue { + Objects.requireNonNull(value, "value"); + } + + @Override // Comparable + public final int compareTo(final StringValue other) { + if (other == null) { + return -1; + } else if (this.equals(other)) { + return 0; + } + return this.value().compareTo(other.value()); + } + + @Override // Constable + public final Optional> describeConstable() { + final ClassDesc cd = ClassDesc.of(this.getClass().getName()); + return + Optional.of(DynamicConstantDesc.of(BSM_INVOKE, + MethodHandleDesc.ofMethod(STATIC, + cd, + "of", + MethodTypeDesc.of(cd, + CD_String)), + this.value())); + } + + @Override + public final boolean equals(final Object other) { + return + other == this || + // Follow java.lang.annotation.Annotation requirements. + other != null && other.getClass() == this.getClass() && this.value().equals(((StringValue)other).value()); + } + + public final int hashCode() { + // Follow java.lang.annotation.Annotation requirements. + return this.value().hashCode(); + } + + @Override // Record + public final String toString() { + return this.value(); + } + + /** + * Returns a {@link StringValue} suitable for the supplied arguments. + * + * @param s a non-{@code null} {@code String} + * + * @return a non-{@code null} {@link StringValue} + * + * @exception NullPointerException if {@code s} is {@code null} + */ + public static final StringValue of(final String s) { + return s.isEmpty() ? EMPTY : new StringValue(s); + } + +} diff --git a/src/main/java/org/microbean/attributes/Value.java b/src/main/java/org/microbean/attributes/Value.java new file mode 100644 index 0000000..f7645c0 --- /dev/null +++ b/src/main/java/org/microbean/attributes/Value.java @@ -0,0 +1,70 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2025 microBean™. + * + * Licensed 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.microbean.attributes; + +import java.lang.constant.Constable; + +/** + * A value of a particular type suitable for representing as an annotation value or similar. + * + * @param a {@link Value} subtype + * + *

{@link Value}s are value-based + * classes.

+ * + * @author Laird Nelson + * + * @see ArrayValue + * + * @see Attributes + * + * @see BooleanValue + * + * @see ByteValue + * + * @see CharValue + * + * @see ClassValue + * + * @see DoubleValue + * + * @see EnumValue + * + * @see FloatValue + * + * @see IntValue + * + * @see LongValue + * + * @see ShortValue + * + * @see StringValue + */ +public sealed interface Value> extends Comparable, Constable + permits ArrayValue, + Attributes, + BooleanValue, + ByteValue, + CharValue, + ClassValue, + DoubleValue, + EnumValue, + FloatValue, + IntValue, + LongValue, + ShortValue, + StringValue { + +} diff --git a/src/main/java/org/microbean/attributes/package-info.java b/src/main/java/org/microbean/attributes/package-info.java new file mode 100644 index 0000000..281cb9b --- /dev/null +++ b/src/main/java/org/microbean/attributes/package-info.java @@ -0,0 +1,20 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2025 microBean™. + * + * Licensed 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. + */ + +/** + * Provides classes and interfaces related to representing annotations. + * + * @author Laird Nelson + */ +package org.microbean.attributes; diff --git a/src/site/markdown/index.md.vm b/src/site/markdown/index.md.vm new file mode 100644 index 0000000..debee59 --- /dev/null +++ b/src/site/markdown/index.md.vm @@ -0,0 +1,2 @@ +#include("../../../README.md") + diff --git a/src/site/site.xml b/src/site/site.xml new file mode 100644 index 0000000..fb69fe9 --- /dev/null +++ b/src/site/site.xml @@ -0,0 +1,32 @@ + + + + + + + + + org.apache.maven.skins + maven-fluido-skin + 2.0.0 + + + + + + + + + + + + true + false + + + diff --git a/src/test/java/org/microbean/attributes/TestAttributes.java b/src/test/java/org/microbean/attributes/TestAttributes.java new file mode 100644 index 0000000..df3ceb0 --- /dev/null +++ b/src/test/java/org/microbean/attributes/TestAttributes.java @@ -0,0 +1,41 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2025 microBean™. + * + * Licensed 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.microbean.attributes; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +final class TestAttributes { + + private TestAttributes() { + super(); + } + + @Test + final void testAttributes() { + final Attributes documented = Attributes.of("Documented"); + final Attributes qualifier = Attributes.of("Qualifier", documented); + final Attributes named = Attributes.of("Named", qualifier); + assertFalse(named.isa(named)); + assertTrue(named.isa(qualifier)); + assertTrue(qualifier.isa(documented)); + assertTrue(named.isa(documented)); + } + +} diff --git a/src/test/java/org/microbean/attributes/TestConstableSemantics.java b/src/test/java/org/microbean/attributes/TestConstableSemantics.java new file mode 100644 index 0000000..5529a63 --- /dev/null +++ b/src/test/java/org/microbean/attributes/TestConstableSemantics.java @@ -0,0 +1,141 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2025 microBean™. + * + * Licensed 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.microbean.attributes; + +import java.lang.annotation.RetentionPolicy; + +import java.util.Arrays; + +import org.junit.jupiter.api.Test; + +import static java.lang.invoke.MethodHandles.lookup; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +final class TestConstableSemantics { + + private TestConstableSemantics() { + super(); + } + + @Test + final void testArrayValue() throws ReflectiveOperationException { + final ArrayValue v = ArrayValue.of("a", "b", "c"); + final ArrayValue v0 = v.describeConstable().orElseThrow().resolveConstantDesc(lookup()); + assertEquals(v, v0); + assertNotSame(v, v0); + } + + @Test + final void testAttributes() throws ReflectiveOperationException { + final Attributes v = Attributes.of("Qualifier"); + final Attributes v0 = v.describeConstable().orElseThrow().resolveConstantDesc(lookup()); + assertEquals(v, v0); + assertNotSame(v, v0); + } + + @Test + final void testBooleanValue() throws ReflectiveOperationException { + final BooleanValue v = BooleanValue.of(false); + final BooleanValue v0 = v.describeConstable().orElseThrow().resolveConstantDesc(lookup()); + assertEquals(v, v0); + assertSame(v, v0); // note + } + + @Test + final void testByteValue() throws ReflectiveOperationException { + final ByteValue v = ByteValue.of((byte)1); + final ByteValue v0 = v.describeConstable().orElseThrow().resolveConstantDesc(lookup()); + assertEquals(v, v0); + assertNotSame(v, v0); + } + + @Test + final void testCharValue() throws ReflectiveOperationException { + final CharValue v = CharValue.of('a'); + final CharValue v0 = v.describeConstable().orElseThrow().resolveConstantDesc(lookup()); + assertEquals(v, v0); + assertNotSame(v, v0); + } + + @Test + final void testClassValue() throws ReflectiveOperationException { + final ClassValue v = ClassValue.of(Long.class); + final ClassValue v0 = v.describeConstable().orElseThrow().resolveConstantDesc(lookup()); + assertEquals(v, v0); + assertNotSame(v, v0); + } + + + @Test + final void testDoubleValue() throws ReflectiveOperationException { + final DoubleValue v = DoubleValue.of(2D); + final DoubleValue v0 = v.describeConstable().orElseThrow().resolveConstantDesc(lookup()); + assertEquals(v, v0); + assertNotSame(v, v0); + } + + @Test + final void testEnumValue() throws ReflectiveOperationException { + final EnumValue v = EnumValue.of(RetentionPolicy.RUNTIME); + final EnumValue v0 = v.describeConstable().orElseThrow().resolveConstantDesc(lookup()); + assertEquals(v, v0); + assertNotSame(v, v0); + } + + @Test + final void testFloatValue() throws ReflectiveOperationException { + final FloatValue v = FloatValue.of(2F); + final FloatValue v0 = v.describeConstable().orElseThrow().resolveConstantDesc(lookup()); + assertEquals(v, v0); + assertNotSame(v, v0); + } + + @Test + final void testIntValue() throws ReflectiveOperationException { + final IntValue v = IntValue.of(2); + final IntValue v0 = v.describeConstable().orElseThrow().resolveConstantDesc(lookup()); + assertEquals(v, v0); + assertNotSame(v, v0); + } + + + @Test + final void testLongValue() throws ReflectiveOperationException { + final LongValue v = LongValue.of(2L); + final LongValue v0 = v.describeConstable().orElseThrow().resolveConstantDesc(lookup()); + assertEquals(v, v0); + assertNotSame(v, v0); + } + + @Test + final void testShortValue() throws ReflectiveOperationException { + final ShortValue v = ShortValue.of((short)2); + final ShortValue v0 = v.describeConstable().orElseThrow().resolveConstantDesc(lookup()); + assertEquals(v, v0); + assertNotSame(v, v0); + } + + @Test + final void testStringValue() throws ReflectiveOperationException { + final StringValue v = StringValue.of("a"); + final StringValue v0 = v.describeConstable().orElseThrow().resolveConstantDesc(lookup()); + assertEquals(v, v0); + assertNotSame(v, v0); + } + +}