diff options
Diffstat (limited to 'jgvariant-core/src/main/java')
5 files changed, 904 insertions, 0 deletions
diff --git a/jgvariant-core/src/main/java/eu/mulk/jgvariant/core/Decoder.java b/jgvariant-core/src/main/java/eu/mulk/jgvariant/core/Decoder.java new file mode 100644 index 0000000..d2f2403 --- /dev/null +++ b/jgvariant-core/src/main/java/eu/mulk/jgvariant/core/Decoder.java @@ -0,0 +1,652 @@ +package eu.mulk.jgvariant.core; + +import static java.nio.ByteOrder.LITTLE_ENDIAN; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.RecordComponent; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.Charset; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; +import org.jetbrains.annotations.Nullable; + +/** + * Type class for decodable types. + * + * <p>Use the {@code of*} family of constructor methods to acquire a suitable {@link Decoder} for + * the type you wish to decode. + * + * <p><strong>Example</strong> + * + * <p>To parse a GVariant of type {@code "a(si)"}, which is an array of pairs of {@link String} and + * {@code int}, you can use the following code: + * + * <pre>{@code + * record ExampleRecord(String s, int i) {} + * + * var decoder = + * Decoder.ofArray( + * Decoder.ofStructure( + * ExampleRecord.class, + * Decoder.ofString(UTF_8), + * Decoder.ofInt().withByteOrder(LITTLE_ENDIAN))); + * + * byte[] bytes = ...; + * List<ExampleRecord> example = decoder.decode(ByteBuffer.wrap(bytes)); + * }</pre> + * + * @param <T> the type that the {@link Decoder} can decode. + */ +@SuppressWarnings("java:S1610") +@API(status = Status.EXPERIMENTAL) +public abstract class Decoder<T> { + + private Decoder() {} + + /** + * Decodes a {@link ByteBuffer} holding a serialized GVariant into a value of type {@code T}. + * + * <p><strong>Note:</strong> Due to the way the GVariant serialization format works, it is + * important that the start and end boundaries of the passed byte slice correspond to the actual + * start and end of the serialized value. The format does generally not allow for the dynamic + * discovery of the end of the data structure. + * + * @param byteSlice a byte slice holding a serialized GVariant. + * @return the deserialized value. + * @throws java.nio.BufferUnderflowException if the byte buffer is shorter than the requested + * data. + * @throws IllegalArgumentException if the serialized GVariant is ill-formed + */ + public abstract T decode(ByteBuffer byteSlice); + + abstract byte alignment(); + + @Nullable + abstract Integer fixedSize(); + + final boolean hasFixedSize() { + return fixedSize() != null; + } + + /** + * Switches the input {@link ByteBuffer} to a given {@link ByteOrder} before reading from it. + * + * @param byteOrder the byte order to use. + * @return a new, decorated {@link Decoder}. + */ + public Decoder<T> withByteOrder(ByteOrder byteOrder) { + var delegate = this; + + return new Decoder<>() { + @Override + public byte alignment() { + return delegate.alignment(); + } + + @Override + public @Nullable Integer fixedSize() { + return delegate.fixedSize(); + } + + @Override + public T decode(ByteBuffer byteSlice) { + byteSlice.order(byteOrder); + return delegate.decode(byteSlice); + } + }; + } + + /** + * Creates a {@link Decoder} for an {@code Array} type. + * + * @param elementDecoder a {@link Decoder} for the elements of the array. + * @param <U> the element type. + * @return a new {@link Decoder}. + */ + public static <U> Decoder<List<U>> ofArray(Decoder<U> elementDecoder) { + return new ArrayDecoder<>(elementDecoder); + } + + /** + * Creates a {@link Decoder} for a {@code Maybe} type. + * + * @param elementDecoder a {@link Decoder} for the contained element. + * @param <U> the element type. + * @return a new {@link Decoder}. + */ + public static <U> Decoder<Optional<U>> ofMaybe(Decoder<U> elementDecoder) { + return new MaybeDecoder<>(elementDecoder); + } + + /** + * Creates a {@link Decoder} for a {@code Structure} type, decoding into a {@link Record}. + * + * @param recordType the {@link Record} type that represents the components of the structure. + * @param componentDecoders a {@link Decoder} for each component of the structure. + * @param <U> the {@link Record} type that represents the components of the structure. + * @return a new {@link Decoder}. + */ + public static <U extends Record> Decoder<U> ofStructure( + Class<U> recordType, Decoder<?>... componentDecoders) { + return new StructureDecoder<>(recordType, componentDecoders); + } + + /** + * Creates a {@link Decoder} for a {@code Structure} type, decoding into a {@link List}. + * + * <p>Prefer {@link #ofStructure(Class, Decoder[])} if possible, which is both more type-safe and + * more convenient. + * + * @param componentDecoders a {@link Decoder} for each component of the structure. + * @return a new {@link Decoder}. + */ + public static Decoder<Object[]> ofStructure(Decoder<?>... componentDecoders) { + return new TupleDecoder(componentDecoders); + } + + /** + * Creates a {@link Decoder} for the {@link Variant} type. + * + * <p>The contained {@link Object} can be of one of the following types: + * + * <ul> + * <li>{@link Boolean} + * <li>{@link Byte} + * <li>{@link Short} + * <li>{@link Integer} + * <li>{@link Long} + * <li>{@link String} + * <li>{@link Optional} (a GVariant {@code Maybe} type) + * <li>{@link List} (a GVariant array) + * <li>{@code Object[]} (a GVariant structure) + * <li>{@link Variant} (a nested variant) + * </ul> + * + * @return a new {@link Decoder}. + */ + public static Decoder<Variant> ofVariant() { + return new VariantDecoder(); + } + + /** + * Creates a {@link Decoder} for the {@code boolean} type. + * + * @return a new {@link Decoder}. + */ + public static Decoder<Boolean> ofBoolean() { + return new BooleanDecoder(); + } + + /** + * Creates a {@link Decoder} for the 8-bit {@code byte} type. + * + * <p><strong>Note:</strong> It is often useful to apply {@link #withByteOrder(ByteOrder)} to the + * result of this method. + * + * @return a new {@link Decoder}. + */ + public static Decoder<Byte> ofByte() { + return new ByteDecoder(); + } + + /** + * Creates a {@link Decoder} for the 16-bit {@code short} type. + * + * <p><strong>Note:</strong> It is often useful to apply {@link #withByteOrder(ByteOrder)} to the + * result of this method. + * + * @return a new {@link Decoder}. + */ + public static Decoder<Short> ofShort() { + return new ShortDecoder(); + } + + /** + * Creates a {@link Decoder} for the 32-bit {@code int} type. + * + * <p><strong>Note:</strong> It is often useful to apply {@link #withByteOrder(ByteOrder)} to the + * result of this method. + * + * @return a new {@link Decoder}. + */ + public static Decoder<Integer> ofInt() { + return new IntegerDecoder(); + } + + /** + * Creates a {@link Decoder} for the 64-bit {@code long} type. + * + * <p><strong>Note:</strong> It is often useful to apply {@link #withByteOrder(ByteOrder)} to the + * result of this method. + * + * @return a new {@link Decoder}. + */ + public static Decoder<Long> ofLong() { + return new LongDecoder(); + } + + /** + * Creates a {@link Decoder} for the {@code double} type. + * + * @return a new {@link Decoder}. + */ + public static Decoder<Double> ofDouble() { + return new DoubleDecoder(); + } + + /** + * Creates a {@link Decoder} for the {@link String} type. + * + * <p><strong>Note:</strong> While GVariant does not prescribe any particular encoding, {@link + * java.nio.charset.StandardCharsets#UTF_8} is the most common choice. + * + * @param charset the {@link Charset} the string is encoded in. + * @return a new {@link Decoder}. + */ + public static Decoder<String> ofString(Charset charset) { + return new StringDecoder(charset); + } + + private static int align(int offset, byte alignment) { + return offset % alignment == 0 ? offset : offset + alignment - (offset % alignment); + } + + private static int getIntN(ByteBuffer byteSlice) { + var intBytes = new byte[4]; + byteSlice.get(intBytes, 0, Math.min(4, byteSlice.limit())); + return ByteBuffer.wrap(intBytes).order(LITTLE_ENDIAN).getInt(); + } + + @SuppressWarnings("java:S3358") + private static int byteCount(int n) { + return n < (1 << 8) ? 1 : n < (1 << 16) ? 2 : 4; + } + + private static class ArrayDecoder<U> extends Decoder<List<U>> { + + private final Decoder<U> elementDecoder; + + ArrayDecoder(Decoder<U> elementDecoder) { + this.elementDecoder = elementDecoder; + } + + @Override + public byte alignment() { + return elementDecoder.alignment(); + } + + @Override + @Nullable + Integer fixedSize() { + return null; + } + + @Override + public List<U> decode(ByteBuffer byteSlice) { + List<U> elements; + + var elementSize = elementDecoder.fixedSize(); + if (elementSize != null) { + // A simple C-style array. + elements = new ArrayList<>(byteSlice.limit() / elementSize); + for (int i = 0; i < byteSlice.limit(); i += elementSize) { + var element = elementDecoder.decode(byteSlice.slice(i, elementSize)); + elements.add(element); + } + } else { + // An array with aligned elements and a vector of framing offsets in the end. + int framingOffsetSize = byteCount(byteSlice.limit()); + int lastFramingOffset = + getIntN(byteSlice.slice(byteSlice.limit() - framingOffsetSize, framingOffsetSize)); + int elementCount = (byteSlice.limit() - lastFramingOffset) / framingOffsetSize; + + elements = new ArrayList<>(elementCount); + int position = 0; + for (int i = 0; i < elementCount; i++) { + int framingOffset = + getIntN( + byteSlice.slice(lastFramingOffset + i * framingOffsetSize, framingOffsetSize)); + elements.add(elementDecoder.decode(byteSlice.slice(position, framingOffset - position))); + position = align(framingOffset, alignment()); + } + } + + return elements; + } + } + + private static class MaybeDecoder<U> extends Decoder<Optional<U>> { + + private final Decoder<U> elementDecoder; + + MaybeDecoder(Decoder<U> elementDecoder) { + this.elementDecoder = elementDecoder; + } + + @Override + public byte alignment() { + return elementDecoder.alignment(); + } + + @Override + @Nullable + Integer fixedSize() { + return null; + } + + @Override + public Optional<U> decode(ByteBuffer byteSlice) { + if (!byteSlice.hasRemaining()) { + return Optional.empty(); + } else { + if (!elementDecoder.hasFixedSize()) { + // Remove trailing zero byte. + byteSlice.limit(byteSlice.limit() - 1); + } + + return Optional.of(elementDecoder.decode(byteSlice)); + } + } + } + + private static class StructureDecoder<U extends Record> extends Decoder<U> { + + private final Class<U> recordType; + private final TupleDecoder tupleDecoder; + + StructureDecoder(Class<U> recordType, Decoder<?>... componentDecoders) { + var recordComponents = recordType.getRecordComponents(); + if (componentDecoders.length != recordComponents.length) { + throw new IllegalArgumentException( + "number of decoders (%d) does not match number of structure components (%d)" + .formatted(componentDecoders.length, recordComponents.length)); + } + + this.recordType = recordType; + this.tupleDecoder = new TupleDecoder(componentDecoders); + } + + @Override + public byte alignment() { + return tupleDecoder.alignment(); + } + + @Override + public Integer fixedSize() { + return tupleDecoder.fixedSize(); + } + + @Override + public U decode(ByteBuffer byteSlice) { + Object[] recordConstructorArguments = tupleDecoder.decode(byteSlice); + + try { + var recordComponentTypes = + Arrays.stream(recordType.getRecordComponents()) + .map(RecordComponent::getType) + .toArray(Class<?>[]::new); + var recordConstructor = recordType.getDeclaredConstructor(recordComponentTypes); + return recordConstructor.newInstance(recordConstructorArguments); + } catch (NoSuchMethodException + | InstantiationException + | IllegalAccessException + | InvocationTargetException e) { + throw new IllegalStateException(e); + } + } + } + + private static class TupleDecoder extends Decoder<Object[]> { + + private final Decoder<?>[] componentDecoders; + + TupleDecoder(Decoder<?>... componentDecoders) { + this.componentDecoders = componentDecoders; + } + + @Override + public byte alignment() { + return (byte) Arrays.stream(componentDecoders).mapToInt(Decoder::alignment).max().orElse(1); + } + + @Override + public Integer fixedSize() { + int position = 0; + for (var componentDecoder : componentDecoders) { + var fixedComponentSize = componentDecoder.fixedSize(); + if (fixedComponentSize == null) { + return null; + } + + position = align(position, componentDecoder.alignment()); + position += fixedComponentSize; + } + + if (position == 0) { + return 1; + } + + return align(position, alignment()); + } + + @Override + public Object[] decode(ByteBuffer byteSlice) { + int framingOffsetSize = byteCount(byteSlice.limit()); + + var objects = new Object[componentDecoders.length]; + + int position = 0; + int framingOffsetIndex = 0; + int componentIndex = 0; + for (var componentDecoder : componentDecoders) { + position = align(position, componentDecoder.alignment()); + + var fixedComponentSize = componentDecoder.fixedSize(); + if (fixedComponentSize != null) { + objects[componentIndex] = + componentDecoder.decode(byteSlice.slice(position, fixedComponentSize)); + position += fixedComponentSize; + } else { + if (componentIndex == componentDecoders.length - 1) { + // The last component never has a framing offset. + int endPosition = byteSlice.limit() - framingOffsetIndex * framingOffsetSize; + objects[componentIndex] = + componentDecoder.decode(byteSlice.slice(position, endPosition - position)); + position = endPosition; + } else { + int framingOffset = + getIntN( + byteSlice.slice( + byteSlice.limit() - (1 + framingOffsetIndex) * framingOffsetSize, + framingOffsetSize)); + objects[componentIndex] = + componentDecoder.decode(byteSlice.slice(position, framingOffset - position)); + position = framingOffset; + ++framingOffsetIndex; + } + } + + ++componentIndex; + } + + return objects; + } + } + + private static class VariantDecoder extends Decoder<Variant> { + + @Override + public byte alignment() { + return 8; + } + + @Override + @Nullable + Integer fixedSize() { + return null; + } + + @Override + public Variant decode(ByteBuffer byteSlice) { + for (int i = byteSlice.limit() - 1; i >= 0; --i) { + if (byteSlice.get(i) != 0) { + continue; + } + + var dataBytes = byteSlice.slice(0, i); + var signatureBytes = byteSlice.slice(i + 1, byteSlice.limit() - (i + 1)); + + Signature signature; + try { + signature = Signature.parse(signatureBytes); + } catch (ParseException e) { + throw new IllegalArgumentException(e); + } + + return new Variant(signature, signature.decoder().decode(dataBytes)); + } + + throw new IllegalArgumentException("variant signature not found"); + } + } + + private static class BooleanDecoder extends Decoder<Boolean> { + + @Override + public byte alignment() { + return 1; + } + + @Override + public Integer fixedSize() { + return 1; + } + + @Override + public Boolean decode(ByteBuffer byteSlice) { + return byteSlice.get() != 0; + } + } + + private static class ByteDecoder extends Decoder<Byte> { + + @Override + public byte alignment() { + return 1; + } + + @Override + public Integer fixedSize() { + return 1; + } + + @Override + public Byte decode(ByteBuffer byteSlice) { + return byteSlice.get(); + } + } + + private static class ShortDecoder extends Decoder<Short> { + + @Override + public byte alignment() { + return 2; + } + + @Override + public Integer fixedSize() { + return 2; + } + + @Override + public Short decode(ByteBuffer byteSlice) { + return byteSlice.getShort(); + } + } + + private static class IntegerDecoder extends Decoder<Integer> { + + @Override + public byte alignment() { + return 4; + } + + @Override + public Integer fixedSize() { + return 4; + } + + @Override + public Integer decode(ByteBuffer byteSlice) { + return byteSlice.getInt(); + } + } + + private static class LongDecoder extends Decoder<Long> { + + @Override + public byte alignment() { + return 8; + } + + @Override + public Integer fixedSize() { + return 8; + } + + @Override + public Long decode(ByteBuffer byteSlice) { + return byteSlice.getLong(); + } + } + + private static class DoubleDecoder extends Decoder<Double> { + + @Override + public byte alignment() { + return 8; + } + + @Override + public Integer fixedSize() { + return 8; + } + + @Override + public Double decode(ByteBuffer byteSlice) { + return byteSlice.getDouble(); + } + } + + private static class StringDecoder extends Decoder<String> { + + private final Charset charset; + + public StringDecoder(Charset charset) { + this.charset = charset; + } + + @Override + public byte alignment() { + return 1; + } + + @Override + @Nullable + Integer fixedSize() { + return null; + } + + @Override + public String decode(ByteBuffer byteSlice) { + byteSlice.limit(byteSlice.limit() - 1); + return charset.decode(byteSlice).toString(); + } + } +} diff --git a/jgvariant-core/src/main/java/eu/mulk/jgvariant/core/Signature.java b/jgvariant-core/src/main/java/eu/mulk/jgvariant/core/Signature.java new file mode 100644 index 0000000..d9de5f1 --- /dev/null +++ b/jgvariant-core/src/main/java/eu/mulk/jgvariant/core/Signature.java @@ -0,0 +1,116 @@ +package eu.mulk.jgvariant.core; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; + +/** + * A GVariant signature string. + * + * <p>Describes a type in the GVariant type system. The type can be arbitrarily complex. + * + * <p><strong>Examples</strong> + * + * <dl> + * <dt>{@code "i"} + * <dd>a single 32-bit integer + * <dt>{@code "ai"} + * <dd>an array of 32-bit integers + * <dt>{@code "(bbb(sai))"} + * <dd>a record consisting of three booleans and a nested record, which consists of a string and + * an array of 32-bit integers + * </dl> + */ +@API(status = Status.STABLE) +public final class Signature { + + private final String signatureString; + private final Decoder<?> decoder; + + Signature(ByteBuffer signatureBytes) throws ParseException { + this.decoder = parseSignature(signatureBytes); + + signatureBytes.rewind(); + this.signatureString = StandardCharsets.US_ASCII.decode(signatureBytes).toString(); + } + + static Signature parse(ByteBuffer signatureBytes) throws ParseException { + return new Signature(signatureBytes); + } + + static Signature parse(String signatureString) throws ParseException { + var signatureBytes = ByteBuffer.wrap(signatureString.getBytes(StandardCharsets.US_ASCII)); + return parse(signatureBytes); + } + + /** + * Returns a {@link Decoder} that can decode values conforming to this signature. + * + * @return a {@link Decoder} for this signature + */ + @SuppressWarnings("unchecked") + Decoder<Object> decoder() { + return (Decoder<Object>) decoder; + } + + /** + * Returns the signature formatted as a GVariant signature string. + * + * @return a GVariant signature string. + */ + @Override + public String toString() { + return signatureString; + } + + @Override + public boolean equals(Object o) { + return (o instanceof Signature signature) + && Objects.equals(signatureString, signature.signatureString); + } + + @Override + public int hashCode() { + return Objects.hash(signatureString); + } + + private static Decoder<?> parseSignature(ByteBuffer signature) throws ParseException { + char c = (char) signature.get(); + return switch (c) { + case 'b' -> Decoder.ofBoolean(); + case 'y' -> Decoder.ofByte(); + case 'n', 'q' -> Decoder.ofShort(); + case 'i', 'u' -> Decoder.ofInt(); + case 'x', 't' -> Decoder.ofLong(); + case 'd' -> Decoder.ofDouble(); + case 's', 'o', 'g' -> Decoder.ofString(StandardCharsets.UTF_8); + case 'v' -> Decoder.ofVariant(); + case 'm' -> Decoder.ofMaybe(parseSignature(signature)); + case 'a' -> Decoder.ofArray(parseSignature(signature)); + case '(', '{' -> Decoder.ofStructure(parseTupleTypes(signature).toArray(new Decoder<?>[0])); + default -> throw new ParseException( + String.format("encountered unknown signature byte '%c'", c), signature.position()); + }; + } + + private static List<Decoder<?>> parseTupleTypes(ByteBuffer signature) throws ParseException { + List<Decoder<?>> decoders = new ArrayList<>(); + + while (true) { + char c = (char) signature.get(signature.position()); + if (c == ')' || c == '}') { + signature.get(); + break; + } + + decoders.add(parseSignature(signature)); + } + + return decoders; + } +} diff --git a/jgvariant-core/src/main/java/eu/mulk/jgvariant/core/Variant.java b/jgvariant-core/src/main/java/eu/mulk/jgvariant/core/Variant.java new file mode 100644 index 0000000..d1c1049 --- /dev/null +++ b/jgvariant-core/src/main/java/eu/mulk/jgvariant/core/Variant.java @@ -0,0 +1,30 @@ +package eu.mulk.jgvariant.core; + +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; + +/** + * A dynamically typed GVariant value carrying a {@link Signature} describing its type. + * + * <p>{@link #value()} can be of one of the following types: + * + * <ul> + * <li>{@link Boolean} + * <li>{@link Byte} + * <li>{@link Short} + * <li>{@link Integer} + * <li>{@link Long} + * <li>{@link String} + * <li>{@link java.util.Optional} (a GVariant {@code Maybe} type) + * <li>{@link java.util.List} (a GVariant array) + * <li>{@code Object[]} (a GVariant structure) + * <li>{@link Variant} (a nested variant) + * </ul> + * + * @param signature the signature describing the type of the value. + * @param value the value itself; one of {@link Boolean}, {@link Byte}, {@link Short}, {@link + * Integer}, {@link Long}, {@link String}, {@link java.util.Optional}, {@link java.util.List}, + * {@code Object[]}, {@link Variant}. + */ +@API(status = Status.EXPERIMENTAL) +public record Variant(Signature signature, Object value) {} diff --git a/jgvariant-core/src/main/java/eu/mulk/jgvariant/core/package-info.java b/jgvariant-core/src/main/java/eu/mulk/jgvariant/core/package-info.java new file mode 100644 index 0000000..1754096 --- /dev/null +++ b/jgvariant-core/src/main/java/eu/mulk/jgvariant/core/package-info.java @@ -0,0 +1,27 @@ +/** + * Provides {@link eu.mulk.jgvariant.core.Decoder}, the foundational class for <a + * href="https://docs.gtk.org/glib/struct.Variant.html">GVariant</a> parsing. + * + * <p>Instances of {@link eu.mulk.jgvariant.core.Decoder} read a given value type from a {@link + * java.nio.ByteBuffer}. The class also contains factory methods to create those instances. + * + * <p><strong>Example</strong> + * + * <p>To parse a GVariant of type {@code "a(si)"}, which is an array of pairs of {@link + * java.lang.String} and {@code int}, you can use the following code: + * + * <pre>{@code + * record ExampleRecord(String s, int i) {} + * + * var decoder = + * Decoder.ofArray( + * Decoder.ofStructure( + * ExampleRecord.class, + * Decoder.ofString(UTF_8), + * Decoder.ofInt().withByteOrder(LITTLE_ENDIAN))); + * + * byte[] bytes = ...; + * List<ExampleRecord> example = decoder.decode(ByteBuffer.wrap(bytes)); + * }</pre> + */ +package eu.mulk.jgvariant.core; diff --git a/jgvariant-core/src/main/java/module-info.java b/jgvariant-core/src/main/java/module-info.java new file mode 100644 index 0000000..a1830f6 --- /dev/null +++ b/jgvariant-core/src/main/java/module-info.java @@ -0,0 +1,79 @@ +/** + * Provides a parser for the <a href="https://docs.gtk.org/glib/struct.Variant.html">GVariant</a> + * serialization format. + * + * <ul> + * <li><a href="#sect-overview">Overview</a> + * <li><a href="#sect-installation">Installation</a> + * </ul> + * + * <h2 id="sect-overview">Overview</h2> + * + * <p>The {@link eu.mulk.jgvariant.core} package contains the {@link eu.mulk.jgvariant.core.Decoder} + * type, which contains classes to parse and represent serialized <a + * href="https://docs.gtk.org/glib/struct.Variant.html">GVariant</a> values. + * + * <h2 id="sect-installation">Installation</h2> + * + * <ul> + * <li><a href="#sect-installation-maven">Usage with Maven</a> + * <li><a href="#sect-installation-gradle">Usage with Gradle</a> + * </ul> + * + * <h3 id="sect-installation-maven">Usage with Maven</h3> + * + * <pre>{@code + * <project> + * ... + * + * <dependencyManagement> + * ... + * + * <dependencies> + * <dependency> + * <groupId>eu.mulk.jgvariant</groupId> + * <artifactId>jgvariant-bom</artifactId> + * <version>0.1.4</version> + * <type>pom</type> + * <scope>import</scope> + * </dependency> + * </dependencies> + * + * ... + * </dependencyManagement> + * + * <dependencies> + * ... + * + * <dependency> + * <groupId>eu.mulk.jgvariant</groupId> + * <artifactId>jgvariant-core</artifactId> + * </dependency> + * + * ... + * </dependencies> + * + * ... + * </project> + * }</pre> + * + * <h3 id="sect-installation-gradle">Usage with Gradle</h3> + * + * <pre>{@code + * dependencies { + * ... + * + * implementation(platform("eu.mulk.jgvariant:jgvariant-bom:0.1.4") + * implementation("eu.mulk.jgvariant:jgvariant-core") + * + * ... + * } + * }</pre> + */ +module eu.mulk.jgvariant.core { + requires com.google.errorprone.annotations; + requires org.jetbrains.annotations; + requires org.apiguardian.api; + + exports eu.mulk.jgvariant.core; +} |