diff --git a/all/pom.xml b/all/pom.xml index 1540b704c69..5b94f055c13 100644 --- a/all/pom.xml +++ b/all/pom.xml @@ -1098,6 +1098,10 @@ io.helidon.service helidon-service-codegen + + io.helidon.metadata + helidon-metadata-hson + io.helidon.integrations.oci.sdk helidon-integrations-oci-sdk-processor diff --git a/bom/pom.xml b/bom/pom.xml index 215184fc7eb..279b34e57e8 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -1447,6 +1447,13 @@ ${helidon.version} + + + io.helidon.metadata + helidon-metadata-hson + ${helidon.version} + + io.helidon.integrations.oci.sdk diff --git a/metadata/hson/pom.xml b/metadata/hson/pom.xml new file mode 100644 index 00000000000..4c3026e4fa8 --- /dev/null +++ b/metadata/hson/pom.xml @@ -0,0 +1,60 @@ + + + + + 4.0.0 + + io.helidon.metadata + helidon-metadata-project + 4.1.0-SNAPSHOT + ../pom.xml + + + helidon-metadata-hson + Helidon Metadata HSON + + Helidon format for metadata (HSON) - a simplified JSON. + + + + + io.helidon.common + helidon-common-buffers + + + io.helidon.common + helidon-common + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + io.helidon.common.testing + helidon-common-testing-junit5 + test + + + diff --git a/metadata/hson/src/main/java/io/helidon/metadata/hson/Hson.java b/metadata/hson/src/main/java/io/helidon/metadata/hson/Hson.java new file mode 100644 index 00000000000..f91735dc91e --- /dev/null +++ b/metadata/hson/src/main/java/io/helidon/metadata/hson/Hson.java @@ -0,0 +1,623 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.metadata.hson; + +import java.io.InputStream; +import java.io.PrintWriter; +import java.math.BigDecimal; +import java.util.List; +import java.util.Optional; + +/** + * Main entry point for Helidon metadata format parsing and writing. + *

+ * This is a simplified JSON, so the same types are available (see {@link io.helidon.metadata.hson.Hson.Type}). + */ +public final class Hson { + private Hson() { + } + + /** + * Parse the data into a value. + * + * @param inputStream stream to parse + * @return a value + * @see io.helidon.metadata.hson.Hson.Value#type() + * @see io.helidon.metadata.hson.Hson.Value#asObject() + * @see io.helidon.metadata.hson.Hson.Value#asArray() + */ + public static Value parse(InputStream inputStream) { + return HsonParser.parse(inputStream); + } + + /** + * A new fluent API builder to construct an HSON Object. + * + * @return a new builder + */ + public static Object.Builder objectBuilder() { + return Object.builder(); + } + + /** + * The type of value. + */ + public enum Type { + /** + * String value. + */ + STRING, + /** + * Numeric value. + */ + NUMBER, + /** + * Boolean value. + */ + BOOLEAN, + /** + * Null value. + */ + NULL, + /** + * Nested object value. + */ + OBJECT, + /** + * Array value. + */ + ARRAY + } + + /** + * HSON Object. + *

+ * A representation of an object, with possible child values. + */ + public sealed interface Object extends Value permits HsonObject { + + /** + * A new fluent API builder to construct a HSON Object. + * + * @return a new builder + */ + static Builder builder() { + return new HsonObject.Builder(); + } + + /** + * Create an empty object. + * + * @return new empty instance + */ + static Object create() { + return builder().build(); + } + + /** + * Get a value. + * + * @param key key under this object + * @return value of that key, or empty if not present; may return value that represents null + * @see io.helidon.metadata.hson.Hson.Type + */ + Optional> value(String key); + + /** + * Get a boolean value. + * + * @param key key under this object + * @return boolean value if present + * @throws HsonException in case the key exists, but is not a {@code boolean} + */ + Optional booleanValue(String key); + + /** + * Get a boolean value with default if not defined. + * + * @param key key under this object + * @param defaultValue default value to use if the key does not exist + * @return boolean value, or default value if the key does not exist + * @throws HsonException in case the key exists, but is not a + * {@link io.helidon.metadata.hson.Hson.Type#BOOLEAN} + */ + boolean booleanValue(String key, boolean defaultValue); + + /** + * Get object value. If the value represents {@code null}, returns empty optional. + * + * @param key key under this object + * @return object value if present + * @throws HsonException in case the key exists, but is not a + * {@link io.helidon.metadata.hson.Hson.Type#OBJECT} + */ + Optional objectValue(String key); + + /** + * Get string value. + * + * @param key key under this object + * @return string value if present + * @throws HsonException in case the key exists, but is not a + * {@link io.helidon.metadata.hson.Hson.Type#STRING} + */ + Optional stringValue(String key); + + /** + * Get a string value with default if not defined. + * + * @param key key under this object + * @param defaultValue default value to use if the key does not exist + * @return string value, or default value if the key does not exist + * @throws HsonException in case the key exists, but is not a + * {@link io.helidon.metadata.hson.Hson.Type#STRING} + */ + String stringValue(String key, String defaultValue); + + /** + * Get int value. + * + * @param key key under this object + * @return int value if present, from {@link java.math.BigDecimal#intValue()} + * @throws HsonException in case the key exists, but is not a + * {@link io.helidon.metadata.hson.Hson.Type#NUMBER} + */ + Optional intValue(String key); + + /** + * Get an int value with default if not defined. + * + * @param key key under this object + * @param defaultValue default value to use if the key does not exist + * @return int value, or default value if the key does not exist + * @throws HsonException in case the key exists, but is not a + * {@link io.helidon.metadata.hson.Hson.Type#NUMBER} + * @see #intValue(String) + */ + int intValue(String key, int defaultValue); + + /** + * Get double value. + * + * @param key key under this object + * @return double value if present, from {@link java.math.BigDecimal#doubleValue()} + * @throws HsonException in case the key exists, but is not a + * {@link io.helidon.metadata.hson.Hson.Type#NUMBER} + */ + Optional doubleValue(String key); + + /** + * Get a double value with default if not defined (or null). + * + * @param key key under this object + * @param defaultValue default value to use if the key does not exist + * @return double value, or default value if the key does not exist + * @throws HsonException in case the key exists, but is not a + * {@link io.helidon.metadata.hson.Hson.Type#NUMBER} + * @see #doubleValue(String) + */ + double doubleValue(String key, double defaultValue); + + /** + * Get number value. + * + * @param key key under this object + * @return big decimal value if present + * @throws HsonException in case the key exists, but is not a + * {@link io.helidon.metadata.hson.Hson.Type#NUMBER} + */ + Optional numberValue(String key); + + /** + * Get number value with default if not defined (or null). + * + * @param key key under this object + * @param defaultValue default value to use if not present or null + * @return big decimal value + */ + BigDecimal numberValue(String key, BigDecimal defaultValue); + + /** + * Get string array value. + * + * @param key key under this object + * @return string array value, if the key exists + * @throws HsonException in case the key exists, is an array, but elements are not strings + * @throws HsonException in case the key exists, but is not an array + */ + Optional> stringArray(String key); + + /** + * Get object array value. + * + * @param key key under this object + * @return object array value, if the key exists + * @throws HsonException in case the key exists, is an array, but elements are not objects + * @throws HsonException in case the key exists, but is not an array + */ + Optional> objectArray(String key); + + /** + * Get number array value. + * + * @param key key under this object + * @return number array value, if the key exists + * @throws HsonException in case the key exists, is an array, but elements are not numbers + * @throws HsonException in case the key exists, but is not an array + */ + Optional> numberArray(String key); + + /** + * Get boolean array value. + * + * @param key key under this object + * @return boolean array value, if the key exists + * @throws HsonException in case the key exists, is an array, but elements are not booleans + * @throws HsonException in case the key exists, but is not an array + */ + Optional> booleanArray(String key); + + /** + * Get array value. + * + * @param key key under this object + * @return array value, if the key exists + * @throws HsonException in case the key exists, but is not an array + */ + Optional arrayValue(String key); + + /** + * Fluent API builder for {@link io.helidon.metadata.hson.Hson.Object}. + * + * @see #build() + */ + interface Builder extends io.helidon.common.Builder { + /** + * Unset an existing value assigned to the key. + * This method does not care if the key is mapped or not. + * + * @param key key to unset + * @return updated instance (this instance) + */ + Builder unset(String key); + + /** + * Set a null value for the specified key. + * + * @param key key to set + * @return updated instance (this instance) + */ + Builder setNull(String key); + + /** + * Set a value. + * + * @param key key to set + * @param value value to assign to the key + * @return updated instance (this instance) + */ + Builder set(String key, Value value); + + /** + * Set a string value. + * + * @param key key to set + * @param value value to assign to the key + * @return updated instance (this instance) + */ + Builder set(String key, String value); + + /** + * Set a boolean value. + * + * @param key key to set + * @param value value to assign to the key + * @return updated instance (this instance) + */ + Builder set(String key, boolean value); + + /** + * Set a double value. + * + * @param key key to set + * @param value value to assign to the key + * @return updated instance (this instance) + */ + Builder set(String key, double value); + + /** + * Set an int value. + * + * @param key key to set + * @param value value to assign to the key + * @return updated instance (this instance) + */ + Builder set(String key, int value); + + /** + * Set a long value. + * + * @param key key to set + * @param value value to assign to the key + * @return updated instance (this instance) + */ + Builder set(String key, long value); + + /** + * Set a {@link java.math.BigDecimal} value. + * + * @param key key to set + * @param value value to assign to the key + * @return updated instance (this instance) + */ + Builder set(String key, BigDecimal value); + + /** + * Set an array. + * + * @param key key to set + * @param value value to assign to the key + * @return updated instance (this instance) + */ + Builder set(String key, Array value); + + /** + * Set an array of objects. + * + * @param key key to set + * @param value value to assign to the key + * @return updated instance (this instance) + */ + Builder setObjects(String key, List value); + + /** + * Set an array of strings. + * + * @param key key to set + * @param value value to assign to the key + * @return updated instance (this instance) + */ + Builder setStrings(String key, List value); + + /** + * Set an array of longs. + * + * @param key key to set + * @param value value to assign to the key + * @return updated instance (this instance) + */ + Builder setLongs(String key, List value); + + /** + * Set an array of doubles. + * + * @param key key to set + * @param value value to assign to the key + * @return updated instance (this instance) + */ + Builder setDoubles(String key, List value); + + /** + * Set an array of numbers (such as {@link java.math.BigDecimal}). + * + * @param key key to set + * @param value value to assign to the key + * @return updated instance (this instance) + */ + Builder setNumbers(String key, List value); + + /** + * Set an array of booleans. + * + * @param key key to set + * @param value value to assign to the key + * @return updated instance (this instance) + */ + Builder setBooleans(String key, List value); + } + } + + /** + * A HSON value (may of types of {@link io.helidon.metadata.hson.Hson.Type}). + * + * @param type of the value + */ + public sealed interface Value permits HsonValues.StringValue, + HsonValues.NumberValue, + HsonValues.BooleanValue, + HsonValues.NullValue, + Hson.Object, + Hson.Array { + /** + * Write the HSON value. + * + * @param writer writer to write to + */ + void write(PrintWriter writer); + + /** + * Value. + * + * @return the value + */ + T value(); + + /** + * Type of this value. + * + * @return type of this value + */ + Type type(); + + /** + * Get an object array from this parsed value. + * + * @return object array, or this object as an array + * @throws HsonException in case this object is not of type + * {@link io.helidon.metadata.hson.Hson.Type#OBJECT} + */ + default Array asArray() { + if (type() != Type.ARRAY) { + throw new HsonException("Attempting to read object of type " + type() + " as an array"); + } + + return (Array) this; + } + + /** + * Get an object from this parsed value. + * + * @return this value as an object + * @throws HsonException in case this object is not of type + * {@link io.helidon.metadata.hson.Hson.Type#OBJECT} + */ + default Hson.Object asObject() { + if (type() != Type.OBJECT) { + throw new HsonException("Attempting to get object of type " + type() + " as an Object"); + } + + return (Hson.Object) this; + } + } + + /** + * A representation of HSON array. + * HSON array is an array of values (of any type). + */ + public sealed interface Array extends Value>> permits HsonArray { + /** + * Create an empty JArray. + * + * @return empty array + */ + static Array create() { + return HsonArray.create(); + } + + /** + * Create a new array of HSON values. + * + * @param values list of values + * @return a new array + */ + static Array create(List> values) { + return HsonArray.create(values); + } + + /** + * Create a new array of Strings. + * + * @param strings String list + * @return a new string array + */ + static Array createStrings(List strings) { + return HsonArray.createStrings(strings); + } + + /** + * Create a new array of Numbers. + * + * @param numbers {@link java.math.BigDecimal} list + * @return a new number array + */ + static Array createNumbers(List numbers) { + return HsonArray.createNumbers(numbers); + } + + /** + * Create a new array of booleans. + * + * @param booleans boolean list + * @return a new boolean array + */ + static Array createBooleans(List booleans) { + return HsonArray.createBooleans(booleans); + } + + /** + * Create a new array of Numbers from long values. + * + * @param values long numbers + * @return a new number array + */ + static Array create(long... values) { + return HsonArray.create(values); + } + + /** + * Create a new array of Numbers from int values. + * + * @param values int numbers + * @return a new number array + */ + static Array create(int... values) { + return HsonArray.create(values); + } + + /** + * Create a new array of Numbers from double values. + * + * @param values double numbers + * @return a new number array + */ + static Array create(double... values) { + return HsonArray.create(values); + } + + /** + * Create a new array of Numbers from float values. + * + * @param values float numbers + * @return a new number array + */ + static Array create(float... values) { + return HsonArray.create(values); + } + + /** + * Assume this is an array of strings, and return the list. + * + * @return all string values of this array, except for nulls + * @throws HsonException in case not all elements of this array are strings (or nulls) + */ + List getStrings(); + + /** + * Assume this is an array of booleans, and return the list. + * + * @return all boolean values of this array, except for nulls + * @throws HsonException in case not all elements of this array are booleans (or nulls) + */ + List getBooleans(); + + /** + * Assume this is an array of numbers, and return the list. + * + * @return all big decimal values of this array, except for nulls + * @throws HsonException in case not all elements of this array are numbers (or nulls) + */ + List getNumbers(); + + /** + * Assume this is an array of objects, and return the list. + * + * @return all object values of this array, except for nulls + * @throws HsonException in case not all elements of this array are objects (or nulls) + */ + List getObjects(); + } +} diff --git a/metadata/hson/src/main/java/io/helidon/metadata/hson/HsonArray.java b/metadata/hson/src/main/java/io/helidon/metadata/hson/HsonArray.java new file mode 100644 index 00000000000..ec2e3067f91 --- /dev/null +++ b/metadata/hson/src/main/java/io/helidon/metadata/hson/HsonArray.java @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.metadata.hson; + +import java.io.PrintWriter; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.DoubleStream; +import java.util.stream.IntStream; +import java.util.stream.LongStream; + +final class HsonArray implements Hson.Array { + private final List> values; + + private HsonArray(List> array) { + this.values = List.copyOf(array); + } + + static Hson.Array create() { + return new HsonArray(List.of()); + } + + static Hson.Array create(List> values) { + return new HsonArray(values); + } + + static Hson.Array createStrings(List strings) { + List values = strings.stream() + .map(HsonValues.StringValue::create) + .collect(Collectors.toList()); + + return new HsonArray(values); + } + + static Hson.Array createNumbers(List numbers) { + return new HsonArray(numbers.stream() + .map(HsonValues.NumberValue::create) + .collect(Collectors.toUnmodifiableList())); + } + + static Hson.Array createBooleans(List booleans) { + return new HsonArray(booleans.stream() + .map(HsonValues.BooleanValue::create) + .collect(Collectors.toUnmodifiableList())); + } + + static Hson.Array create(long... values) { + List collect = LongStream.of(values) + .mapToObj(BigDecimal::new) + .collect(Collectors.toUnmodifiableList()); + return Hson.Array.createNumbers(collect); + } + + static Hson.Array create(int... values) { + List collect = IntStream.of(values) + .mapToObj(BigDecimal::new) + .collect(Collectors.toUnmodifiableList()); + return Hson.Array.createNumbers(collect); + } + + static Hson.Array create(double... values) { + List collect = DoubleStream.of(values) + .mapToObj(BigDecimal::new) + .collect(Collectors.toUnmodifiableList()); + return Hson.Array.createNumbers(collect); + } + + static Hson.Array create(float... values) { + List list = new ArrayList<>(values.length); + for (float value : values) { + list.add(new BigDecimal(value)); + } + + return Hson.Array.createNumbers(list); + } + + @Override + public List> value() { + return values; + } + + @Override + public void write(PrintWriter metaWriter) { + metaWriter.write('['); + + for (int i = 0; i < values.size(); i++) { + values.get(i).write(metaWriter); + if (i < (values.size() - 1)) { + metaWriter.write(','); + } + } + + metaWriter.write(']'); + } + + @Override + public Hson.Type type() { + return Hson.Type.ARRAY; + } + + @Override + public List getStrings() { + return getTypedList(Hson.Type.STRING); + } + + @Override + public List getBooleans() { + return getTypedList(Hson.Type.BOOLEAN); + } + + @Override + public List getNumbers() { + return getTypedList(Hson.Type.NUMBER); + } + + @Override + public List getObjects() { + return getTypedList(Hson.Type.OBJECT); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof HsonArray jArray)) { + return false; + } + return Objects.equals(values, jArray.values); + } + + @Override + public int hashCode() { + return Objects.hash(values); + } + + @Override + public String toString() { + return "[" + + "values=" + values + + ']'; + } + + @SuppressWarnings("unchecked") + private List getTypedList(Hson.Type type) { + if (values.isEmpty()) { + return List.of(); + } + + List list = new ArrayList<>(); + + for (Hson.Value value : values) { + if (value.type() == Hson.Type.NULL) { + // null values are ignored + continue; + } + if (value.type() != type) { + throw new HsonException("Requested array of " + type + ", but array element is of type: " + + value.type()); + } + list.add((T) value.value()); + } + + return List.copyOf(list); + } +} diff --git a/metadata/hson/src/main/java/io/helidon/metadata/hson/HsonException.java b/metadata/hson/src/main/java/io/helidon/metadata/hson/HsonException.java new file mode 100644 index 00000000000..9a0b95f7d2d --- /dev/null +++ b/metadata/hson/src/main/java/io/helidon/metadata/hson/HsonException.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.metadata.hson; + +import java.util.Objects; + +/** + * Exception marking a problem with HSON operations. + */ +public class HsonException extends RuntimeException { + /** + * Create a new instance with a customized message. + * + * @param message message to use + */ + public HsonException(String message) { + super(Objects.requireNonNull(message)); + } + + /** + * Create a new instance with a customized message and cause. + * + * @param message message to use + * @param cause cause of this exception + */ + public HsonException(String message, Throwable cause) { + super(Objects.requireNonNull(message), + Objects.requireNonNull(cause)); + } +} diff --git a/metadata/hson/src/main/java/io/helidon/metadata/hson/HsonObject.java b/metadata/hson/src/main/java/io/helidon/metadata/hson/HsonObject.java new file mode 100644 index 00000000000..a826223daba --- /dev/null +++ b/metadata/hson/src/main/java/io/helidon/metadata/hson/HsonObject.java @@ -0,0 +1,361 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.metadata.hson; + +import java.io.PrintWriter; +import java.math.BigDecimal; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Function; +import java.util.stream.Collectors; + +final class HsonObject implements Hson.Object { + private final Map> values; + + private HsonObject(Map> values) { + this.values = values; + } + + @Override + public Hson.Object value() { + return this; + } + + @Override + public Optional> value(String key) { + return Optional.ofNullable(values.get(key)); + } + + @Override + public Optional booleanValue(String key) { + return getValue(key, Hson.Type.BOOLEAN); + } + + @Override + public boolean booleanValue(String key, boolean defaultValue) { + return booleanValue(key).orElse(defaultValue); + } + + @Override + public Optional objectValue(String key) { + return getValue(key, Hson.Type.OBJECT); + } + + @Override + public Optional stringValue(String key) { + return getValue(key, Hson.Type.STRING); + } + + @Override + public String stringValue(String key, String defaultValue) { + return stringValue(key).orElse(defaultValue); + } + + @Override + public Optional intValue(String key) { + return this.getValue(key, Hson.Type.NUMBER).map(BigDecimal::intValue); + } + + @Override + public int intValue(String key, int defaultValue) { + return intValue(key).orElse(defaultValue); + } + + @Override + public Optional doubleValue(String key) { + return this.getValue(key, Hson.Type.NUMBER).map(BigDecimal::doubleValue); + } + + @Override + public double doubleValue(String key, double defaultValue) { + return doubleValue(key).orElse(defaultValue); + } + + @Override + public Optional numberValue(String key) { + return this.getValue(key, Hson.Type.NUMBER); + } + + @Override + public BigDecimal numberValue(String key, BigDecimal defaultValue) { + return numberValue(key).orElse(defaultValue); + } + + @Override + public Optional> stringArray(String key) { + return getTypedList(key, Hson.Array::getStrings); + } + + @Override + public Optional> objectArray(String key) { + return getTypedList(key, Hson.Array::getObjects); + } + + @Override + public Optional> numberArray(String key) { + return getTypedList(key, Hson.Array::getNumbers); + } + + @Override + public Optional> booleanArray(String key) { + return getTypedList(key, Hson.Array::getBooleans); + } + + @Override + public Optional arrayValue(String key) { + Hson.Value jValue = values.get(key); + if (jValue == null) { + return Optional.empty(); + } + if (jValue.type() != Hson.Type.ARRAY) { + throw new HsonException(exceptionMessage(key, " array requested, yet this key is not an array, but " + type())); + } + + return Optional.of((Hson.Array) jValue); + } + + @Override + public void write(PrintWriter writer) { + Objects.requireNonNull(writer); + + writer.write('{'); + AtomicBoolean first = new AtomicBoolean(true); + + values.forEach((key, value) -> { + writeNext(writer, first); + writer.write('\"'); + writer.write(key); + writer.write("\":"); + value.write(writer); + }); + + writer.write('}'); + } + + @Override + public Hson.Type type() { + return Hson.Type.OBJECT; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof HsonObject object)) { + return false; + } + return Objects.equals(values, object.values); + } + + @Override + public int hashCode() { + return Objects.hashCode(values); + } + + @Override + public String toString() { + return "{" + + values + + '}'; + } + + @SuppressWarnings("unchecked") + private Optional getValue(String key, Hson.Type type) { + Hson.Value jValue = values.get(key); + if (jValue == null || jValue.type() == Hson.Type.NULL) { + return Optional.empty(); + } + if (jValue.type() != type) { + throw new HsonException(exceptionMessage(key, "requested as a " + type + + ", but it is of type " + jValue.type())); + } + return Optional.of((T) jValue.value()); + } + + private Optional> getTypedList(String key, Function> arrayFunction) { + try { + return arrayValue(key).map(arrayFunction); + } catch (HsonException e) { + throw new HsonException(exceptionMessage(key, " failed to get typed array"), e); + } + } + + private String exceptionMessage(String key, String message) { + return "Object key \"" + key + "\": " + message; + } + + private void writeNext(PrintWriter metaWriter, AtomicBoolean first) { + if (first.get()) { + first.set(false); + return; + } + metaWriter.write(','); + } + + static class Builder implements Hson.Object.Builder { + private final Map> values = new LinkedHashMap<>(); + + Builder() { + } + + @Override + public Hson.Object build() { + return new HsonObject(new LinkedHashMap<>(values)); + } + + @Override + public Builder unset(String key) { + Objects.requireNonNull(key, "key cannot be null"); + + values.remove(key); + return this; + } + + @Override + public Hson.Object.Builder setNull(String key) { + values.put(key, HsonValues.NullValue.INSTANCE); + return this; + } + + @Override + public Builder set(String key, Hson.Value value) { + values.put(key, value); + return this; + } + + @Override + public Builder set(String key, String value) { + Objects.requireNonNull(key, "key cannot be null"); + Objects.requireNonNull(value, "value cannot be null"); + + values.put(key, HsonValues.StringValue.create(value)); + return this; + } + + @Override + public Builder set(String key, boolean value) { + Objects.requireNonNull(key, "key cannot be null"); + + values.put(key, HsonValues.BooleanValue.create(value)); + return this; + } + + @Override + public Builder set(String key, double value) { + Objects.requireNonNull(key, "key cannot be null"); + + values.put(key, HsonValues.NumberValue.create(new BigDecimal(value))); + return this; + } + + @Override + public Builder set(String key, int value) { + Objects.requireNonNull(key, "key cannot be null"); + + values.put(key, HsonValues.NumberValue.create(new BigDecimal(value))); + return this; + } + + @Override + public Builder set(String key, long value) { + Objects.requireNonNull(key, "key cannot be null"); + + values.put(key, HsonValues.NumberValue.create(new BigDecimal(value))); + return this; + } + + @Override + public Builder set(String key, BigDecimal value) { + Objects.requireNonNull(key, "key cannot be null"); + Objects.requireNonNull(value, "value cannot be null"); + + values.put(key, HsonValues.NumberValue.create(value)); + return this; + } + + @Override + public Builder set(String key, Hson.Array value) { + Objects.requireNonNull(key, "key cannot be null"); + Objects.requireNonNull(value, "value cannot be null"); + + values.put(key, value); + return this; + } + + @Override + public Builder setObjects(String key, List value) { + Objects.requireNonNull(key, "key cannot be null"); + Objects.requireNonNull(value, "value cannot be null"); + + values.put(key, Hson.Array.create(value)); + return this; + } + + @Override + public Builder setStrings(String key, List value) { + Objects.requireNonNull(key, "key cannot be null"); + Objects.requireNonNull(value, "value cannot be null"); + + values.put(key, Hson.Array.createStrings(value)); + return this; + } + + @Override + public Builder setLongs(String key, List value) { + Objects.requireNonNull(key, "key cannot be null"); + Objects.requireNonNull(value, "value cannot be null"); + + values.put(key, Hson.Array.createNumbers(value.stream() + .map(BigDecimal::new) + .collect(Collectors.toUnmodifiableList()))); + return this; + } + + @Override + public Builder setDoubles(String key, List value) { + Objects.requireNonNull(key, "key cannot be null"); + Objects.requireNonNull(value, "value cannot be null"); + values.put(key, Hson.Array.createNumbers(value.stream() + .map(BigDecimal::new) + .collect(Collectors.toUnmodifiableList()))); + return this; + } + + @Override + public Builder setNumbers(String key, List value) { + Objects.requireNonNull(key, "key cannot be null"); + Objects.requireNonNull(value, "value cannot be null"); + + values.put(key, Hson.Array.createNumbers(value)); + return this; + } + + @Override + public Builder setBooleans(String key, List value) { + Objects.requireNonNull(key, "key cannot be null"); + Objects.requireNonNull(value, "value cannot be null"); + + values.put(key, Hson.Array.createBooleans(value)); + return this; + } + } +} diff --git a/metadata/hson/src/main/java/io/helidon/metadata/hson/HsonParser.java b/metadata/hson/src/main/java/io/helidon/metadata/hson/HsonParser.java new file mode 100644 index 00000000000..eb9b4cdff8c --- /dev/null +++ b/metadata/hson/src/main/java/io/helidon/metadata/hson/HsonParser.java @@ -0,0 +1,290 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.metadata.hson; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +import io.helidon.common.buffers.BufferData; +import io.helidon.common.buffers.Bytes; +import io.helidon.common.buffers.DataReader; + +class HsonParser { + private static final int MAX_FIELD_LENGTH = 64000; + private static final byte COMMA = (byte) ','; + private static final byte QUOTES = (byte) '"'; + private static final byte ARRAY_START = (byte) '['; + private static final byte ARRAY_END = (byte) ']'; + private static final byte OBJECT_START = (byte) '{'; + private static final byte OBJECT_END = (byte) '}'; + private static final byte BACKSLASH = (byte) '\\'; + + private final DataReader reader; + private int position; + + private HsonParser(DataReader reader) { + this.reader = reader; + } + + static Hson.Value parse(InputStream stream) { + DataReader dr = new DataReader(() -> { + byte[] buffer = new byte[1024]; + try { + int num = stream.read(buffer); + if (num > 0) { + return buffer; + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + + return null; + }); + return new HsonParser(dr).read(true); + } + + private Hson.Value read(boolean topLevel) { + byte next = skipWhitespace(); + if (next == ARRAY_START) { + return readArray(); + } else if (next == OBJECT_START) { + return readObject(); + } + if (topLevel) { + throw new HsonException("Index: " + position + + ": failed to parse HSON, invalid object/array opening character: \n" + + BufferData.create(new byte[] {next}).debugDataHex()); + } + if (next == QUOTES) { + return readString("Object"); + } + return readValue(); + } + + private Hson.Value readArray() { + skip(); // skip [ + + List> values = new ArrayList<>(); + + while (true) { + byte next = skipWhitespace(); + if (next == ARRAY_END) { + skip(); + // end of array + return Hson.Array.create(values); + } + + Hson.Value value = switch (next) { + case OBJECT_START -> readObject(); + case ARRAY_START -> readArray(); + case QUOTES -> readString("Array"); + default -> readValue(); + }; + values.add(value); + + next = skipWhitespace(); + if (next == COMMA) { + skip(); // , + } else { + // this must be array end + next = skipWhitespace(); + if (next == ARRAY_END) { + skip(); + return Hson.Array.create(values); + } else { + throw new HsonException("Index: " + position + + ": value not followed by a comma, and array does not end"); + } + } + } + } + + private Hson.Value readObject() { + skip(); // skip { + + var object = Hson.Object.builder(); + + while (true) { + byte next = skipWhitespace(); + if (next == OBJECT_END) { + skip(); // skip } + return object.build(); + } + // now we have "key": value (may be an object, value, string) + String key = readKey(); + skipWhitespace(); + next = read(); + if (next != Bytes.COLON_BYTE) { + throw new HsonException("Index: " + position + + ": key is not followed by a colon. Key: " + BufferData.create(key) + .debugDataHex()); + } + skipWhitespace(); + // the value may be object, array, value + Hson.Value value = read(false); + object.set(key, value); + next = skipWhitespace(); + if (next == COMMA) { + skip(); // , + } else { + // this must be object end + next = skipWhitespace(); + if (next == OBJECT_END) { + skip(); // skip } + return object.build(); + } else { + throw new HsonException("Index: " + position + + ": value not followed by a comma, and object does not end. Found: \n" + + BufferData.create(new byte[] {next}).debugDataHex() + ", for key: \n" + + BufferData.create(key).debugDataHex()); + } + } + } + } + + private String readKey() { + byte read = reader.lookup(); + if (read != QUOTES) { + throw new HsonException("Index: " + position + + ": keys must be quoted, invalid beginning of key"); + } + return readString("Key").value(); + } + + private Hson.Value readString(String type) { + skip(); // skip " + + // now go until the first unescaped quotes + ByteArrayOutputStream value = new ByteArrayOutputStream(); + int count = 0; + boolean escaping = false; + while (count < MAX_FIELD_LENGTH) { + byte next = reader.read(); + + if (!escaping && next == QUOTES) { + return HsonValues.StringValue.create(value.toString(StandardCharsets.UTF_8)); + } + if (escaping) { + escaping = false; + char nextChar = (char) (next & 0xff); + if (nextChar == 'u') { + // there must be 4 hexadecimal digits after this + String hexadecimalEscape = reader.readAsciiString(4); + value.write((char) Integer.parseInt(hexadecimalEscape, 16)); + } else { + byte toWrite = switch (nextChar) { + case 'f' -> (byte) '\f'; + case 'n' -> (byte) '\n'; + case 'r' -> (byte) '\r'; + case 't' -> (byte) '\t'; + case 'b' -> (byte) '\b'; + case '\\' -> (byte) '\\'; + case '\"' -> (byte) '\"'; + case '/' -> (byte) '/'; + default -> throw new HsonException("Index " + position + + ": invalid escape char after backslash: '" + + nextChar + "'"); + }; + value.write(toWrite); + } + } else if (next == BACKSLASH) { + escaping = true; + } else { + value.write(next); + } + count++; + } + + throw new HsonException("Index: " + position + + ": " + type + " failed to find end quotes, or length is bigger than allowed. Max " + + "length: " + + MAX_FIELD_LENGTH + " bytes"); + } + + private Hson.Value readValue() { + String value = toNonStringValueEnd(); + + // true | false, integer, double + if ("true".equals(value) || "false".equals(value)) { + return HsonValues.BooleanValue.create(Boolean.parseBoolean(value)); + } + if ("null".equals(value)) { + return HsonValues.NullValue.INSTANCE; + } + try { + return HsonValues.NumberValue.create(new BigDecimal(value)); + } catch (NumberFormatException e) { + throw new HsonException("Index: " + position + + ": cannot parse HSON value into a number. Data: " + + BufferData.create(value).debugDataHex()); + } + } + + private String toNonStringValueEnd() { + ByteArrayOutputStream bo = new ByteArrayOutputStream(); + + // anything from here to next whitespace or comma (separating values) + while (true) { + byte next = reader.lookup(); + if (whitespace(next)) { + break; + } + if (next == COMMA || next == ARRAY_END || next == OBJECT_END) { + break; + } + skip(); + bo.write(next); + } + + return bo.toString(StandardCharsets.US_ASCII); + } + + private byte skipWhitespace() { + while (true) { + byte lookup = reader.lookup(); + if (whitespace(lookup)) { + skip(); + } else { + return lookup; + } + } + } + + private boolean whitespace(byte lookup) { + return switch (lookup) { + case Bytes.SPACE_BYTE, Bytes.TAB_BYTE, Bytes.CR_BYTE, Bytes.LF_BYTE -> true; + default -> false; + }; + } + + private void skip() { + reader.skip(1); + position++; + } + + private byte read() { + byte r = reader.read(); + position++; + return r; + } +} diff --git a/metadata/hson/src/main/java/io/helidon/metadata/hson/HsonValues.java b/metadata/hson/src/main/java/io/helidon/metadata/hson/HsonValues.java new file mode 100644 index 00000000000..6c94cb8e497 --- /dev/null +++ b/metadata/hson/src/main/java/io/helidon/metadata/hson/HsonValues.java @@ -0,0 +1,206 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.metadata.hson; + +import java.io.PrintWriter; +import java.math.BigDecimal; +import java.util.Objects; + +final class HsonValues { + private HsonValues() { + } + + static final class StringValue implements Hson.Value { + private final String value; + + StringValue(String value) { + this.value = value; + } + + public static StringValue create(String value) { + return new StringValue(value); + } + + @Override + public void write(PrintWriter writer) { + writer.write(quote(escape(value))); + } + + @Override + public String value() { + return value; + } + + @Override + public Hson.Type type() { + return Hson.Type.STRING; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof StringValue that)) { + return false; + } + return Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hashCode(value); + } + + @Override + public String toString() { + return quote(value); + } + + private String quote(String value) { + return '"' + value + '"'; + } + + private String escape(String string) { + return string.replaceAll("\n", "\\\\n") + .replaceAll("\"", "\\\\\"") + .replaceAll("\t", "\\\\\t") + .replaceAll("\r", "\\\\\r") + // replace two backslashes with four backslashes + .replaceAll("\\\\\\\\", "\\\\\\\\\\\\\\\\") + .replaceAll("\f", "\\\\\f"); + } + } + + static final class NumberValue implements Hson.Value { + private final BigDecimal value; + + NumberValue(BigDecimal value) { + this.value = value; + } + + public static NumberValue create(BigDecimal value) { + return new NumberValue(value); + } + + @Override + public void write(PrintWriter writer) { + writer.write(String.valueOf(value)); + } + + @Override + public BigDecimal value() { + return value; + } + + @Override + public Hson.Type type() { + return Hson.Type.NUMBER; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof NumberValue that)) { + return false; + } + return Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hashCode(value); + } + + @Override + public String toString() { + return String.valueOf(value); + } + } + + static final class NullValue implements Hson.Value { + static final NullValue INSTANCE = new NullValue(); + + private NullValue() { + } + + @Override + public void write(PrintWriter writer) { + writer.write("null"); + } + + @Override + public Void value() { + return null; + } + + @Override + public Hson.Type type() { + return Hson.Type.NULL; + } + } + + static final class BooleanValue implements Hson.Value { + private final boolean value; + + BooleanValue(boolean value) { + this.value = value; + } + + public static BooleanValue create(boolean value) { + return new BooleanValue(value); + } + + @Override + public void write(PrintWriter writer) { + writer.write(String.valueOf(value)); + } + + @Override + public Boolean value() { + return value; + } + + @Override + public Hson.Type type() { + return Hson.Type.BOOLEAN; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof BooleanValue that)) { + return false; + } + return value == that.value; + } + + @Override + public int hashCode() { + return Objects.hashCode(value); + } + + @Override + public String toString() { + return String.valueOf(value); + } + } +} diff --git a/metadata/hson/src/main/java/io/helidon/metadata/hson/package-info.java b/metadata/hson/src/main/java/io/helidon/metadata/hson/package-info.java new file mode 100644 index 00000000000..c4816172424 --- /dev/null +++ b/metadata/hson/src/main/java/io/helidon/metadata/hson/package-info.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * 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. + */ + +/** + * Tiny HSON parser and writer. This is intended for annotation processors, code generators, + * and readers of the generated files, such as Config Metadata, Service Registry etc. + *

+ * To write HSON (compatible with JSON), start with either of the following types: + *

    + *
  • {@link io.helidon.metadata.hson.Hson.Array}
  • + *
  • {@link io.helidon.metadata.hson.Hson.Object}
  • + *
+ * To read HSON, start with {@link io.helidon.metadata.hson.Hson#parse(java.io.InputStream)}. + *

+ * Supported features, non-features: + *

    + *
  • Only UTF-8 is supported
  • + *
  • Arrays
  • + *
  • Objects
  • + *
  • Nesting of objects and arrays
  • + *
  • String, BigDecimal, boolean, null
  • + *
  • No pretty print (always writes as small as possible)
  • + *
  • Keeps order of insertion on write
  • + *
  • Keeps order of original HSON on read
  • + *
  • Can read unicode escaped characters ({@code \ u0000}), but not write them
  • + *
  • Maximal value size is 64000 bytes
  • + *
  • Maximal HSON size is unlimited - NEVER USE WITH OVER-THE-NETWORK PROVIDED HSON
  • + *
+ * + * Should you use this library? + * No, unless: + *
    + *
  • You are developing writing or parsing of metadata in Helidon codegen or runtime for Helidon features, + * the metadata must be produced by Helidon as well (NEVER USE ON OVER-THE-NETWORK PROVIDED HSON)
  • + *
  • You are a brave user who wants to do something similar in their library
  • + *
+ * NOTE: HSON is compatible with JSON, with the limitations mentioned in the list + * above. This module is not Helidon JSON solution - please use one of the supported JSON libraries, + * such as JSON-P, JSON-B, or Jackson. + *

+ * WARNING: The HSON object is always fully read into memory + */ +package io.helidon.metadata.hson; diff --git a/metadata/hson/src/main/java/module-info.java b/metadata/hson/src/main/java/module-info.java new file mode 100644 index 00000000000..aa9ed225af5 --- /dev/null +++ b/metadata/hson/src/main/java/module-info.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * 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. + */ + +/** + * Helidon Metadata Format (simplified JSON). + *

+ * Kindly check the package documentation before considering using this module. + * + * @see io.helidon.metadata.hson + */ +module io.helidon.metadata.hson { + requires io.helidon.common; + requires io.helidon.common.buffers; + + exports io.helidon.metadata.hson; +} \ No newline at end of file diff --git a/metadata/hson/src/test/java/io/helidon/metadata/hson/ExistingTypesTest.java b/metadata/hson/src/test/java/io/helidon/metadata/hson/ExistingTypesTest.java new file mode 100644 index 00000000000..96fd6745e14 --- /dev/null +++ b/metadata/hson/src/test/java/io/helidon/metadata/hson/ExistingTypesTest.java @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.metadata.hson; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.Test; + +import static io.helidon.common.testing.junit5.OptionalMatcher.optionalEmpty; +import static io.helidon.common.testing.junit5.OptionalMatcher.optionalPresent; +import static io.helidon.common.testing.junit5.OptionalMatcher.optionalValue; +import static org.hamcrest.CoreMatchers.hasItems; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsCollectionWithSize.hasSize; + +class ExistingTypesTest { + @Test + void testServiceRegistry() throws IOException { + Hson.Object object; + try (InputStream inputStream = resource("/service-registry.json")) { + assertThat(inputStream, notNullValue()); + object = Hson.parse(inputStream) + .asObject() + .value(); + } + + Hson.Object generated = object.objectValue("generated") + .orElseThrow(() -> new IllegalStateException("Cannot find 'generated' object under root")); + + assertThat(generated.stringValue("trigger"), + optionalValue(is("io.helidon.service.codegen.ServiceRegistryCodegenExtension"))); + assertThat(generated.stringValue("value"), + optionalValue(is("io.helidon.service.codegen.ServiceRegistryCodegenExtension"))); + assertThat(generated.stringValue("version"), + optionalValue(is("1"))); + assertThat(generated.stringValue("comments"), + optionalValue(is("Service descriptors in module unnamed/io.helidon.examples.quickstart.se"))); + + List services = object.objectArray("services") + .orElseThrow(() -> new IllegalStateException("Cannot find 'services' object under root")); + + assertThat(services, hasSize(2)); + + Hson.Object service = services.get(0); + + assertThat(service.doubleValue("version"), optionalValue(is(1d))); + assertThat(service.stringValue("type"), + optionalValue(is("inject"))); + assertThat(service.stringValue("descriptor"), + optionalValue(is("io.helidon.examples.quickstart.se.GreetEndpoint__HttpFeature__ServiceDescriptor"))); + assertThat(service.doubleValue("weight"), optionalValue(is(100d))); + List contracts = service.stringArray("contracts") + .orElseThrow(() -> new IllegalStateException("Cannot find 'contracts' object under service")); + assertThat(contracts, hasItems("io.helidon.examples.quickstart.se.GreetEndpoint__HttpFeature", + "io.helidon.webserver.http.HttpFeature", + "io.helidon.webserver.ServerLifecycle", + "java.util.function.Supplier")); + + service = services.get(1); + assertThat(service.doubleValue("version"), optionalEmpty()); + assertThat(service.stringValue("type"), + optionalValue(is("inject"))); + assertThat(service.stringValue("descriptor"), + optionalValue(is("io.helidon.examples.quickstart.se.GreetEndpoint__ServiceDescriptor"))); + assertThat(service.doubleValue("weight"), optionalEmpty()); + contracts = service.stringArray("contracts") + .orElseThrow(() -> new IllegalStateException("Cannot find 'contracts' object under service")); + assertThat(contracts, hasItems("io.helidon.examples.quickstart.se.GreetEndpoint")); + } + + @Test + void testConfigMetadata() throws IOException { + List objects; + try (InputStream inputStream = resource("/config-metadata.json")) { + assertThat(inputStream, notNullValue()); + objects = Hson.parse(inputStream) + .asArray() + .getObjects(); + } + + assertThat(objects, hasSize(1)); + + Hson.Object module = objects.getFirst(); + + assertThat(module.stringValue("module"), optionalValue(is("io.helidon.common.configurable"))); + Optional> types = module.objectArray("types"); + assertThat(types, optionalPresent()); + List typesList = types.get(); + assertThat(typesList, hasSize(5)); + + Hson.Object first = typesList.getFirst(); + assertThat(first.stringValue("annotatedType"), + optionalValue(is("io.helidon.common.configurable.ResourceConfig"))); + assertThat(first.stringValue("type"), + optionalValue(is("io.helidon.common.configurable.Resource"))); + assertThat(first.booleanValue("is"), + optionalValue(is(true))); + assertThat(first.intValue("number"), + optionalValue(is(49))); + + List optionsList = first.objectArray("options") + .orElse(List.of()); + assertThat(optionsList, hasSize(9)); + Hson.Object firstOption = optionsList.getFirst(); + assertThat(firstOption.stringValue("description"), + optionalValue(is("Resource is located on filesystem.\n\n Path of the resource"))); + assertThat(firstOption.stringValue("key"), + optionalValue(is("path"))); + assertThat(firstOption.stringValue("method"), + optionalValue(is("io.helidon.common.configurable.ResourceConfig." + + "Builder#path(java.util.Optional)"))); + assertThat(firstOption.stringValue("type"), + optionalValue(is("java.nio.file.Path"))); + } + + private InputStream resource(String location) { + return ExistingTypesTest.class.getResourceAsStream(location); + } + +} diff --git a/metadata/hson/src/test/java/io/helidon/metadata/hson/HsonTest.java b/metadata/hson/src/test/java/io/helidon/metadata/hson/HsonTest.java new file mode 100644 index 00000000000..8cd164be71e --- /dev/null +++ b/metadata/hson/src/test/java/io/helidon/metadata/hson/HsonTest.java @@ -0,0 +1,332 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.metadata.hson; + +import java.io.ByteArrayInputStream; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.Test; + +import static io.helidon.common.testing.junit5.OptionalMatcher.optionalEmpty; +import static io.helidon.common.testing.junit5.OptionalMatcher.optionalPresent; +import static io.helidon.common.testing.junit5.OptionalMatcher.optionalValue; +import static org.hamcrest.CoreMatchers.hasItems; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.sameInstance; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsCollectionWithSize.hasSize; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class HsonTest { + @Test + void testWrongTypes() { + Hson.Object object = Hson.objectBuilder() + .set("number", 1) + .set("string", "hi") + .setLongs("numbers", List.of(1L, 2L, 3L)) + .setStrings("strings", List.of("hi", "there")) + .build(); + + assertThrows(HsonException.class, + () -> object.stringValue("number")); + assertThrows(HsonException.class, + () -> object.booleanValue("number")); + assertThrows(HsonException.class, + () -> object.doubleValue("string")); + assertThrows(HsonException.class, + () -> object.objectValue("string")); + assertThrows(HsonException.class, + () -> object.stringArray("string")); + assertThrows(HsonException.class, + () -> object.objectArray("string")); + assertThrows(HsonException.class, + () -> object.booleanArray("string")); + assertThrows(HsonException.class, + () -> object.numberArray("string")); + + assertThrows(HsonException.class, + () -> object.stringValue("strings")); + assertThrows(HsonException.class, + () -> object.booleanValue("strings")); + assertThrows(HsonException.class, + () -> object.doubleValue("strings")); + assertThrows(HsonException.class, + () -> object.objectValue("strings")); + assertThrows(HsonException.class, + () -> object.stringArray("numbers")); + assertThrows(HsonException.class, + () -> object.objectArray("strings")); + assertThrows(HsonException.class, + () -> object.booleanArray("strings")); + assertThrows(HsonException.class, + () -> object.numberArray("strings")); + } + + @Test + void testReads() { + Hson.Object empty = Hson.Object.create(); + + assertThat(empty.booleanValue("anyKey"), optionalEmpty()); + assertThat(empty.booleanValue("anyKey", true), is(true)); + + assertThat(empty.intValue("anyKey"), optionalEmpty()); + assertThat(empty.intValue("anyKey", 876), is(876)); + + assertThat(empty.doubleValue("anyKey"), optionalEmpty()); + assertThat(empty.doubleValue("anyKey", 876), is(876d)); + + assertThat(empty.stringValue("anyKey"), optionalEmpty()); + assertThat(empty.stringValue("anyKey", "default"), is("default")); + + assertThat(empty.numberValue("anyKey"), optionalEmpty()); + BigDecimal bd = new BigDecimal(14); + assertThat(empty.numberValue("anyKey", bd), sameInstance(bd)); + + assertThat(empty.stringArray("anyKey"), optionalEmpty()); + assertThat(empty.objectArray("anyKey"), optionalEmpty()); + assertThat(empty.numberArray("anyKey"), optionalEmpty()); + } + + @Test + void testSetNumbers() { + + BigDecimal one = new BigDecimal(14); + BigDecimal two = new BigDecimal(15); + + Hson.Object jObject = Hson.objectBuilder() + .setNumbers("numbers", List.of(one, two)) + .build(); + + List objects = jObject.numberArray("numbers") + .orElseThrow(() -> new IllegalStateException("numbers key should be filled with array")); + + assertThat(objects, hasItems(one, two)); + } + + @Test + void testSetBooleans() { + Hson.Object jObject = Hson.objectBuilder() + .setBooleans("booleans", List.of(true, false, true)) + .build(); + + List objects = jObject.booleanArray("booleans") + .orElseThrow(() -> new IllegalStateException("booleans key should be filled with array")); + + assertThat(objects, hasItems(true, false, true)); + } + + @Test + void testWriteStringArray() { + Hson.Array array = Hson.Array.createStrings(List.of("a", "b", "c")); + + StringWriter sw = new StringWriter(); + try (PrintWriter pw = new PrintWriter(sw)) { + array.write(pw); + } + + String string = sw.toString(); + assertThat(string, is("[\"a\",\"b\",\"c\"]")); + } + + @Test + void testWriteLongArray() { + Hson.Array array = Hson.Array.create(2L, 3L, 4L); + + StringWriter sw = new StringWriter(); + try (PrintWriter pw = new PrintWriter(sw)) { + array.write(pw); + } + + String string = sw.toString(); + assertThat(string, is("[2,3,4]")); + } + + @Test + void testWriteDoubleArray() { + Hson.Array array = Hson.Array.createNumbers(List.of(new BigDecimal(2), new BigDecimal(3), new BigDecimal("4.2"))); + + StringWriter sw = new StringWriter(); + try (PrintWriter pw = new PrintWriter(sw)) { + array.write(pw); + } + + String string = sw.toString(); + assertThat(string, is("[2,3,4.2]")); + } + + @Test + void testWriteBooleanArray() { + Hson.Array array = Hson.Array.createBooleans(List.of(true, false, true)); + + StringWriter sw = new StringWriter(); + try (PrintWriter pw = new PrintWriter(sw)) { + array.write(pw); + } + + String string = sw.toString(); + assertThat(string, is("[true,false,true]")); + } + + @Test + void testWriteObjectArray() { + Hson.Object first = Hson.objectBuilder() + .set("string", "value") + .set("long", 4L) + .set("double", 4d) + .set("boolean", true) + .setStrings("strings", List.of("a", "b")) + .setLongs("longs", List.of(1L, 2L)) + .setDoubles("doubles", List.of(1.5d, 2.5d)) + .setBooleans("booleans", List.of(true, false)) + .build(); + Hson.Object second = Hson.objectBuilder() + .set("string", "value2") + .build(); + + Hson.Array array = Hson.Array.create(List.of(first, second)); + + StringWriter sw = new StringWriter(); + try (PrintWriter pw = new PrintWriter(sw)) { + array.write(pw); + } + + String string = sw.toString(); + String expected = "[{\"string\":\"value\"," + + "\"long\":4," + + "\"double\":4," + + "\"boolean\":true," + + "\"strings\":[\"a\",\"b\"]," + + "\"longs\":[1,2]," + + "\"doubles\":[1.5,2.5]," + + "\"booleans\":[true,false]}," + + "{\"string\":\"value2\"}]"; + assertThat(string, is(expected)); + + Hson.Value read = Hson.parse(new ByteArrayInputStream(string.getBytes(StandardCharsets.UTF_8))); + assertThat(read.type(), is(Hson.Type.ARRAY)); + + Hson.Array objects = read.asArray(); + List value = objects.getObjects(); + assertThat(value, hasSize(2)); + Hson.Object readFirst = value.get(0); + assertThat(readFirst, is(first)); + Hson.Object readSecond = value.get(1); + assertThat(readSecond, is(second)); + } + + void testSetObjects() { + Hson.Object first = Hson.objectBuilder() + .set("key", "value1") + .build(); + Hson.Object second = Hson.objectBuilder() + .set("key", "value2") + .build(); + Hson.Object withArray = Hson.objectBuilder() + .setObjects("objects", List.of(first, second)) + .build(); + + List objects = withArray.objectArray("objects").get(); + assertThat(objects, hasItems(first, second)); + } + + @Test + void testNull() { + Hson.Object object = Hson.objectBuilder() + .setNull("null-key") + .build(); + + assertThat(object.value("null-key"), optionalPresent()); + assertThat(object.value("null-key").get().type(), is(Hson.Type.NULL)); + } + + @Test + void testUnset() { + Hson.Object object = Hson.objectBuilder() + .setNull("null-key") + .unset("null-key") + .build(); + + assertThat(object.value("null-key"), optionalEmpty()); + } + + @Test + void testFeatures() { + Hson.Value parsedValue = Hson.parse(HsonTest.class.getResourceAsStream("/json-features.json")); + assertThat(parsedValue.type(), is(Hson.Type.OBJECT)); + Hson.Object object = parsedValue.asObject(); + + testFeaturesNull(object); + testFeaturesArray(object); + testFeaturesEscapes(object); + testFeaturesNumbers(object); + } + + private void testFeaturesNumbers(Hson.Object object) { + Optional maybeObject = object.objectValue("numbers"); + assertThat(maybeObject, optionalPresent()); + Hson.Object numbers = maybeObject.get(); + + assertThat(numbers.numberValue("number1"), optionalValue(is(new BigDecimal(1)))); + assertThat(numbers.numberValue("number2"), optionalValue(is(new BigDecimal("1.5")))); + assertThat(numbers.numberValue("number3"), optionalValue(is(new BigDecimal("1.5e2")))); + assertThat(numbers.numberValue("number4"), optionalValue(is(new BigDecimal("1.5e-2")))); + assertThat(numbers.numberValue("number5"), optionalValue(is(new BigDecimal("1.5e2")))); + } + + private void testFeaturesEscapes(Hson.Object object) { + Optional maybeObject = object.objectValue("escapes"); + assertThat(maybeObject, optionalPresent()); + Hson.Object escapes = maybeObject.get(); + + assertThat(escapes.stringValue("newline"), optionalValue(is("a\nb"))); + assertThat(escapes.stringValue("quotes"), optionalValue(is("a\"b"))); + assertThat(escapes.stringValue("backslash"), optionalValue(is("a\\b"))); + assertThat(escapes.stringValue("slash"), optionalValue(is("a/b"))); + assertThat(escapes.stringValue("backspace"), optionalValue(is("a\bb"))); + assertThat(escapes.stringValue("formfeed"), optionalValue(is("a\fb"))); + assertThat(escapes.stringValue("cr"), optionalValue(is("a\rb"))); + assertThat(escapes.stringValue("tab"), optionalValue(is("a\tb"))); + assertThat(escapes.stringValue("unicode"), optionalValue(is("aHb"))); + + } + + private void testFeaturesArray(Hson.Object object) { + Optional maybeArray = object.arrayValue("array"); + assertThat(maybeArray, optionalPresent()); + List> value = maybeArray.get().value(); + assertThat("There should be 6 elements in array", value, hasSize(6)); + assertThat(value.get(0).type(), is(Hson.Type.BOOLEAN)); + assertThat(value.get(1).type(), is(Hson.Type.NULL)); + assertThat(value.get(2).type(), is(Hson.Type.STRING)); + assertThat(value.get(3).type(), is(Hson.Type.NUMBER)); + assertThat(value.get(4).type(), is(Hson.Type.OBJECT)); + assertThat(value.get(5).type(), is(Hson.Type.ARRAY)); + } + + private void testFeaturesNull(Hson.Object object) { + Optional> maybeValue = object.objectValue("nulls") + .flatMap(it -> it.value("field")); + assertThat(maybeValue, optionalPresent()); + Hson.Value value = maybeValue.get(); + assertThat(value.type(), is(Hson.Type.NULL)); + } +} diff --git a/metadata/hson/src/test/resources/config-metadata.json b/metadata/hson/src/test/resources/config-metadata.json new file mode 100644 index 00000000000..1084e4c7050 --- /dev/null +++ b/metadata/hson/src/test/resources/config-metadata.json @@ -0,0 +1,285 @@ +[ + { + "module": "io.helidon.common.configurable", + "types": [ + { + "annotatedType": "io.helidon.common.configurable.ResourceConfig", + "type": "io.helidon.common.configurable.Resource", + "is": true, + "number": 49, + "producers": [ + "io.helidon.common.configurable.ResourceConfig#create(io.helidon.common.config.Config)", + "io.helidon.common.configurable.ResourceConfig#builder()", + "io.helidon.common.configurable.Resource#create(io.helidon.common.configurable.ResourceConfig)" + ], + "options": [ + { + "description": "Resource is located on filesystem.\n\n Path of the resource", + "key": "path", + "method": "io.helidon.common.configurable.ResourceConfig.Builder#path(java.util.Optional)", + "type": "java.nio.file.Path" + }, + { + "description": "Resource is located on classpath.\n\n Classpath location of the resource", + "key": "resource-path", + "method": "io.helidon.common.configurable.ResourceConfig.Builder#resourcePath(java.util.Optional)" + }, + { + "description": "Host of the proxy when using URI.\n\n Proxy host", + "key": "proxy-host", + "method": "io.helidon.common.configurable.ResourceConfig.Builder#proxyHost(java.util.Optional)" + }, + { + "description": "Resource is available on a java.net.URI.\n\n Of the resource\n See proxy()\n See useProxy()", + "key": "uri", + "method": "io.helidon.common.configurable.ResourceConfig.Builder#uri(java.util.Optional)", + "type": "java.net.URI" + }, + { + "defaultValue": "true", + "description": "Whether to use proxy. If set to `false`, proxy will not be used even if configured.\n When set to `true` (default), proxy will be used if configured.\n\n Whether to use proxy if configured", + "key": "use-proxy", + "method": "io.helidon.common.configurable.ResourceConfig.Builder#useProxy(boolean)", + "type": "java.lang.Boolean" + }, + { + "description": "Plain content of the resource (text).\n\n Plain content", + "key": "content-plain", + "method": "io.helidon.common.configurable.ResourceConfig.Builder#contentPlain(java.util.Optional)" + }, + { + "defaultValue": "80", + "description": "Port of the proxy when using URI.\n\n Proxy port", + "key": "proxy-port", + "method": "io.helidon.common.configurable.ResourceConfig.Builder#proxyPort(int)", + "type": "java.lang.Integer" + }, + { + "defaultValue": "", + "description": "Description of this resource when configured through plain text or binary.\n\n Description", + "key": "description", + "method": "io.helidon.common.configurable.ResourceConfig.Builder#description(java.lang.String)" + }, + { + "description": "Binary content of the resource (base64 encoded).\n\n Binary content", + "key": "content", + "method": "io.helidon.common.configurable.ResourceConfig.Builder#content(java.util.Optional)" + } + ] + }, + { + "annotatedType": "io.helidon.common.configurable.AllowListConfig", + "type": "io.helidon.common.configurable.AllowList", + "producers": [ + "io.helidon.common.configurable.AllowListConfig#create(io.helidon.common.config.Config)", + "io.helidon.common.configurable.AllowListConfig#builder()", + "io.helidon.common.configurable.AllowList#create(io.helidon.common.configurable.AllowListConfig)" + ], + "options": [ + { + "defaultValue": "false", + "description": "Allows all strings to match (subject to \"deny\" conditions). An `allow.all` setting of `false` does\n not deny all strings but rather represents the absence of a universal match, meaning that other allow and deny settings\n determine the matching outcomes.\n\n Whether to allow all strings to match (subject to \"deny\" conditions)", + "key": "allow.all", + "method": "io.helidon.common.configurable.AllowListConfig.Builder#allowAll(boolean)", + "type": "java.lang.Boolean" + }, + { + "description": "Patterns specifying strings to allow.\n\n Patterns which allow matching", + "key": "allow.pattern", + "kind": "LIST", + "method": "io.helidon.common.configurable.AllowListConfig.Builder#allowedPatterns(java.util.List)", + "type": "java.util.regex.Pattern" + }, + { + "description": "Suffixes specifying strings to deny.\n\n Suffixes which deny matching", + "key": "deny.suffix", + "kind": "LIST", + "method": "io.helidon.common.configurable.AllowListConfig.Builder#deniedSuffixes(java.util.List)" + }, + { + "description": "Prefixes specifying strings to allow.\n\n Prefixes which allow matching", + "key": "allow.prefix", + "kind": "LIST", + "method": "io.helidon.common.configurable.AllowListConfig.Builder#allowedPrefixes(java.util.List)" + }, + { + "description": "Exact strings to deny.\n\n Exact strings to allow", + "key": "deny.exact", + "kind": "LIST", + "method": "io.helidon.common.configurable.AllowListConfig.Builder#denied(java.util.List)" + }, + { + "description": "Patterns specifying strings to deny.\n\n Patterns which deny matching", + "key": "deny.pattern", + "kind": "LIST", + "method": "io.helidon.common.configurable.AllowListConfig.Builder#deniedPatterns(java.util.List)", + "type": "java.util.regex.Pattern" + }, + { + "description": "Exact strings to allow.\n\n Exact strings to allow", + "key": "allow.exact", + "kind": "LIST", + "method": "io.helidon.common.configurable.AllowListConfig.Builder#allowed(java.util.List)" + }, + { + "description": "Prefixes specifying strings to deny.\n\n Prefixes which deny matching", + "key": "deny.prefix", + "kind": "LIST", + "method": "io.helidon.common.configurable.AllowListConfig.Builder#deniedPrefixes(java.util.List)" + }, + { + "description": "Suffixes specifying strings to allow.\n\n Suffixes which allow matching", + "key": "allow.suffix", + "kind": "LIST", + "method": "io.helidon.common.configurable.AllowListConfig.Builder#allowedSuffixes(java.util.List)" + } + ] + }, + { + "annotatedType": "io.helidon.common.configurable.ThreadPoolConfig", + "type": "io.helidon.common.configurable.ThreadPoolSupplier", + "producers": [ + "io.helidon.common.configurable.ThreadPoolConfig#create(io.helidon.common.config.Config)", + "io.helidon.common.configurable.ThreadPoolConfig#builder()", + "io.helidon.common.configurable.ThreadPoolSupplier#create(io.helidon.common.configurable.ThreadPoolConfig)" + ], + "options": [ + { + "defaultValue": "50", + "description": "Max pool size of the thread pool executor.\n Defaults to DEFAULT_MAX_POOL_SIZE.\n\n MaxPoolSize see java.util.concurrent.ThreadPoolExecutor.getMaximumPoolSize()", + "key": "max-pool-size", + "method": "io.helidon.common.configurable.ThreadPoolConfig.Builder#maxPoolSize(int)", + "type": "java.lang.Integer" + }, + { + "defaultValue": "PT3M", + "description": "Keep alive of the thread pool executor.\n Defaults to DEFAULT_KEEP_ALIVE.\n\n Keep alive see java.util.concurrent.ThreadPoolExecutor.getKeepAliveTime(java.util.concurrent.TimeUnit)", + "key": "keep-alive", + "method": "io.helidon.common.configurable.ThreadPoolConfig.Builder#keepAlive(java.time.Duration)", + "type": "java.time.Duration" + }, + { + "description": "Name prefix for threads in this thread pool executor.\n Defaults to DEFAULT_THREAD_NAME_PREFIX.\n\n Prefix of a thread name", + "key": "thread-name-prefix", + "method": "io.helidon.common.configurable.ThreadPoolConfig.Builder#threadNamePrefix(java.util.Optional)" + }, + { + "defaultValue": "true", + "description": "Whether to prestart core threads in this thread pool executor.\n Defaults to DEFAULT_PRESTART.\n\n Whether to prestart the threads", + "key": "should-prestart", + "method": "io.helidon.common.configurable.ThreadPoolConfig.Builder#shouldPrestart(boolean)", + "type": "java.lang.Boolean" + }, + { + "description": "When configured to `true`, an unbounded virtual executor service (project Loom) will be used.\n

\n If enabled, all other configuration options of this executor service are ignored!\n\n Whether to use virtual threads or not, defaults to `false`", + "key": "virtual-threads", + "method": "io.helidon.common.configurable.ThreadPoolConfig.Builder#virtualThreads(boolean)", + "type": "java.lang.Boolean" + }, + { + "defaultValue": "10", + "description": "Core pool size of the thread pool executor.\n Defaults to DEFAULT_CORE_POOL_SIZE.\n\n CorePoolSize see java.util.concurrent.ThreadPoolExecutor.getCorePoolSize()", + "key": "core-pool-size", + "method": "io.helidon.common.configurable.ThreadPoolConfig.Builder#corePoolSize(int)", + "type": "java.lang.Integer" + }, + { + "description": "Name of this thread pool executor.\n\n The pool name", + "key": "name", + "method": "io.helidon.common.configurable.ThreadPoolConfig.Builder#name(java.util.Optional)" + }, + { + "defaultValue": "true", + "description": "Is daemon of the thread pool executor.\n Defaults to DEFAULT_IS_DAEMON.\n\n Whether the threads are daemon threads", + "key": "is-daemon", + "method": "io.helidon.common.configurable.ThreadPoolConfig.Builder#daemon(boolean)", + "type": "java.lang.Boolean" + }, + { + "defaultValue": "1000", + "description": "The queue size above which pool growth will be considered if the pool is not fixed size.\n Defaults to DEFAULT_GROWTH_THRESHOLD.\n\n The growth threshold", + "key": "growth-threshold", + "method": "io.helidon.common.configurable.ThreadPoolConfig.Builder#growthThreshold(int)", + "type": "java.lang.Integer" + }, + { + "defaultValue": "10000", + "description": "Queue capacity of the thread pool executor.\n Defaults to DEFAULT_QUEUE_CAPACITY.\n\n Capacity of the queue backing the executor", + "key": "queue-capacity", + "method": "io.helidon.common.configurable.ThreadPoolConfig.Builder#queueCapacity(int)", + "type": "java.lang.Integer" + }, + { + "defaultValue": "0", + "description": "The percentage of task submissions that should result in adding threads, expressed as a value from 1 to 100. The\n rate applies only when all of the following are true:\n

    \n
  • the pool size is below the maximum, and
  • \n
  • there are no idle threads, and
  • \n
  • the number of tasks in the queue exceeds the `growthThreshold`
  • \n
\n For example, a rate of 20 means that while these conditions are met one thread will be added for every 5 submitted\n tasks.\n

\n Defaults to DEFAULT_GROWTH_RATE\n\n The growth rate", + "key": "growth-rate", + "method": "io.helidon.common.configurable.ThreadPoolConfig.Builder#growthRate(int)", + "type": "java.lang.Integer" + } + ] + }, + { + "annotatedType": "io.helidon.common.configurable.LruCacheConfig", + "type": "io.helidon.common.configurable.LruCache", + "producers": [ + "io.helidon.common.configurable.LruCacheConfig#create(io.helidon.common.config.Config)", + "io.helidon.common.configurable.LruCacheConfig#builder()", + "io.helidon.common.configurable.LruCache#create(io.helidon.common.configurable.LruCacheConfig)" + ], + "options": [ + { + "defaultValue": "10000", + "description": "Configure capacity of the cache. Defaults to LruCache.DEFAULT_CAPACITY.\n\n Maximal number of records in the cache before the oldest one is removed", + "key": "capacity", + "method": "io.helidon.common.configurable.LruCacheConfig.Builder#capacity(int)", + "type": "java.lang.Integer" + } + ] + }, + { + "annotatedType": "io.helidon.common.configurable.ScheduledThreadPoolConfig", + "type": "io.helidon.common.configurable.ScheduledThreadPoolSupplier", + "producers": [ + "io.helidon.common.configurable.ScheduledThreadPoolConfig#create(io.helidon.common.config.Config)", + "io.helidon.common.configurable.ScheduledThreadPoolConfig#builder()", + "io.helidon.common.configurable.ScheduledThreadPoolSupplier#create(io.helidon.common.configurable.ScheduledThreadPoolConfig)" + ], + "options": [ + { + "defaultValue": "false", + "description": "Whether to prestart core threads in this thread pool executor.\n Defaults to DEFAULT_PRESTART.\n\n Whether to prestart the threads", + "key": "prestart", + "method": "io.helidon.common.configurable.ScheduledThreadPoolConfig.Builder#prestart(boolean)", + "type": "java.lang.Boolean" + }, + { + "defaultValue": "helidon-", + "description": "Name prefix for threads in this thread pool executor.\n Defaults to DEFAULT_THREAD_NAME_PREFIX.\n\n Prefix of a thread name", + "key": "thread-name-prefix", + "method": "io.helidon.common.configurable.ScheduledThreadPoolConfig.Builder#threadNamePrefix(java.lang.String)" + }, + { + "description": "When configured to `true`, an unbounded virtual executor service (project Loom) will be used.\n

\n If enabled, all other configuration options of this executor service are ignored!\n\n Whether to use virtual threads or not, defaults to `false`", + "key": "virtual-threads", + "method": "io.helidon.common.configurable.ScheduledThreadPoolConfig.Builder#virtualThreads(boolean)", + "type": "java.lang.Boolean" + }, + { + "defaultValue": "16", + "description": "Core pool size of the thread pool executor.\n Defaults to DEFAULT_CORE_POOL_SIZE.\n\n CorePoolSize see java.util.concurrent.ThreadPoolExecutor.getCorePoolSize()", + "key": "core-pool-size", + "method": "io.helidon.common.configurable.ScheduledThreadPoolConfig.Builder#corePoolSize(int)", + "type": "java.lang.Integer" + }, + { + "defaultValue": "true", + "description": "Is daemon of the thread pool executor.\n Defaults to DEFAULT_IS_DAEMON.\n\n Whether the threads are daemon threads", + "key": "is-daemon", + "method": "io.helidon.common.configurable.ScheduledThreadPoolConfig.Builder#daemon(boolean)", + "type": "java.lang.Boolean" + } + ] + } + ] + } +] \ No newline at end of file diff --git a/metadata/hson/src/test/resources/json-features.json b/metadata/hson/src/test/resources/json-features.json new file mode 100644 index 00000000000..ba891d1207b --- /dev/null +++ b/metadata/hson/src/test/resources/json-features.json @@ -0,0 +1,36 @@ +{ + "nulls": { + "field": null + }, + "array": [ + true, + null, + "string", + 14, + { + "object": "value" + }, + [ + "first", + "second" + ] + ], + "escapes": { + "newline": "a\nb", + "quotes": "a\"b", + "backslash": "a\\b", + "slash": "a\/b", + "backspace": "a\bb", + "formfeed": "a\fb", + "cr": "a\rb", + "tab": "a\tb", + "unicode": "a\u0048b" + }, + "numbers": { + "number1": 1, + "number2": 1.5, + "number3": 1.5e2, + "number4": 1.5e-2, + "number5": 1.5e+2 + } +} diff --git a/metadata/hson/src/test/resources/reflection-config.json b/metadata/hson/src/test/resources/reflection-config.json new file mode 100644 index 00000000000..79f3b98cbbd --- /dev/null +++ b/metadata/hson/src/test/resources/reflection-config.json @@ -0,0 +1,26 @@ +{ + "annotated":[ + "jakarta.persistence.Entity", + "jakarta.persistence.MappedSuperclass" + ], + "class-hierarchy": [ + "javax.xml.stream.XMLEventFactory", + "javax.sql.DataSource", + "javax.sql.XADataSource", + "javax.sql.XAConnection", + "jakarta.transaction.xa.XAResource", + "java.sql.Connection", + "java.sql.Statement", + "java.sql.ResultSet", + "jakarta.transaction.TransactionManager", + "jakarta.transaction.Transaction", + "jakarta.persistence.EntityManager", + "jakarta.transaction.TransactionSynchronizationRegistry" + ], + "classes": [ + "com.sun.xml.internal.stream.events.XMLEventFactoryImpl", + "java.net.URLClassLoader", + "java.sql.Statement[]" + ], + "exclude": [] +} diff --git a/metadata/hson/src/test/resources/service-registry.json b/metadata/hson/src/test/resources/service-registry.json new file mode 100644 index 00000000000..8281ae894ad --- /dev/null +++ b/metadata/hson/src/test/resources/service-registry.json @@ -0,0 +1,29 @@ +{ + "generated": { + "trigger": "io.helidon.service.codegen.ServiceRegistryCodegenExtension", + "value": "io.helidon.service.codegen.ServiceRegistryCodegenExtension", + "version": "1", + "comments": "Service descriptors in module unnamed/io.helidon.examples.quickstart.se" + }, + "services": [ + { + "version": 1.0, + "type": "inject", + "descriptor": "io.helidon.examples.quickstart.se.GreetEndpoint__HttpFeature__ServiceDescriptor", + "weight": 100.0, + "contracts": [ + "io.helidon.examples.quickstart.se.GreetEndpoint__HttpFeature", + "io.helidon.webserver.http.HttpFeature", + "io.helidon.webserver.ServerLifecycle", + "java.util.function.Supplier" + ] + }, + { + "type": "inject", + "descriptor": "io.helidon.examples.quickstart.se.GreetEndpoint__ServiceDescriptor", + "contracts": [ + "io.helidon.examples.quickstart.se.GreetEndpoint" + ] + } + ] +} \ No newline at end of file diff --git a/metadata/pom.xml b/metadata/pom.xml new file mode 100644 index 00000000000..0926ca062d2 --- /dev/null +++ b/metadata/pom.xml @@ -0,0 +1,58 @@ + + + + + 4.0.0 + + io.helidon + helidon-project + 4.1.0-SNAPSHOT + + pom + + io.helidon.metadata + helidon-metadata-project + Helidon Metadata Project + + Tools for metadata handling in Helidon. + + + + true + + + + hson + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + check-dependencies + verify + + + + + + diff --git a/pom.xml b/pom.xml index f7551ecd10d..dcb99a6d96e 100644 --- a/pom.xml +++ b/pom.xml @@ -226,6 +226,7 @@ websocket codegen service + metadata