diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 18ebaf4..e40a174 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -1,6 +1,8 @@ + \ No newline at end of file diff --git a/elementfactory/src/main/java/io/github/xmljim/json/elementfactory/ElementFactoryImpl.java b/elementfactory/src/main/java/io/github/xmljim/json/elementfactory/ElementFactoryImpl.java index 282d74f..79a1353 100644 --- a/elementfactory/src/main/java/io/github/xmljim/json/elementfactory/ElementFactoryImpl.java +++ b/elementfactory/src/main/java/io/github/xmljim/json/elementfactory/ElementFactoryImpl.java @@ -60,7 +60,7 @@ public JsonValue newValue(T value, JsonElement parent) { @SuppressWarnings("unchecked") private JsonValue mapToValue(T value) { if (mapperProviderAvailable) { - return (JsonValue) mapperFactory.newBuilder().build().toValue(value); + return (JsonValue) mapperFactory.newMapper().toValue(value); } else { throwUnsupportedError(value.getClass()); } diff --git a/elementfactory/src/main/java/io/github/xmljim/json/elementfactory/JsonArrayImpl.java b/elementfactory/src/main/java/io/github/xmljim/json/elementfactory/JsonArrayImpl.java index e601e4a..fe78651 100644 --- a/elementfactory/src/main/java/io/github/xmljim/json/elementfactory/JsonArrayImpl.java +++ b/elementfactory/src/main/java/io/github/xmljim/json/elementfactory/JsonArrayImpl.java @@ -129,7 +129,7 @@ public boolean equals(Object o) { if (!(o instanceof JsonArrayImpl jsonArray)) { return false; } - + return Objects.equals(valueList, jsonArray.valueList); } @@ -183,6 +183,11 @@ public String prettyPrint(int indent) { return builder.toString(); } + @Override + public int hashCode() { + return Objects.hash(this.valueList); + } + public String toString() { return toJsonString(); } diff --git a/elementfactory/src/main/java/io/github/xmljim/json/elementfactory/JsonObjectImpl.java b/elementfactory/src/main/java/io/github/xmljim/json/elementfactory/JsonObjectImpl.java index c4ed8ad..3b0ae94 100644 --- a/elementfactory/src/main/java/io/github/xmljim/json/elementfactory/JsonObjectImpl.java +++ b/elementfactory/src/main/java/io/github/xmljim/json/elementfactory/JsonObjectImpl.java @@ -117,7 +117,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(data, parent()); + return Objects.hash(data); } @Override diff --git a/json-api/README.md b/json-api/README.md new file mode 100644 index 0000000..bc4d626 --- /dev/null +++ b/json-api/README.md @@ -0,0 +1,5 @@ +# JSON API + +The Json API combines all the reference implementation services into a single API layer for use as single library. + +Each "service" is exposed as static class instances that expose methods provided by that service. \ No newline at end of file diff --git a/json-api/pom.xml b/json-api/pom.xml new file mode 100644 index 0000000..18fe787 --- /dev/null +++ b/json-api/pom.xml @@ -0,0 +1,62 @@ + + + + json-library + io.github.xmljim.json + 1.0.2 + + 4.0.0 + + json-api + + + + + io.github.xmljim.json + json-parser + ${project.version} + + + io.github.xmljim.json + elementfactory + ${project.version} + + + io.github.xmljim.json + json-merger + ${project.version} + + + io.github.xmljim.json + json-mapper + ${project.version} + + + io.github.xmljim.json + jsonpath + ${project.version} + compile + + + + + + + org.jacoco + jacoco-maven-plugin + + + report + verify + + report + + + + + + + + \ No newline at end of file diff --git a/json-api/src/main/java/io/github/xmljim/json/api/JsonApi.java b/json-api/src/main/java/io/github/xmljim/json/api/JsonApi.java new file mode 100644 index 0000000..8566cff --- /dev/null +++ b/json-api/src/main/java/io/github/xmljim/json/api/JsonApi.java @@ -0,0 +1,460 @@ +package io.github.xmljim.json.api; + +import io.github.xmljim.json.factory.jsonpath.JsonPathBuilder; +import io.github.xmljim.json.factory.jsonpath.JsonPathFactory; +import io.github.xmljim.json.factory.jsonpath.ResultType; +import io.github.xmljim.json.factory.mapper.MapperFactory; +import io.github.xmljim.json.factory.mapper.MappingConfig; +import io.github.xmljim.json.factory.mapper.MappingParserConfig; +import io.github.xmljim.json.factory.mapper.parser.MappingParser; +import io.github.xmljim.json.factory.merge.MergeFactory; +import io.github.xmljim.json.factory.merge.strategy.ArrayConflictStrategy; +import io.github.xmljim.json.factory.merge.strategy.MergeResultStrategy; +import io.github.xmljim.json.factory.merge.strategy.ObjectConflictStrategy; +import io.github.xmljim.json.factory.model.ElementFactory; +import io.github.xmljim.json.factory.parser.InputData; +import io.github.xmljim.json.factory.parser.JsonParserException; +import io.github.xmljim.json.factory.parser.ParserBuilder; +import io.github.xmljim.json.factory.parser.ParserFactory; +import io.github.xmljim.json.model.JsonArray; +import io.github.xmljim.json.model.JsonNode; +import io.github.xmljim.json.model.JsonObject; +import io.github.xmljim.json.model.JsonValue; +import io.github.xmljim.json.service.ServiceManager; + +import java.io.InputStream; +import java.io.Reader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +public final class JsonApi { + + /** + * JsonPath ({@link JsonPathFactory}) API functionality + */ + public static final JsonPathApi JsonPath = new JsonPathApi(); + + /** + * JsonParser ({@link ParserFactory}) API functionality + */ + public static final JsonParserApi JsonParser = new JsonParserApi(); + + /** + * JsonElement ({@link ElementFactory}) functionality + */ + public static final ElementApi JsonElement = new ElementApi(); + + /** + * JsonMerge ({@link MergeFactory}) functionality + */ + public static final JsonMergeApi JsonMerge = new JsonMergeApi(); + + /** + * JsonMapper ({@link MapperFactory}) functionality + */ + public static final JsonMapperApi JsonMapper = new JsonMapperApi(); + + /** + * Private constructor, use static fields instead + */ + private JsonApi() { + //private constructor + } + + /** + * API for JsonPath functionality + */ + public static final class JsonPathApi { + private final JsonPathFactory jsonPathFactory = ServiceManager.getProvider(JsonPathFactory.class); + + private JsonPathApi() { + //no-op + } + + /** + * Return the underlying JsonPathBuilder to modify settings + * for a new {@link io.github.xmljim.json.factory.jsonpath.JsonPath} instance + * + * @return a new JsonPathBuilder + */ + public JsonPathBuilder getBuilder() { + return jsonPathFactory.newJsonPathBuilder(); + } + + /** + * Return a JsonArray of values selected from a JsonPath expression. + * This uses the default settings and properties for a JsonPath instance. + * If you need to modify settings (e.g., properties, or set variables), + * use {@link #getBuilder()} to apply these settings and return a + * {@link io.github.xmljim.json.factory.jsonpath.JsonPath} instance + * + *

This is the syntactic equivalent of:

+ *
+         *     JsonPathFactory jsonPathFactory = ServiceManager.getProvider(JsonPathFactory.class);
+         *     JsonPath jsonPath = jsonPathFactory.newJsonPath();
+         *     JsonArray result = jsonPath.select(node, pathExpression);
+         * 
+ * + * @param node The context node that will be used to evaluate and select values + * @param pathExpression the JsonPath expression to query the context node + * @return a JsonArray of values that represent the select expression + */ + public JsonArray select(JsonNode node, String pathExpression) { + return jsonPathFactory.newJsonPath().select(node, pathExpression); + } + + /** + * Return a List of values selected from a JsonPath expression. + * This uses the default settings and properties for a JsonPath instance. + * If you need to modify settings (e.g., properties, or set variables), + * use {@link #getBuilder()} to apply these settings and return a + * {@link io.github.xmljim.json.factory.jsonpath.JsonPath} instance + * + *

This is the syntactic equivalent of:

+ *
+         *     JsonPathFactory jsonPathFactory = ServiceManager.getProvider(JsonPathFactory.class);
+         *     JsonPath jsonPath = jsonPathFactory.newJsonPath();
+         *     JsonArray result = jsonPath.select(node, pathExpression);
+         *
+         *     MapperFactory mapperFactory = ServiceManager.getProvider(MapperFactory.class);
+         *     Mapper mapper = mapperFactory.newMapper();
+         *     return mapper.toList(result);
+         * 
+ * + * @param node The context node that will be used to evaluate and select values + * @param pathExpression the JsonPath expression to query the context node + * @return a JsonArray of values that represent the select expression + */ + @SuppressWarnings("unchecked") + public List selectList(JsonNode node, String pathExpression) { + JsonMapperApi jsonMapperApi = new JsonMapperApi(); + return (List) jsonMapperApi.toList(select(node, pathExpression)); + } + + /** + * Return a JsonArray of paths selected from a JsonPath expression. + * This uses the default settings and properties for a JsonPath instance. + * If you need to modify settings (e.g., properties, or set variables), + * use {@link #getBuilder()} to apply these settings and return a + * {@link io.github.xmljim.json.factory.jsonpath.JsonPath} instance + * + *

This is the syntactic equivalent of:

+ *
+         *     JsonPathFactory jsonPathFactory = ServiceManager.getProvider(JsonPathFactory.class);
+         *     JsonPath jsonPath = jsonPathFactory.newJsonPath();
+         *     JsonArray result = jsonPath.select(node, pathExpression, ResultType.PATH);
+         * 
+ * + * @param node The context node that will be used to evaluate and select values + * @param pathExpression the JsonPath expression to query the context node + * @return a JsonArray of normalized Json paths to each selected item + */ + public JsonArray selectPath(JsonNode node, String pathExpression) { + return jsonPathFactory.newJsonPath().select(node, pathExpression, ResultType.PATH); + } + + @SuppressWarnings("unchecked") + public List selectPathList(JsonNode node, String pathExpression) { + JsonMapperApi jsonMapperApi = new JsonMapperApi(); + return (List) jsonMapperApi.toList(select(node, pathExpression)); + } + + public T selectValue(JsonNode node, String pathExpression) { + return jsonPathFactory.newJsonPath().selectValue(node, pathExpression); + } + } + + /** + * Parser API + */ + public static final class JsonParserApi { + private final ParserFactory parserFactory = ServiceManager.getProvider(ParserFactory.class); + + private JsonParserApi() { + //no-op + } + + /** + * Parse a JSON String + * + * @param jsonString the JSON String + * @param The JsonNode type (either a {@link JsonArray} or {@link JsonObject}) + * @return a new JsonNode type + */ + public T parse(String jsonString) { + return parserFactory.newParser().parse(InputData.of(jsonString)); + } + + /** + * Parse a JSON String + * + * @param path the path to the Json data + * @param The JsonNode type (either a {@link JsonArray} or {@link JsonObject}) + * @return a new JsonNode type + */ + public T parse(Path path) { + if (path.getFileSystem().isOpen()) { + if (!Files.exists(path)) { + throw new JsonParserException("File does not exist: " + path.getFileName().toString()); + } + } + return parserFactory.newParser().parse(InputData.of(path)); + } + + /** + * Parse a JSON String + * + * @param inputStream the InputStream to the Json data + * @param The JsonNode type (either a {@link JsonArray} or {@link JsonObject}) + * @return a new JsonNode type + */ + public T parse(InputStream inputStream) { + return parserFactory.newParser().parse(InputData.of(inputStream)); + } + + /** + * Parse a JSON String + * + * @param reader the Reader to the Json data + * @param The JsonNode type (either a {@link JsonArray} or {@link JsonObject}) + * @return a new JsonNode type + */ + public T parse(Reader reader) { + return parserFactory.newParser().parse(InputData.of(reader)); + } + + /** + * Return the ParserBuilder to configure a new Parser + * + * @return the ParserBuilder + */ + public ParserBuilder getParserBuilder() { + return parserFactory.newParserBuilder(); + } + + public T parse(InputData inputData, Class targetClass) { + MappingParser mappingParser = JsonMapper.newMappingParser(); + return mappingParser.parse(inputData, targetClass); + } + + public T parse(MappingParserConfig mappingParserConfig, InputData inputData, Class targetClass) { + MappingParser mappingParser = JsonMapper.newMappingParser(mappingParserConfig); + return mappingParser.parse(inputData, targetClass); + } + } + + /** + * Json Element API + */ + public static final class ElementApi { + private final ElementFactory elementFactory = ServiceManager.getProvider(ElementFactory.class); + + private ElementApi() { + //no-op + } + + /** + * Create a new, empty JSONObject instance + * + * @return a new empty JSONObject instance + */ + public JsonObject newObject() { + return elementFactory.newObject(); + } + + /** + * Create a new, empty JSONArray instance + * + * @return a new empty JSONArray instance + */ + public JsonArray newArray() { + return elementFactory.newArray(); + } + + /** + * Create a new JsonValue instance + * + * @param value the raw value + * @param the value type + * @return the new JsonValue + */ + public JsonValue newValue(T value) { + return elementFactory.newValue(value); + } + } + + /** + * The JsonMerge API + */ + public static final class JsonMergeApi { + private final MergeFactory mergeFactory = ServiceManager.getProvider(MergeFactory.class); + + private JsonMergeApi() { + // no-op + } + + /** + * Merge two JsonNodes, using the default configuration (i.e., conflict strategies) + * + * @param primary the primary node + * @param secondary the secondary node + * @param The JsonNode type + * @return The merged JsonNode + */ + public T merge(T primary, T secondary) { + return mergeFactory.newMergeProcessor().merge(primary, secondary); + } + + /** + * Merge two JsonNodes, specifying the ArrayConflictStrategy and ObjectConflict Strategy + * + * @param primary the primary node + * @param secondary the secondary node + * @param arrayConflictStrategy the conflict strategy to apply for JsonArrays + * @param objectConflictStrategy The conflict strategy to apply for JsonObjects + * @param the node type + * @return The merged JsonNode + */ + public T merge(T primary, T secondary, ArrayConflictStrategy arrayConflictStrategy, + ObjectConflictStrategy objectConflictStrategy) { + return mergeFactory.newMergeBuilder() + .setArrayConflictStrategy(arrayConflictStrategy) + .setObjectConflictStrategy(objectConflictStrategy) + .build() + .merge(primary, secondary); + } + + /** + * Merge two JsonNodes, specifying the ArrayConflictStrategy and ObjectConflict Strategy + * + * @param primary the primary node + * @param secondary the secondary node + * @param arrayConflictStrategy the conflict strategy to apply for JsonArrays + * @param objectConflictStrategy The conflict strategy to apply for JsonObjects + * @param mergeResultStrategy the merge result strategy - Here there be dragons. KNOW what you're doing + * @param mergeAppendKey the key value to append to a given JsonObject key for Append conflicts + * @param the node type + * @return The merged JsonNode + */ + public T merge(T primary, T secondary, ArrayConflictStrategy arrayConflictStrategy, + ObjectConflictStrategy objectConflictStrategy, + MergeResultStrategy mergeResultStrategy, + String mergeAppendKey) { + + return mergeFactory.newMergeBuilder() + .setArrayConflictStrategy(arrayConflictStrategy) + .setObjectConflictStrategy(objectConflictStrategy) + .setMergeAppendKey(mergeAppendKey) + .setMergeResultStrategy(mergeResultStrategy) + .build() + .merge(primary, secondary); + } + } + + /** + * JsonMapper API + */ + public static final class JsonMapperApi { + private final MapperFactory mapperFactory = ServiceManager.getProvider(MapperFactory.class); + + private JsonMapperApi() { + //no-op + } + + /** + * Convert an object into a JsonObject + * + * @param object the object instance + * @return the JsonObject + */ + public JsonObject toJsonObject(Object object) { + return mapperFactory.newMapper().toJson(object); + } + + /** + * Convert a map into a JsonObject + * + * @param objectMap the map instance + * @return a new JsonObject + */ + public JsonObject toJsonObject(Map objectMap) { + return mapperFactory.newMapper().toJson(objectMap); + } + + /** + * Convert a collection to a JsonArray + * + * @param collection the collection + * @return a new JsonArray + */ + public JsonArray toJsonArray(Collection collection) { + return mapperFactory.newMapper().toJson(collection); + } + + /** + * Convert a JsonObject to a class instance + * + * @param jsonObject The JsonObject + * @param targetClass the target class, should be concrete class + * @param the class type or a super type or interface + * @return the converted class + */ + public T toClass(JsonObject jsonObject, Class targetClass) { + return mapperFactory.newMapper().toClass(jsonObject, targetClass); + } + + public T toClass(MappingConfig mappingConfig, JsonObject jsonObject, Class targetClass) { + return mapperFactory.newMapper(mappingConfig).toClass(jsonObject, targetClass); + } + + public JsonObject toJson(T instance) { + return mapperFactory.newMapper().toJson(instance); + } + + public JsonObject toJson(MappingConfig mappingConfig, T instance) { + return mapperFactory.newMapper(mappingConfig).toJson(instance); + } + + /** + * Convert a JsonObject to a Map + * + * @param jsonObject the JsonObject + * @return a new Map instance + */ + public Map toMap(JsonObject jsonObject) { + return mapperFactory.newMapper().toMap(jsonObject); + } + + /** + * Convert a JsonArray to a List + * + * @param jsonArray the JsonArray + * @return the new List instance + */ + public List toList(JsonArray jsonArray) { + return mapperFactory.newMapper().toList(jsonArray); + } + + /** + * Convert a raw value to a JsonValue + * + * @param value the value to convert + * @return a new JsonValue + */ + public JsonValue toValue(Object value) { + return mapperFactory.newMapper().toValue(value); + } + + MappingParser newMappingParser() { + return mapperFactory.newMappingParser(); + } + + MappingParser newMappingParser(MappingParserConfig mappingParserConfig) { + return mapperFactory.newMappingParser(mappingParserConfig); + } + + } +} diff --git a/json-api/src/main/java/module-info.java b/json-api/src/main/java/module-info.java new file mode 100644 index 0000000..16f3deb --- /dev/null +++ b/json-api/src/main/java/module-info.java @@ -0,0 +1,38 @@ +import io.github.xmljim.json.factory.jsonpath.JsonPathFactory; +import io.github.xmljim.json.factory.mapper.*; +import io.github.xmljim.json.factory.merge.MergeFactory; +import io.github.xmljim.json.factory.model.ElementFactory; +import io.github.xmljim.json.factory.parser.ParserFactory; + +module io.github.xmljim.json.api { + + requires transitive io.github.xmljim.json.elementfactory; + requires transitive io.github.xmljim.json.parser; + requires transitive io.github.xmljim.json.merger; + requires transitive io.github.xmljim.json.mapper; + requires transitive io.github.xmljim.json.jsonpath; + + // ** Services Consumed * + + // Element Factory: for creating new model elements + uses ElementFactory; + + //Mapper Factory: for marshalling and unmarshalling Json and Java objects + uses MapperFactory; + //Builder for creating MappingConfig instances used by a Mapper + uses MappingConfig.Builder; + //Builder for creating ClassConfig instances used in a MappingConfig + uses ClassConfig.Builder; + //Builder for creating MemberConfig instances used in a ClassConfig + uses MemberConfig.Builder; + //Builder for creating a MapperParserConfig used by the MapperParser + uses MappingParserConfig.Builder; + //Parser Factory: for creating a new JsonParser + uses ParserFactory; + //JsonPath Factory: for creating JsonPath instances + uses JsonPathFactory; + //Merge Factory: for creating Merger instances + uses MergeFactory; + + exports io.github.xmljim.json.api; +} \ No newline at end of file diff --git a/json-api/src/test/java/io/github/xmljim/json/api/test/JsonApiTests.java b/json-api/src/test/java/io/github/xmljim/json/api/test/JsonApiTests.java new file mode 100644 index 0000000..f52cbe2 --- /dev/null +++ b/json-api/src/test/java/io/github/xmljim/json/api/test/JsonApiTests.java @@ -0,0 +1,154 @@ +package io.github.xmljim.json.api.test; + +import io.github.xmljim.json.model.JsonArray; +import io.github.xmljim.json.model.JsonObject; +import io.github.xmljim.json.model.JsonValue; +import io.github.xmljim.json.model.NodeType; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.io.InputStream; + +import static io.github.xmljim.json.api.JsonApi.JsonElement; +import static io.github.xmljim.json.api.JsonApi.JsonParser; +import static org.junit.jupiter.api.Assertions.*; + +public class JsonApiTests { + + @Test + @DisplayName("JsonElement - New Array") + void testCreateNewArray() { + JsonArray array = JsonElement.newArray(); + assertNotNull(array); + assertEquals(NodeType.ARRAY, array.type()); + assertTrue(array.type().isArray()); + } + + @Test + @DisplayName("JsonElement - New Object") + void testCreateNewObject() { + JsonObject object = JsonElement.newObject(); + assertNotNull(object); + assertEquals(NodeType.OBJECT, object.type()); + assertTrue(object.type().isObject()); + } + + @Test + @DisplayName("JsonElement - String Value") + void testCreateStringValue() { + String value = "TestString"; + JsonValue stringJsonValue = JsonElement.newValue(value); + assertNotNull(stringJsonValue); + assertEquals(NodeType.STRING, stringJsonValue.type()); + assertEquals(value, stringJsonValue.get()); + assertTrue(stringJsonValue.type().isPrimitive()); + } + + @Test + @DisplayName("JsonElement - Boolean Value") + void testCreateBooleanValue() { + boolean value = true; + JsonValue booleanJsonValue = JsonElement.newValue(value); + assertNotNull(booleanJsonValue); + assertEquals(NodeType.BOOLEAN, booleanJsonValue.type()); + assertEquals(value, booleanJsonValue.get()); + assertTrue(booleanJsonValue.type().isPrimitive()); + } + + @Test + @DisplayName("JsonElement - Long Value") + void testCreateLongValue() { + Long value = 1L; + JsonValue jsonValue = JsonElement.newValue(value); + assertNotNull(jsonValue); + assertEquals(NodeType.LONG, jsonValue.type()); + assertEquals(value, jsonValue.get()); + assertTrue(jsonValue.type().isPrimitive()); + assertTrue(jsonValue.type().isNumeric()); + } + + @Test + @DisplayName("JsonElement - Integer Value") + void testCreateIntegerValue() { + int value = 1; + JsonValue jsonValue = JsonElement.newValue(value); + assertNotNull(jsonValue); + assertEquals(NodeType.INTEGER, jsonValue.type()); + assertEquals(value, jsonValue.get()); + assertTrue(jsonValue.type().isPrimitive()); + assertTrue(jsonValue.type().isNumeric()); + } + + @Test + @DisplayName("JsonElement - Double Value") + void testCreateDoubleValue() { + double value = 3.1415926; + JsonValue jsonValue = JsonElement.newValue(value); + assertNotNull(jsonValue); + assertEquals(NodeType.DOUBLE, jsonValue.type()); + assertEquals(value, jsonValue.get()); + assertTrue(jsonValue.type().isPrimitive()); + assertTrue(jsonValue.type().isNumeric()); + } + + @Test + @DisplayName("JsonElement - Null Value") + void testCreateNullValue() { + Object value = null; + JsonValue jsonValue = JsonElement.newValue(value); + assertNotNull(jsonValue); + assertEquals(NodeType.NULL, jsonValue.type()); + assertNull(jsonValue.get()); + assertTrue(jsonValue.type().isPrimitive()); + } + + @Test + @DisplayName("JsonElement - JsonArray Value") + void testCreateArrayValue() { + JsonArray array = JsonElement.newArray(); + JsonValue jsonValue = JsonElement.newValue(array); + assertNotNull(jsonValue); + assertEquals(NodeType.ARRAY, jsonValue.type()); + assertEquals(array, jsonValue.get()); + assertTrue(jsonValue.type().isArray()); + } + + @Test + @DisplayName("JsonElement - JsonObject Value") + void testCreateObjectValue() { + JsonObject value = JsonElement.newObject(); + JsonValue jsonValue = JsonElement.newValue(value); + assertNotNull(jsonValue); + assertEquals(NodeType.OBJECT, jsonValue.type()); + assertEquals(value, jsonValue.get()); + assertTrue(jsonValue.type().isObject()); + } + + @Test + @DisplayName("JsonParser - Parse String") + void testParserString() { + String jsonString = """ + {"a": 1, "b": true, "c": null, "d": {"foo": "bar"}, "e": [1, true, null, {"foo": "bar"}, [3.14, 1.62]]} + """; + + JsonObject jsonObject = JsonParser.parse(jsonString); + + assertNotNull(jsonObject); + assertEquals(1, (long) jsonObject.get("a")); + assertTrue((boolean) jsonObject.get("b")); + } + + @Test + @DisplayName("JsonParser - Parse InputStream") + void testParserInputStream() { + try (InputStream inputStream = getClass().getResourceAsStream("/test1.json")) { + JsonObject jsonObject = JsonParser.parse(inputStream); + assertNotNull(jsonObject); + assertEquals(1, (long) jsonObject.get("a")); + assertTrue((boolean) jsonObject.get("b")); + } catch (IOException ioException) { + fail(); + } + } +} diff --git a/json-api/src/test/resources/test1.json b/json-api/src/test/resources/test1.json new file mode 100644 index 0000000..3058b85 --- /dev/null +++ b/json-api/src/test/resources/test1.json @@ -0,0 +1,20 @@ +{ + "a": 1, + "b": true, + "c": null, + "d": { + "foo": "bar" + }, + "e": [ + 1, + true, + null, + { + "foo": "bar" + }, + [ + 3.14, + 1.62 + ] + ] +} \ No newline at end of file diff --git a/json-logging/pom.xml b/json-logging/pom.xml new file mode 100644 index 0000000..4f030c5 --- /dev/null +++ b/json-logging/pom.xml @@ -0,0 +1,19 @@ + + + + json-library + io.github.xmljim.json + 1.0.2 + + 4.0.0 + + json-logging + + + 17 + 17 + + + \ No newline at end of file diff --git a/json-test-coverage/pom.xml b/json-test-coverage/pom.xml new file mode 100644 index 0000000..4570728 --- /dev/null +++ b/json-test-coverage/pom.xml @@ -0,0 +1,65 @@ + + + + json-library + io.github.xmljim.json + 1.0.2 + + 4.0.0 + pom + + json-test-coverage + + + 17 + 17 + + + + + io.github.xmljim.json + elementfactory + ${project.version} + + + io.github.xmljim.json + jsonpath + ${project.version} + + + io.github.xmljim.json + json-parser + ${project.version} + + + io.github.xmljim.json + json-mapper + ${project.version} + + + io.github.xmljim.json + json-merger + ${project.version} + + + + + + + org.jacoco + jacoco-maven-plugin + + + report-aggregate + verify + + report-aggregate + + + + + + + \ No newline at end of file diff --git a/jsonfactory/README.md b/jsonfactory/README.md index e55226c..0e0d999 100644 --- a/jsonfactory/README.md +++ b/jsonfactory/README.md @@ -7,6 +7,18 @@ All services typically follow a *Factory* design pattern and all extend the `Jso example, the `ElementFactory` interface extends `JsonService` and provides methods for creating concrete instances of the JSON Model. +## JsonService + +The `JsonService` interface is nothing more than a tagging interface that identifies that any interface that extends +it _is_ intended to be a service to be consumed by another component. Internally, the `ServiceManager` scans +the module path for all `JsonService` instances. + +## @JsonService Annotation + +The `@JsonService` annotation is applied to `JsonService` interface implementation classes. In addition to tagging +the implementation class as a Service Provider, it contains other metadata that facilitate in helping the +Service Manager select the appropriate implementation class, if more than one exists. + ## ServiceManager The `ServiceManager` class provides static methods for instantiating *Service Providers*. Service diff --git a/jsonfactory/src/main/java/io/github/xmljim/json/factory/config/AbstractConfiguration.java b/jsonfactory/src/main/java/io/github/xmljim/json/factory/config/AbstractConfiguration.java new file mode 100644 index 0000000..5967ea3 --- /dev/null +++ b/jsonfactory/src/main/java/io/github/xmljim/json/factory/config/AbstractConfiguration.java @@ -0,0 +1,63 @@ +package io.github.xmljim.json.factory.config; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; + +public abstract class AbstractConfiguration implements Configuration { + private final Map configurationMap = new HashMap<>(); + private boolean immutable = true; + + @Override + public boolean put(ConfigKey configKey, V value) { + if (isImmutable()) { + return putIfAbsent(configKey, value); + } else { + configurationMap.put(configKey, ConfigurationEntry.of(configKey, value)); + return true; + } + } + + @Override + public boolean putIfAbsent(ConfigKey configKey, V value) { + @SuppressWarnings("unchecked") + V val = (V) configurationMap.putIfAbsent(configKey, ConfigurationEntry.of(configKey, value)); + return val == null; + } + + @Override + @SuppressWarnings("unchecked") + public Optional getOptional(ConfigKey configKey) { + ConfigurationEntry entry = configurationMap.getOrDefault(configKey, null); + if (entry != null) { + return Optional.of(entry.getValue()); + } + return Optional.empty(); + } + + @Override + public void setImmutable(boolean immutable) { + this.immutable = immutable; + } + + @Override + public boolean isImmutable() { + return immutable; + } + + @Override + public boolean containsKey(ConfigKey configKey) { + return configurationMap.containsKey(configKey); + } + + @Override + public boolean containsValue(V value) { + return configurationMap.containsValue(value); + } + + @Override + public Stream entries() { + return configurationMap.values().stream(); + } +} diff --git a/jsonfactory/src/main/java/io/github/xmljim/json/factory/config/ConfigKey.java b/jsonfactory/src/main/java/io/github/xmljim/json/factory/config/ConfigKey.java new file mode 100644 index 0000000..5d6dd6a --- /dev/null +++ b/jsonfactory/src/main/java/io/github/xmljim/json/factory/config/ConfigKey.java @@ -0,0 +1,43 @@ +package io.github.xmljim.json.factory.config; + +import java.util.Objects; + +public final class ConfigKey { + private Object key; + + private ConfigKey(T key) { + this.key = key; + } + + public static ConfigKey of(T key) { + Objects.requireNonNull(key, "Key value cannot be null"); + return new ConfigKey(key); + } + + @SuppressWarnings("unchecked") + public T get() { + return (T) key; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ConfigKey configKey = (ConfigKey) o; + return key.equals(configKey.key); + } + + @Override + public int hashCode() { + return Objects.hash(key); + } + + @Override + public String toString() { + return key.toString(); + } +} diff --git a/jsonfactory/src/main/java/io/github/xmljim/json/factory/config/Configuration.java b/jsonfactory/src/main/java/io/github/xmljim/json/factory/config/Configuration.java new file mode 100644 index 0000000..671f110 --- /dev/null +++ b/jsonfactory/src/main/java/io/github/xmljim/json/factory/config/Configuration.java @@ -0,0 +1,72 @@ +package io.github.xmljim.json.factory.config; + +import java.util.Optional; +import java.util.stream.Stream; + +public interface Configuration { + + boolean put(ConfigKey configKey, V value); + + default boolean put(K key, V value) { + return put(ConfigKey.of(key), value); + } + + boolean putIfAbsent(ConfigKey configKey, V value); + + default boolean putIfAbsent(K key, V value) { + return putIfAbsent(ConfigKey.of(key), value); + } + + Optional getOptional(ConfigKey configKey); + + default Optional getOptional(K key) { + return getOptional(ConfigKey.of(key)); + } + + @SuppressWarnings("unchecked") + default V getOrDefault(ConfigKey configKey, V defaultIfMissing) { + return (V) getOptional(configKey).orElse(defaultIfMissing); + } + + default V getOrDefault(K key, V defaultIfMissing) { + + + return getOrDefault(ConfigKey.of(key), defaultIfMissing); + } + + default V get(ConfigKey configKey) { + return getOrDefault(configKey, null); + } + + default V get(K key) { + return get(ConfigKey.of(key)); + } + + void setImmutable(boolean immutable); + + boolean isImmutable(); + + boolean containsKey(ConfigKey configKey); + + default boolean containsKey(K key) { + return containsKey(ConfigKey.of(key)); + } + + boolean containsValue(V value); + + default Stream configKeys() { + return entries().map(ConfigurationEntry::getKey); + } + + default Stream keys() { + return configKeys().map(ConfigKey::get); + } + + default Stream values() { + return entries().map(ConfigurationEntry::getValue); + } + + Stream entries(); + + +} diff --git a/jsonfactory/src/main/java/io/github/xmljim/json/factory/config/ConfigurationEntry.java b/jsonfactory/src/main/java/io/github/xmljim/json/factory/config/ConfigurationEntry.java new file mode 100644 index 0000000..e0345ae --- /dev/null +++ b/jsonfactory/src/main/java/io/github/xmljim/json/factory/config/ConfigurationEntry.java @@ -0,0 +1,61 @@ +package io.github.xmljim.json.factory.config; + +import java.util.Objects; + +public final class ConfigurationEntry { + private final ConfigKey key; + private final Object value; + + private ConfigurationEntry(K key, V value) { + this.key = ConfigKey.of(key); + this.value = value; + } + + private ConfigurationEntry(ConfigKey key, V value) { + this.key = key; + this.value = value; + } + + + public static ConfigurationEntry of(K key, V value) { + return new ConfigurationEntry(key, value); + } + + public static ConfigurationEntry of(ConfigKey key, V value) { + return new ConfigurationEntry(key, value); + } + + public ConfigKey getKey() { + return key; + } + + @SuppressWarnings("unchecked") + public T getValue() { + return (T) value; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ConfigurationEntry that = (ConfigurationEntry) o; + return key.equals(that.key) && value.equals(that.value); + } + + @Override + public int hashCode() { + return Objects.hash(key, value); + } + + @Override + public String toString() { + return "ConfigurationEntry {" + + "key=" + key + + ", value=" + value + + '}'; + } +} diff --git a/jsonfactory/src/main/java/io/github/xmljim/json/factory/jsonpath/JsonPath.java b/jsonfactory/src/main/java/io/github/xmljim/json/factory/jsonpath/JsonPath.java index ce241da..26b2d10 100644 --- a/jsonfactory/src/main/java/io/github/xmljim/json/factory/jsonpath/JsonPath.java +++ b/jsonfactory/src/main/java/io/github/xmljim/json/factory/jsonpath/JsonPath.java @@ -74,5 +74,10 @@ default T selectValue(JsonNode jsonNode, String expression) { return null; } + /** + * To be implemented + * + * @return stay tuned + */ List getErrors(); } diff --git a/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/ClassConfig.java b/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/ClassConfig.java new file mode 100644 index 0000000..bca002c --- /dev/null +++ b/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/ClassConfig.java @@ -0,0 +1,60 @@ +package io.github.xmljim.json.factory.mapper; + +import io.github.xmljim.json.factory.config.Configuration; +import io.github.xmljim.json.service.JsonService; +import io.github.xmljim.json.service.ServiceManager; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public interface ClassConfig extends Configuration { + + Class getTargetClass(); + + Class getSourceClass(); + + Set getIgnoredKeys(); + + List getConstructorKeys(); + + Stream getMemberConfigurations(); + + default boolean containsMember(Field field) { + return getMemberConfigurations().anyMatch(memberConfig -> memberConfig.getField().equals(field)); + } + + default boolean containsMember(String jsonKey) { + return getMemberConfigurations().anyMatch(memberConfig -> memberConfig.getJsonKey().equals(jsonKey)); + } + + static ClassConfig.Builder with() { + return ServiceManager.getProvider(ClassConfig.Builder.class); + } + + interface Builder extends JsonService { + Builder targetClass(Class targetClass); + + Builder sourceClass(Class sourceClass); + + default Builder ignoreKeys(String... keys) { + return ignoreKeys(Arrays.stream(keys).collect(Collectors.toSet())); + } + + Builder ignoreKeys(Collection keys); + + default Builder constructorKeys(String... keys) { + return constructorKeys(Arrays.stream(keys).toList()); + } + + Builder constructorKeys(List keys); + + Builder appendMemberConfig(MemberConfig memberConfig); + + ClassConfig build(); + } +} diff --git a/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/ClassMapping.java b/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/ClassMapping.java new file mode 100644 index 0000000..fc75b41 --- /dev/null +++ b/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/ClassMapping.java @@ -0,0 +1,40 @@ +package io.github.xmljim.json.factory.mapper; + +import io.github.xmljim.json.model.JsonObject; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; + +public interface ClassMapping { + ClassConfig getClassConfig(); + + Mapping getMapping(); + + Class getSourceClass(); + + Class getTargetClass(); + + boolean isPublic(); + + boolean isRecord(); + + List getConstructorKeys(); + + Stream getMemberMappings(); + + MemberMapping getMemberMapping(Field var1); + + MemberMapping getMemberMapping(String var1); + + Set getIgnoredKeys(); + + void appendMemberMapping(MemberMapping var1); + + + Constructor getConstructor(); + + T toClass(JsonObject jsonObject); +} \ No newline at end of file diff --git a/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/Converter.java b/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/Converter.java new file mode 100644 index 0000000..1095152 --- /dev/null +++ b/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/Converter.java @@ -0,0 +1,11 @@ +package io.github.xmljim.json.factory.mapper; + + +import java.util.Map; + +public interface Converter { + + R convert(T value); + + Map getArguments(); +} diff --git a/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/KeyNameCase.java b/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/KeyNameCase.java index 90a7544..c9305cd 100644 --- a/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/KeyNameCase.java +++ b/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/KeyNameCase.java @@ -1,8 +1,20 @@ package io.github.xmljim.json.factory.mapper; public enum KeyNameCase { + /** + * Default case + */ DEFAULT, + /** + * Camel case + */ CAMEL, + /** + * Snake case + */ SNAKE, + /** + * Kebab case + */ KEBAB } \ No newline at end of file diff --git a/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/Mapper.java b/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/Mapper.java index bf6a056..6950d9b 100644 --- a/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/Mapper.java +++ b/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/Mapper.java @@ -21,6 +21,8 @@ public interface Mapper { */ MapperConfig getConfig(); + Mapping getMapping(); + /** * Convert a Map to a JsonObject * diff --git a/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/MapperBuilder.java b/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/MapperBuilder.java deleted file mode 100644 index 666232a..0000000 --- a/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/MapperBuilder.java +++ /dev/null @@ -1,27 +0,0 @@ -package io.github.xmljim.json.factory.mapper; - -import java.util.Collection; - -public interface MapperBuilder { - - Mapper build(); - - MapperBuilder setTargetClass(Class targetClass); - - MapperBuilder setValueConverter(ValueConverter valueConverter); - - MapperBuilder setKeyNameCase(KeyNameCase keyNameCase); - - MapperBuilder setIgnoreKeys(Collection ignoreKeys); - - @SuppressWarnings("unused") - MapperBuilder setIgnoreKeys(String... ignoreKeys); - - default MapperBuilder merge(Mapper mapper) { - return setTargetClass(mapper.getConfig().getTargetClass().orElse(null)) - .setValueConverter(mapper.getConfig().getValueConverter().orElse(null)) - .setKeyNameCase(mapper.getConfig().getKeyNameCase()) - .setIgnoreKeys(mapper.getConfig().getIgnoreKeys()); - - } -} diff --git a/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/MapperConfig.java b/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/MapperConfig.java index d102317..7c379b3 100644 --- a/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/MapperConfig.java +++ b/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/MapperConfig.java @@ -3,12 +3,30 @@ import java.util.Optional; import java.util.Set; +/** + * Interface for the Mapper Configuration + */ public interface MapperConfig { + /** + * Return the target class for this Mapper + * + * @return the target class for this Mapper + */ Optional> getTargetClass(); + /** + * Return a value converter for this Mapper + * + * @return the value converter for this mapper + */ Optional> getValueConverter(); + /** + * Return the keyNameCase for this mapper + * + * @return the keyNameCase for this mapper + */ KeyNameCase getKeyNameCase(); Set getIgnoreKeys(); diff --git a/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/MapperFactory.java b/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/MapperFactory.java index 200109b..4bf2ba9 100644 --- a/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/MapperFactory.java +++ b/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/MapperFactory.java @@ -1,12 +1,37 @@ package io.github.xmljim.json.factory.mapper; +import io.github.xmljim.json.factory.mapper.parser.MappingParser; import io.github.xmljim.json.service.JsonService; +/** + * Factory/Service for Mapping Json to Java Classes + */ public interface MapperFactory extends JsonService { - MapperBuilder newBuilder(); - default Mapper newMapper() { - return newBuilder().build(); + /** + * Create a new Mapper with default configurations, i.e., no {@link Mapping} data + * + * @return a new Mapper with default configurations + */ + Mapper newMapper(); + + Mapper newMapper(Mapping mapping); + + default Mapper newMapper(MappingConfig mappingConfig) { + return newMapper(newMapping(mappingConfig)); } + + ClassMapping newClassMapping(Mapping mapping, ClassConfig classConfig); + + MemberMapping newMemberMapping(ClassMapping classMapping, MemberConfig memberConfig); + + Mapping newMapping(MappingConfig mappingConfig); + + MappingParser newMappingParser(MappingParserConfig mappingParserConfig); + + default MappingParser newMappingParser() { + return newMappingParser(MappingParserConfig.withDefaults()); + } + } diff --git a/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/Mapping.java b/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/Mapping.java new file mode 100644 index 0000000..e831706 --- /dev/null +++ b/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/Mapping.java @@ -0,0 +1,18 @@ +package io.github.xmljim.json.factory.mapper; + +import java.util.stream.Stream; + +public interface Mapping { + + MapperFactory getMapperFactory(); + + Stream getClassMappings(); + + ClassMapping getClassMapping(Class mappedClass); + + boolean containsClassMapping(Class mappedClass); + + void append(ClassMapping classMapping); + + void append(Class classMapping); +} diff --git a/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/MappingConfig.java b/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/MappingConfig.java new file mode 100644 index 0000000..69768b2 --- /dev/null +++ b/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/MappingConfig.java @@ -0,0 +1,30 @@ +package io.github.xmljim.json.factory.mapper; + +import io.github.xmljim.json.service.JsonService; +import io.github.xmljim.json.service.ServiceManager; + +import java.util.List; + +public interface MappingConfig { + + List getClassConfigurations(); + + static Builder with() { + return ServiceManager.getProvider(MappingConfig.Builder.class); + } + + static MappingConfig empty() { + return with().build(); + } + + interface Builder extends JsonService { + + default Builder withClass(Class sourceClass) { + return appendClassConfig(ClassConfig.with().sourceClass(sourceClass).build()); + } + + Builder appendClassConfig(ClassConfig classConfig); + + MappingConfig build(); + } +} diff --git a/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/MappingParserConfig.java b/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/MappingParserConfig.java new file mode 100644 index 0000000..36f3c34 --- /dev/null +++ b/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/MappingParserConfig.java @@ -0,0 +1,70 @@ +package io.github.xmljim.json.factory.mapper; + +import io.github.xmljim.json.factory.parser.NumericValueType; +import io.github.xmljim.json.factory.parser.Parser; +import io.github.xmljim.json.factory.parser.ParserSettings; +import io.github.xmljim.json.factory.parser.event.Assembler; +import io.github.xmljim.json.factory.parser.event.EventHandler; +import io.github.xmljim.json.factory.parser.event.Processor; +import io.github.xmljim.json.service.JsonService; +import io.github.xmljim.json.service.ServiceManager; + +import java.nio.charset.Charset; + +/** + * Configurations for Mapping Parser that extends both + * {@link ParserSettings} and {@link MappingConfig} + */ +public interface MappingParserConfig extends ParserSettings, MappingConfig { + + /** + * Get the Builder for creating configuration settings for Mapping Parser. Requires a + * {@link JsonService} service provider implementation to be present on the module path + * @return a new Builder implementation. If no provider is present, it will throw + * a {@link io.github.xmljim.json.service.exception.JsonServiceProviderUnavailableException}. + */ + static Builder with() { + return ServiceManager.getProvider(MappingParserConfig.Builder.class); + } + + /** + * Create a new Configuration from default settings + * @return a new configuration + */ + static MappingParserConfig withDefaults() { + return with().build(); + } + + /** + * Builder interface for creating the Mapper Parser settings. This is defined as a service + * so that a service provider can register with the ServiceManager. + */ + interface Builder extends MappingConfig.Builder, JsonService { + + Builder assembler(Assembler assembler); + + Builder blockCount(int blockCount); + + Builder characterSet(Charset charset); + + Builder eventHandler(EventHandler eventHandler); + + Builder enableStatistics(boolean enableStatistics); + + Builder fixedNumberStrategy(NumericValueType fixedNumberStrategy); + + Builder floatingNumberStrategy(NumericValueType floatingNumberStrategy); + + Builder maxEventBufferCapacity(int maxEventBufferCapacity); + + Builder useStrict(boolean useStrict); + + Builder parser(Parser parser); + + Builder processor(Processor processor); + + Builder requestNextLength(int requestNextLength); + + MappingParserConfig build(); + } +} diff --git a/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/MemberConfig.java b/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/MemberConfig.java new file mode 100644 index 0000000..8422949 --- /dev/null +++ b/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/MemberConfig.java @@ -0,0 +1,60 @@ +package io.github.xmljim.json.factory.mapper; + +import io.github.xmljim.json.factory.config.Configuration; +import io.github.xmljim.json.model.NodeType; +import io.github.xmljim.json.service.JsonService; +import io.github.xmljim.json.service.ServiceManager; + +import java.lang.reflect.Field; + +public interface MemberConfig extends Configuration { + + Field getField(); + + String getJsonKey(); + + NodeType getNodeType(); + + Class getContainerClass(); + + Class getElementTargetClass(); + + Converter getFieldConverter(); + + Converter getJsonConverter(); + + String getGetterMethodName(); + + String getSetterMethodName(); + + boolean isIgnored(); + + public static Builder with() { + return ServiceManager.getProvider(MemberConfig.Builder.class); + } + + interface Builder extends JsonService { + + Builder field(Field field); + + Builder jsonKey(String jsonKey); + + Builder nodeType(NodeType nodeType); + + Builder containerClass(Class containerClass); + + Builder elementTargetClass(Class elementTargetClass); + + Builder fieldConverter(Converter fieldConverter); + + Builder jsonConverter(Converter jsonConverter); + + Builder getterMethodName(String getterMethodName); + + Builder setterMethodName(String setterMethodName); + + Builder ignored(boolean ignored); + + MemberConfig build(); + } +} diff --git a/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/MemberMapping.java b/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/MemberMapping.java new file mode 100644 index 0000000..a7384e5 --- /dev/null +++ b/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/MemberMapping.java @@ -0,0 +1,40 @@ +package io.github.xmljim.json.factory.mapper; + +import io.github.xmljim.json.model.JsonObject; +import io.github.xmljim.json.model.NodeType; + +import java.lang.reflect.Type; + +public interface MemberMapping { + String getJsonKey(); + + NodeType getNodeType(); + + String getFieldName(); + + Type getFieldType(); + + boolean isIgnored(); + + boolean isAccessible(); + + Class getContainerClass(); + + String getSetterMethodName(); + + String getGetterMethodName(); + + Class getElementTargetClass(); + + Converter getJsonConverter(); + + Converter getFieldConverter(); + + void applyToClass(JsonObject var1, T var2); + + void applyToJson(T var1, JsonObject var2); + + T getValue(JsonObject var1); + + ClassMapping getClassMapping(); +} diff --git a/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/ValueConverter.java b/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/ValueConverter.java index fc311a7..760f835 100644 --- a/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/ValueConverter.java +++ b/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/ValueConverter.java @@ -3,12 +3,34 @@ import java.util.Map; import java.util.Optional; +/** + * Interface for value converters + * + * @param the converter value type + */ public interface ValueConverter { + /** + * Specifies the class types this converter will accept + * + * @return the accepted class types + */ Class[] accepts(); + /** + * Contains a map of arguments/parameters for this converter + * + * @return a map of arguments/parameters for this converter + */ Map args(); + /** + * Convert the value + * + * @param value the input value + * @param The output type + * @return the output + */ T convert(V value); static Optional> empty() { diff --git a/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/annotation/ConvertClass.java b/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/annotation/ConvertClass.java index a21a16e..2b54a22 100644 --- a/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/annotation/ConvertClass.java +++ b/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/annotation/ConvertClass.java @@ -5,8 +5,16 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +/** + * Specifies the target class used for mapping to and from Json and Java + */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD, ElementType.TYPE, ElementType.METHOD}) public @interface ConvertClass { + /** + * The target class + * + * @return the target class + */ Class target(); } diff --git a/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/annotation/ConvertValue.java b/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/annotation/ConvertValue.java index 442df96..6fb74c2 100644 --- a/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/annotation/ConvertValue.java +++ b/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/annotation/ConvertValue.java @@ -7,6 +7,10 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +/** + * Annotation that is assigned to a field, method or class that + * assigns ValueConverters to and from Json and a class instance + */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD}) public @interface ConvertValue { diff --git a/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/annotation/ConverterArg.java b/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/annotation/ConverterArg.java index aa4da94..5a2f23f 100644 --- a/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/annotation/ConverterArg.java +++ b/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/annotation/ConverterArg.java @@ -1,7 +1,20 @@ package io.github.xmljim.json.factory.mapper.annotation; +/** + * Specifies an argument for a ValueConverter + */ public @interface ConverterArg { + /** + * The argument name + * + * @return the argument name + */ String name(); + /** + * The argument value + * + * @return the argument value + */ String value(); } diff --git a/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/annotation/JsonElement.java b/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/annotation/JsonElement.java index 34329ee..0f2ace0 100644 --- a/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/annotation/JsonElement.java +++ b/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/annotation/JsonElement.java @@ -2,6 +2,9 @@ import java.lang.annotation.*; +/** + * Specifies a mapping to a Json element + */ @Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) diff --git a/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/parser/MappingParser.java b/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/parser/MappingParser.java new file mode 100644 index 0000000..c6a3827 --- /dev/null +++ b/jsonfactory/src/main/java/io/github/xmljim/json/factory/mapper/parser/MappingParser.java @@ -0,0 +1,30 @@ +package io.github.xmljim.json.factory.mapper.parser; + +import io.github.xmljim.json.factory.mapper.Mapper; +import io.github.xmljim.json.factory.parser.InputData; +import io.github.xmljim.json.factory.parser.Parser; + +/** + * Wrapper interface around a {@link Parser} and {@link Mapper}. + * It's a parser in name only in the sense that it + * uses a Parser to load the {@link io.github.xmljim.json.model.JsonObject} + * data. After parsing, the JsonObject instance is passed to a Mapper to + * create a class instance. + *

+ * Internally this is a functional interface that passes in + * an {@link InputData} and {@code Class} targetClass + *

+ */ +@FunctionalInterface +public interface MappingParser { + + /** + * Parse a Json data and load into a class instance + * + * @param inputData The input data + * @param targetClass the target class + * @param + * @return + */ + T parse(InputData inputData, Class targetClass); +} diff --git a/jsonfactory/src/main/java/io/github/xmljim/json/factory/merge/MergeConfig.java b/jsonfactory/src/main/java/io/github/xmljim/json/factory/merge/MergeConfig.java index 4184cb5..1e29e53 100644 --- a/jsonfactory/src/main/java/io/github/xmljim/json/factory/merge/MergeConfig.java +++ b/jsonfactory/src/main/java/io/github/xmljim/json/factory/merge/MergeConfig.java @@ -4,13 +4,36 @@ import io.github.xmljim.json.factory.merge.strategy.MergeResultStrategy; import io.github.xmljim.json.factory.merge.strategy.ObjectConflictStrategy; +/** + * Merge configuration + */ public interface MergeConfig { + /** + * Return the Array Conflict Strategy + * + * @return the Array Conflict Strategy + */ ArrayConflictStrategy getArrayConflictStrategy(); + /** + * Return the Object Conflict Strategy + * + * @return the Object Conflict Strategy + */ ObjectConflictStrategy getObjectConflictStrategy(); + /** + * Return the MergeResultStrategy + * + * @return the MergeResultStrategy + */ MergeResultStrategy getMergeResultStrategy(); + /** + * Return the key string to append to conflicted object keys in an Append strategy + * + * @return the key string to append + */ String getMergeAppendKey(); } diff --git a/jsonfactory/src/main/java/io/github/xmljim/json/factory/merge/MergeFactory.java b/jsonfactory/src/main/java/io/github/xmljim/json/factory/merge/MergeFactory.java index 5a3b666..c41be72 100644 --- a/jsonfactory/src/main/java/io/github/xmljim/json/factory/merge/MergeFactory.java +++ b/jsonfactory/src/main/java/io/github/xmljim/json/factory/merge/MergeFactory.java @@ -2,10 +2,23 @@ import io.github.xmljim.json.service.JsonService; +/** + * Factory/Service for Json Merging + */ public interface MergeFactory extends JsonService { + /** + * Create a new MergeBuilder for configuring a Merge + * + * @return a new MergeBuilder + */ MergeBuilder newMergeBuilder(); + /** + * Create a default MergeProcessor + * + * @return a new MergeProcess with default configuration + */ default MergeProcessor newMergeProcessor() { return newMergeBuilder().build(); } diff --git a/jsonfactory/src/main/java/io/github/xmljim/json/factory/merge/strategy/MergeResultStrategy.java b/jsonfactory/src/main/java/io/github/xmljim/json/factory/merge/strategy/MergeResultStrategy.java index 9215cc0..ab47939 100644 --- a/jsonfactory/src/main/java/io/github/xmljim/json/factory/merge/strategy/MergeResultStrategy.java +++ b/jsonfactory/src/main/java/io/github/xmljim/json/factory/merge/strategy/MergeResultStrategy.java @@ -1,5 +1,8 @@ package io.github.xmljim.json.factory.merge.strategy; +/** + * Specifies the output of a merge operation + */ public enum MergeResultStrategy { /** * Merge directly to the primary instance. diff --git a/jsonfactory/src/main/java/io/github/xmljim/json/factory/model/ElementFactory.java b/jsonfactory/src/main/java/io/github/xmljim/json/factory/model/ElementFactory.java index 65f4192..57f0a20 100644 --- a/jsonfactory/src/main/java/io/github/xmljim/json/factory/model/ElementFactory.java +++ b/jsonfactory/src/main/java/io/github/xmljim/json/factory/model/ElementFactory.java @@ -6,17 +6,65 @@ import io.github.xmljim.json.model.JsonValue; import io.github.xmljim.json.service.JsonService; +/** + * Factory/service for creating Json elements + */ public interface ElementFactory extends JsonService { + /** + * Create a new JsonValue + * + * @param value the raw value + * @param the JsonValue type + * @param the raw value type + * @return a new JsonValue + */ JsonValue newValue(T value); + /** + * create a new JsonValue + * + * @param value the raw value + * @param parent the parent element + * @param the JsonValue type + * @param the raw value type + * @return a new JsonValue + * @deprecated Do not use, will be removed at a later date + */ + @Deprecated JsonValue newValue(T value, JsonElement parent); + /** + * Create a new JsonObject + * + * @return a new JsonObject instance + */ JsonObject newObject(); + /** + * Create a new JsonObject + * + * @param parent the parent element + * @return a new JsonObject instance + * @deprecated do not use, will be removed + */ + @Deprecated JsonObject newObject(JsonElement parent); + /** + * Create a new JsonArray + * + * @return a new JsonArray + */ JsonArray newArray(); + /** + * Create a new JsonArray + * + * @param parent the parent element + * @return a new JsonArray + * @deprecated do not use, will be removed + */ + @Deprecated JsonArray newArray(JsonElement parent); } diff --git a/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/FixedNumberValueType.java b/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/FixedNumberValueType.java index 3c4b2fe..8d0c09a 100644 --- a/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/FixedNumberValueType.java +++ b/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/FixedNumberValueType.java @@ -2,7 +2,13 @@ import java.math.BigInteger; +/** + * An enumeration of fixed number strategies + */ public enum FixedNumberValueType implements NumericValueType { + /** + * Integer + */ INTEGER { @Override public Number apply(String numericString) { @@ -10,6 +16,9 @@ public Number apply(String numericString) { } }, + /** + * Long + */ LONG { @Override public Number apply(String numericString) { @@ -17,6 +26,9 @@ public Number apply(String numericString) { } }, + /** + * Big Integer + */ BIG_INTEGER { @Override public Number apply(String numericString) { diff --git a/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/FloatingNumberValueType.java b/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/FloatingNumberValueType.java index 9e70bbd..5dd211c 100644 --- a/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/FloatingNumberValueType.java +++ b/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/FloatingNumberValueType.java @@ -2,7 +2,13 @@ import java.math.BigDecimal; +/** + * An enumeration of floating number strategies + */ public enum FloatingNumberValueType implements NumericValueType { + /** + * Float + */ FLOAT { @Override public Number apply(String numericString) { @@ -10,6 +16,9 @@ public Number apply(String numericString) { } }, + /** + * Double + */ DOUBLE { @Override public Number apply(String numericString) { @@ -17,6 +26,9 @@ public Number apply(String numericString) { } }, + /** + * Big Decimal + */ BIG_DECIMAL { @Override public Number apply(String numericString) { diff --git a/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/InputData.java b/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/InputData.java index 6963876..8a5bd26 100644 --- a/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/InputData.java +++ b/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/InputData.java @@ -12,16 +12,39 @@ import java.nio.file.Path; import java.nio.file.StandardOpenOption; +/** + * Utility class the takes an input and converts it to an InputStream for used by a Parser + * + * @param inputStream + */ public record InputData(InputStream inputStream) implements AutoCloseable { + /** + * Return the inputStream for this data + * + * @return the inputstream + */ public InputStream getInputStream() { return inputStream; } + /** + * Create an InputData from a Json String + * + * @param data the Json String + * @return a new InputData + */ public static InputData of(final String data) { return of(data, StandardCharsets.UTF_8); } + /** + * Create an InputData from a Json String + * + * @param data the Json String + * @param charSet the character set + * @return a new InputData + */ public static InputData of(final String data, final Charset charSet) { try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(data.getBytes(charSet))) { return new InputData(byteArrayInputStream); @@ -30,6 +53,12 @@ public static InputData of(final String data, final Charset charSet) { } } + /** + * Create an InputData from a Path + * + * @param path the Path + * @return a new InputData + */ public static InputData of(final Path path) { try (InputStream inputStream = Files.newInputStream(path, StandardOpenOption.READ)) { return new InputData(inputStream); @@ -38,6 +67,12 @@ public static InputData of(final Path path) { } } + /** + * Create an InputData from a Reader + * + * @param reader the Json String + * @return a new InputData + */ public static InputData of(final Reader reader) { try { char[] charBuffer = new char[8 * 1024]; @@ -57,10 +92,21 @@ public static InputData of(final Reader reader) { } + /** + * Create an InputData from an InputStream + * + * @param inputStream the Json String + * @return a new InputData + */ public static InputData of(final InputStream inputStream) { return new InputData(inputStream); } + /** + * Close the underlying inputstream + * + * @throws Exception thrown if a problem occurs + */ @Override public void close() throws Exception { if (this.inputStream != null) { diff --git a/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/JsonEventParserException.java b/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/JsonEventParserException.java index 062aa44..2f0fb29 100644 --- a/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/JsonEventParserException.java +++ b/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/JsonEventParserException.java @@ -1,34 +1,51 @@ package io.github.xmljim.json.factory.parser; +/** + * An event parser exception + */ public class JsonEventParserException extends JsonParserException { private int line = -1; private int column = -1; + /** + * Constructor + */ public JsonEventParserException() { } + + /** + * Constructor + * + * @param lineNumber line number + * @param column column + * @param message message + */ public JsonEventParserException(int lineNumber, int column, String message) { super(message + " [at line: " + lineNumber + "; col: " + column + "]"); this.line = lineNumber; this.column = column; } + /** + * Constructor + * + * @param message message + */ public JsonEventParserException(final String message) { super(message); } - public JsonEventParserException(final String message, final Throwable cause) { - super(message, cause); - } + /** + * Constructor + * + * @param cause the underlying exception + */ public JsonEventParserException(final Throwable cause) { super(cause); } - public JsonEventParserException(final String message, final Throwable cause, final boolean enableSuppression, final boolean writableStackTrace) { - super(message, cause, enableSuppression, writableStackTrace); - } - public long getLineNumber() { return line; } diff --git a/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/JsonParserException.java b/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/JsonParserException.java index fbec94a..cdb6ab5 100644 --- a/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/JsonParserException.java +++ b/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/JsonParserException.java @@ -2,24 +2,33 @@ import io.github.xmljim.json.exception.JsonException; +/** + * A parser exception + */ public class JsonParserException extends JsonException { + /** + * Constructor + */ public JsonParserException() { super(); } + /** + * Constructor + * + * @param message message + */ public JsonParserException(String message) { super(message); } - public JsonParserException(String message, Throwable cause) { - super(message, cause); - } - + /** + * Constructor + * + * @param cause the underlying exception + */ public JsonParserException(Throwable cause) { super(cause); } - protected JsonParserException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { - super(message, cause, enableSuppression, writableStackTrace); - } } diff --git a/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/NumericValueType.java b/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/NumericValueType.java index a66fad1..fcbc152 100644 --- a/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/NumericValueType.java +++ b/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/NumericValueType.java @@ -1,5 +1,9 @@ package io.github.xmljim.json.factory.parser; + +/** + * Applies a number value to a numeric string + */ @FunctionalInterface public interface NumericValueType { Number apply(String numericString); diff --git a/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/Parser.java b/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/Parser.java index 644dbe5..e7d9c60 100644 --- a/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/Parser.java +++ b/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/Parser.java @@ -4,12 +4,32 @@ import java.util.concurrent.ExecutionException; import java.util.function.Supplier; +/** + * A JSON Parser + */ public interface Parser { + /** + * Return the Parser's settings + * + * @return the parser settings + */ ParserSettings getSettings(); + /** + * Utility method to apply parser settings + * + * @param settings the parser settings + */ void setSettings(ParserSettings settings); + /** + * Parse the data + * + * @param data the input data + * @param the data format + * @return the parsed JSON data in the format requested + */ default T parse(InputData data) { initializeProcessor(); @@ -31,10 +51,18 @@ default T parse(InputData data) { } + /** + * Initialize the Processer used by this parser + */ default void initializeProcessor() { getSettings().getProcessor().subscribe(getSettings().getEventHandler()); } + /** + * Return the statistics from the parsing + * + * @return the statistics + */ default Statistics getStatistics() { Statistics statistics = new Statistics(); if (getSettings().enableStatistics()) { diff --git a/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/ParserBuilder.java b/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/ParserBuilder.java index 278fcb7..4242ec0 100644 --- a/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/ParserBuilder.java +++ b/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/ParserBuilder.java @@ -6,34 +6,154 @@ import java.nio.charset.Charset; +/** + * Builder interface for creating a Parser with given settings + */ public interface ParserBuilder { + /** + * Specify a parser implementation + * + * @param parser the parser + * @return the builder + */ ParserBuilder withParser(Parser parser); + /** + * Specify the JSON character set to use + * + * @param characterSet the character set + * @return the builder + */ ParserBuilder setCharacterSet(Charset characterSet); + /** + * Specify the Event Handler implementation to use + * + * @param handler the event handler + * @return the builder + */ ParserBuilder setEventHandler(EventHandler handler); + /** + * Specify the Assembler to use with the Event Handler + * + * @param assembler the assembler + * @return the builder + */ ParserBuilder setAssembler(Assembler assembler); + /** + * Specify a Processor that will process the JSON data + * + * @param processor the processor + * @return the builder + */ ParserBuilder withProcessor(Processor processor); + /** + * Specify a fixed number strategy (i.e., use a Long or Integer as a default) + * + * @param numberStrategy the number strategy + * @return the builder + */ ParserBuilder setFixedNumberStrategy(NumericValueType numberStrategy); + /** + * Specify a floating number strategy (i.e., Float or Double) + * + * @param numberStrategy the number strategy + * @return the builder + */ ParserBuilder setFloatingNumberStrategy(NumericValueType numberStrategy); + /** + * Specify whether or not use strict EMCA encoding rules + * + * @param useStrict true to use strict, false otherwise + * @return the builder + */ ParserBuilder setUseStrict(boolean useStrict); + /** + * Specify the block size to use with the Processor to process data. + * Note: Not all Processors will recognize this value + * + * @param blockCount the block size + * @return the builder + */ ParserBuilder setBlockCount(int blockCount); + /** + * Specify the MaxEventBufferCapacity to be used by EventHandlers for subscribed events. + * Not: Not all EventHandlers will recognize or use this value + * + * @param capacity the capacity + * @return the builder + */ ParserBuilder setMaxEventBufferCapacity(int capacity); + /** + * Specify a custom setting. These will be implementation specific + * + * @param name the setting name + * @param value the setting value + * @param The value type + * @return the bulder + */ ParserBuilder setSetting(String name, T value); + /** + * Return the default settings + * + * @return the builder + */ ParserBuilder withDefaultSettings(); + default ParserBuilder mergeSettings(ParserSettings settings) { + if (settings.getAssembler() != null) { + setAssembler(settings.getAssembler()); + } + + setBlockCount(settings.getBlockCount()); + + if (settings.getCharacterSet() != null) { + setCharacterSet(settings.getCharacterSet()); + } + + if (settings.getEventHandler() != null) { + setEventHandler(settings.getEventHandler()); + } + + if (settings.fixedNumberStrategy() != null) { + setFixedNumberStrategy(settings.fixedNumberStrategy()); + } + + if (settings.floatingNumberStrategy() != null) { + setFloatingNumberStrategy(settings.floatingNumberStrategy()); + } + + setMaxEventBufferCapacity(settings.getMaxEventBufferCapacity()); + setUseStrict(settings.useStrict()); + + if (settings.getProcessor() != null) { + withProcessor(settings.getProcessor()); + } + return this; + } + + /** + * Create a new Parser + * + * @return + */ Parser build(); + /** + * Create a parser with default settings. + * + * @return + */ default Parser defaultParser() { return withDefaultSettings().build(); } diff --git a/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/ParserFactory.java b/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/ParserFactory.java index 25f8b79..a3d0507 100644 --- a/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/ParserFactory.java +++ b/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/ParserFactory.java @@ -2,9 +2,23 @@ import io.github.xmljim.json.service.JsonService; +/** + * Factory/Service for the Json Parser + */ public interface ParserFactory extends JsonService { + + /** + * Create a new builder to configure and build a Parser + * + * @return a new ParserBuilder + */ ParserBuilder newParserBuilder(); + /** + * Create a new Parser with default settings + * + * @return a new Parser + */ default Parser newParser() { return newParserBuilder().defaultParser(); } diff --git a/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/ParserSettings.java b/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/ParserSettings.java index 2cb12f4..a596120 100644 --- a/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/ParserSettings.java +++ b/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/ParserSettings.java @@ -6,33 +6,107 @@ import java.nio.charset.Charset; +/** + * Parser settings. Note that not all Parsers or constituent components + * orchestrated by the Parser will recognize or use these settings + */ public interface ParserSettings { + /** + * Returns whether nor not to enable statistics + * + * @return true to enable statistics; false otherwise + */ boolean enableStatistics(); + /** + * Return the fixed number strategy + * + * @return the fixed number strategy for Json number values + */ NumericValueType fixedNumberStrategy(); + /** + * Return the floating number strategy + * + * @return the floating number strategy + */ NumericValueType floatingNumberStrategy(); + /** + * Return the Processor to use + * + * @return the Processor + */ Processor getProcessor(); + /** + * Return the Event Handler to use + * + * @return the Event Handler + */ EventHandler getEventHandler(); + /** + * Return the Assembler to use + * + * @param The assembler type/format + * @return the Assembler + */ Assembler getAssembler(); + /** + * Return the character set to use for parsing + * + * @return the character set + */ Charset getCharacterSet(); + /** + * Return the Maximum Event Buffer Capacity to use with Event subscriptions + * + * @return the Maximum Event Buffer Capacity to use with Event subscriptions + */ int getMaxEventBufferCapacity(); + /** + * Return the number of events to send via subscription + * + * @return the number of events + */ long getRequestNextLength(); + /** + * Return block count + * + * @return the block count + */ int getBlockCount(); + /** + * Return the block size + * + * @return the block size + */ default int getBlockSizeBytes() { return getBlockCount() * 1024; } + /** + * Return whether to use strict ECMA rules for parsing + * + * @return true if using strict; false otherwise + */ boolean useStrict(); + /** + * Return a custom setting + * + * @param name the setting name + * @param the value type + * @return the setting value + */ T getSetting(String name); + ParserSettings merge(ParserSettings settings); + } diff --git a/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/Statistic.java b/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/Statistic.java index de2be9c..91d2021 100644 --- a/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/Statistic.java +++ b/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/Statistic.java @@ -1,10 +1,30 @@ package io.github.xmljim.json.factory.parser; +/** + * A parser statistic. Can be applied to any component of the parser (i.e., Processor, EventHandler, or Assembler) + * + * @param the statistic value type + */ public interface Statistic { + /** + * Return the component name + * + * @return the component name + */ String getComponent(); + /** + * Return the statistic name + * + * @return the statistic name + */ String getName(); + /** + * return the statistic value + * + * @return the statistic value + */ T getValue(); } diff --git a/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/Statistics.java b/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/Statistics.java index 4f1b56b..76acd6e 100644 --- a/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/Statistics.java +++ b/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/Statistics.java @@ -7,101 +7,270 @@ import java.util.Optional; import java.util.stream.Stream; +/** + * A collection of {@link Statistic} elements + */ public final class Statistics { + /** + * EventProcessor component + */ public static final String COMPONENT_EVENT_PROCESSOR = "EventProcessor"; + /** + * EventHandler component + */ public static final String COMPONENT_EVENT_HANDLER = "EventHandler"; + /** + * EventAssembler component + */ public static final String COMPONENT_EVENT_ASSEMBLER = "EventAssembler"; + /** + * Processing Time statistic + */ public static final String STAT_PROCESS_TIME = "processingTime"; + /** + * Bytes processed statistic + */ public static final String STAT_BYTES_PROCESSED = "bytesProcessed"; + /** + * Event Send Time statistic + */ public static final String STAT_EVENT_SEND_TIME = "eventSendTime"; + /** + * Entity count statistic + */ public static final String STAT_ENTITY_COUNT = "entityCount"; + + /** + * Assembly time statistic + */ public static final String STAT_ASSEMBLY_TIME = "assemblyTime"; + + /** + * Bytes per second statistic + */ public static final String STAT_BYTES_PER_SECOND = "bytesPerSecond"; + /** + * Parsing time statistic + */ public static final String STAT_PARSING_TIME = "parsingTime"; + /** + * Event processing time statistic + */ public static final String STAT_EVENT_PROCESSING_TIME = "eventProcessingTime"; + /** + * Event count statistic + */ public static final String STAT_EVENT_COUNT = "eventCount"; + /** + * Events sent per second statistic + */ public static final String STAT_EVENT_SEND_PER_SECOND = "eventSendPerSecond"; + /** + * Entities assembled per second statistic + */ public static final String STAT_ASSEMBLY_ENTITY_PER_SECOND = "entitiesPerSecond"; private final Map> statisticMap = new LinkedHashMap<>(); + /** + * Utility method for setting the {@link #STAT_PROCESS_TIME} linked to the + * {@link #COMPONENT_EVENT_PROCESSOR} + * + * @param value the value + * @param the value type + */ public void setProcessorBytesPerSecond(T value) { setStatistic(COMPONENT_EVENT_PROCESSOR, STAT_PROCESS_TIME, value); } + /** + * Utility method for setting the {@link #STAT_ASSEMBLY_TIME} linked to the + * {@link #COMPONENT_EVENT_ASSEMBLER} + * + * @param value the value + * @param the value type + */ public void setAssemblyTime(T value) { setStatistic(COMPONENT_EVENT_ASSEMBLER, STAT_ASSEMBLY_TIME, value); } + /** + * Utility method for setting the {@link #STAT_BYTES_PROCESSED} linked to the + * {@link #COMPONENT_EVENT_PROCESSOR} + * + * @param value the value + * @param the value type + */ public void setBytesProcessed(T value) { setStatistic(COMPONENT_EVENT_PROCESSOR, STAT_BYTES_PROCESSED, value); } + /** + * Utility method for setting the {@link #STAT_EVENT_SEND_TIME} linked to the + * {@link #COMPONENT_EVENT_PROCESSOR} + * + * @param value the value + * @param the value type + */ public void setEventSendTime(T value) { setStatistic(COMPONENT_EVENT_PROCESSOR, STAT_EVENT_SEND_TIME, value); } + /** + * Utility method for setting the {@link #STAT_ENTITY_COUNT} linked to the + * {@link #COMPONENT_EVENT_ASSEMBLER} + * + * @param value the value + * @param the value type + */ public void setEntityCount(T value) { setStatistic(COMPONENT_EVENT_ASSEMBLER, STAT_ENTITY_COUNT, value); } + /** + * Utility method for setting the {@link #STAT_BYTES_PER_SECOND} linked to the + * {@link #COMPONENT_EVENT_PROCESSOR} + * + * @param value the value + * @param the value type + */ public void setBytesPerSecond(T value) { setStatistic(COMPONENT_EVENT_PROCESSOR, STAT_BYTES_PER_SECOND, value); } + /** + * Utility method for setting the {@link #STAT_PARSING_TIME} linked to the + * {@link #COMPONENT_EVENT_PROCESSOR} + * + * @param value the value + * @param the value type + */ public void setParsingTime(T value) { setStatistic(COMPONENT_EVENT_PROCESSOR, STAT_PARSING_TIME, value); } + /** + * Utility method for setting the {@link #STAT_PROCESS_TIME} linked to the + * {@link #COMPONENT_EVENT_PROCESSOR} + * + * @param value the value + * @param the value type + */ public void setProcessingTime(T value) { setStatistic(COMPONENT_EVENT_PROCESSOR, STAT_PROCESS_TIME, value); } + /** + * Utility method for setting the {@link #STAT_EVENT_PROCESSING_TIME} linked to the + * {@link #COMPONENT_EVENT_HANDLER} + * + * @param value the value + * @param the value type + */ public void setEventProcessingTime(T value) { setStatistic(COMPONENT_EVENT_HANDLER, STAT_EVENT_PROCESSING_TIME, value); } + /** + * Utility method for setting the {@link #STAT_ENTITY_COUNT} linked to the + * {@link #COMPONENT_EVENT_HANDLER} + * + * @param value the value + * @param the value type + */ public void setEventCount(T value) { setStatistic(COMPONENT_EVENT_HANDLER, STAT_EVENT_COUNT, value); } + /** + * Utility method for setting the {@link #STAT_EVENT_SEND_PER_SECOND} linked to the + * {@link #COMPONENT_EVENT_HANDLER} + * + * @param value the value + * @param the value type + */ public void setEventsSentPerSecond(T value) { setStatistic(COMPONENT_EVENT_HANDLER, STAT_EVENT_SEND_PER_SECOND, value); } + /** + * Utility method for setting the {@link #STAT_ASSEMBLY_ENTITY_PER_SECOND} linked to the + * {@link #COMPONENT_EVENT_ASSEMBLER} + * + * @param value the value + * @param the value type + */ public void setAssemblyEntitiesPerSecond(T value) { setStatistic(COMPONENT_EVENT_ASSEMBLER, STAT_ASSEMBLY_ENTITY_PER_SECOND, value); } + /** + * Set a statistic + * + * @param componentName the component name + * @param statisticName the statistic name + * @param value the statistic value + * @param value type + */ public void setStatistic(String componentName, String statisticName, T value) { appendStatistic(new StatisticsRecord<>(componentName, statisticName, value)); } + /** + * Append statistic + * + * @param statistic the statistic + */ private void appendStatistic(Statistic statistic) { statisticMap.put(statistic.getName(), statistic); } + /** + * Merge statistics + * + * @param statistics another statistics instance + */ public void merge(Statistics statistics) { - statistics.stream().forEach(stat -> { - statisticMap.putIfAbsent(stat.getName(), stat); - }); + statistics.stream().forEach(stat -> statisticMap.putIfAbsent(stat.getName(), stat)); } + /** + * Stream of statistics + * + * @return a stream of statistics + */ public Stream> stream() { return statisticMap.values().stream(); } + /** + * Return a statistic by name + * + * @param statisticName the statistic name + * @return the statistic + */ public Optional> getByName(String statisticName) { return Optional.ofNullable(statisticMap.getOrDefault(statisticName, null)); } + /** + * Return a list of statistics by component name + * + * @param component the component name + * @return the list of statistics + */ public List> getByComponent(String component) { return stream().filter(stat -> stat.getComponent().equals(component)).toList(); } + /** + * Convert the statistics to a string output + * + * @return the statistics to a string output + */ public String toString() { StringBuilder builder = new StringBuilder(); diff --git a/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/event/Assembler.java b/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/event/Assembler.java index 0cae87f..82b412f 100644 --- a/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/event/Assembler.java +++ b/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/event/Assembler.java @@ -6,6 +6,11 @@ import java.math.BigDecimal; import java.util.function.Supplier; +/** + * Assembles a specific format instance from incoming JsonEvents + * + * @param The format type + */ public interface Assembler extends Configurable { /** @@ -114,9 +119,24 @@ public interface Assembler extends Configurable { */ void valueNull(String key); + /** + * Assembled from a new key + * + * @param key The key name + */ void newKey(String key); + /** + * Return the formatted result + * + * @return the formatted result + */ Supplier getResult(); + /** + * Return the Statistics associated with the assembler + * + * @return the statistics associated with the assembler + */ Statistics getStatistics(); } diff --git a/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/event/Configurable.java b/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/event/Configurable.java index a874c91..2591682 100644 --- a/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/event/Configurable.java +++ b/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/event/Configurable.java @@ -2,9 +2,22 @@ import io.github.xmljim.json.factory.parser.ParserSettings; +/** + * Wrapper interface for configuration + */ public interface Configurable { + /** + * Set settings + * + * @param settings the parser settings + */ void setSettings(ParserSettings settings); + /** + * return the Parser settings + * + * @return the parser settings + */ ParserSettings getSettings(); } diff --git a/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/event/EventHandler.java b/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/event/EventHandler.java index ac38e45..ca6a4ee 100644 --- a/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/event/EventHandler.java +++ b/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/event/EventHandler.java @@ -4,11 +4,30 @@ import java.util.concurrent.Flow; +/** + * Interface for Event Handlers. It extends the Flow API Subscriber interface so that + * it can receive events from a {@link Processor} (which is a {@link Flow.Publisher}) + */ public interface EventHandler extends Flow.Subscriber, Configurable { + /** + * Returns whether the event handler is complete + * + * @return true if complete + */ boolean isComplete(); + /** + * Return the Assembler associated with this event handler + * + * @return the Assembler + */ Assembler getAssembler(); + /** + * Return the statistics from this event handler + * + * @return the statistics + */ Statistics getStatistics(); } diff --git a/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/event/EventPublisher.java b/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/event/EventPublisher.java index 0ca1931..1c604a6 100644 --- a/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/event/EventPublisher.java +++ b/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/event/EventPublisher.java @@ -2,6 +2,9 @@ import java.util.concurrent.Flow; +/** + * Tagging interface that extends the Flow.Publisher interface + */ public interface EventPublisher extends Flow.Publisher { } diff --git a/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/event/EventType.java b/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/event/EventType.java index 7cb8b52..1b352df 100644 --- a/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/event/EventType.java +++ b/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/event/EventType.java @@ -1,20 +1,74 @@ package io.github.xmljim.json.factory.parser.event; +/** + * Enumeration of event types + */ public enum EventType { + /** + * Document Start - either with a { or {@code [} + */ DOCUMENT_START, + /** + * Document End + */ DOCUMENT_END, + /** + * JsonObject start - with a { + */ OBJECT_START, + /** + * JsonObject end + */ OBJECT_END, + /** + * JsonArray start + */ ARRAY_START, + /** + * JsonArray end + */ ARRAY_END, + /** + * String value start + */ STRING_START, + /** + * String value end + */ STRING_END, + /** + * Number value start + */ NUMBER_START, + /** + * Number value end + */ NUMBER_END, + /** + * Boolean value start + */ BOOLEAN_START, + + /** + * Boolean value end + */ BOOLEAN_END, + /** + * Null value start + */ NULL_START, + /** + * Null value end + */ NULL_END, + + /** + * JsonObject key start + */ KEY_END, + + /** + * JsonObject key end + */ ENTITY_END } diff --git a/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/event/JsonAssembler.java b/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/event/JsonAssembler.java index 4413a89..fc31e09 100644 --- a/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/event/JsonAssembler.java +++ b/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/event/JsonAssembler.java @@ -2,5 +2,8 @@ import io.github.xmljim.json.model.JsonNode; +/** + * Tagging interface for Assemblers that create Json instances + */ public interface JsonAssembler extends Assembler { } diff --git a/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/event/JsonEvent.java b/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/event/JsonEvent.java index 529bc10..0a1cc5d 100644 --- a/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/event/JsonEvent.java +++ b/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/event/JsonEvent.java @@ -2,12 +2,36 @@ import java.nio.ByteBuffer; +/** + * A processing event produced by a {@link Processor}, handled by + * an {@link EventHandler} and then subsequently assembed by an {@link Assembler} + */ public interface JsonEvent { + /** + * The column number for the event + * + * @return the column number for the event + */ int getColumn(); + /** + * The line number of the event + * + * @return the line number of the event + */ int getLineNumber(); + /** + * The data associated with the event + * + * @return the data associated with the event + */ ByteBuffer getData(); + /** + * The event type + * + * @return the event type + */ EventType getEventType(); } diff --git a/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/event/Processor.java b/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/event/Processor.java index 5f9c9c1..855406f 100644 --- a/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/event/Processor.java +++ b/jsonfactory/src/main/java/io/github/xmljim/json/factory/parser/event/Processor.java @@ -4,9 +4,23 @@ import java.io.InputStream; +/** + * The Json Processer, which processes the JSON data and passes the data + * as events to an {@link EventHandler} + */ public interface Processor extends EventPublisher, Configurable { + /** + * Begin processing the data + * + * @param inputStream the input stream containing the JSON data + */ void process(InputStream inputStream); + /** + * Return the Statistics associated with this processor + * + * @return the Statistics associated with this processor + */ Statistics getStatistics(); } diff --git a/jsonfactory/src/main/java/module-info.java b/jsonfactory/src/main/java/module-info.java index ea64d79..3419fe9 100644 --- a/jsonfactory/src/main/java/module-info.java +++ b/jsonfactory/src/main/java/module-info.java @@ -11,4 +11,6 @@ exports io.github.xmljim.json.service; exports io.github.xmljim.json.service.exception; exports io.github.xmljim.json.factory.jsonpath; + exports io.github.xmljim.json.factory.config; + exports io.github.xmljim.json.factory.mapper.parser; } \ No newline at end of file diff --git a/jsonfactory/src/test/java/io/github/xmljim/json/factory/test/ServiceManagerTests.java b/jsonfactory/src/test/java/io/github/xmljim/json/factory/test/ServiceManagerTests.java new file mode 100644 index 0000000..11d0da4 --- /dev/null +++ b/jsonfactory/src/test/java/io/github/xmljim/json/factory/test/ServiceManagerTests.java @@ -0,0 +1,50 @@ +package io.github.xmljim.json.factory.test; + +import io.github.xmljim.json.factory.jsonpath.JsonPathFactory; +import io.github.xmljim.json.factory.mapper.MapperFactory; +import io.github.xmljim.json.factory.model.ElementFactory; +import io.github.xmljim.json.factory.parser.ParserFactory; +import io.github.xmljim.json.service.JsonService; +import io.github.xmljim.json.service.ServiceManager; +import io.github.xmljim.json.service.exception.JsonServiceProviderUnavailableException; +import org.junit.jupiter.api.Test; + +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; + +public class ServiceManagerTests { + + @Test + void testGetProviderFails() { + + assertFalse(ServiceManager.isServiceAvailable(ElementFactory.class)); + assertThrows(JsonServiceProviderUnavailableException.class, + () -> ServiceManager.getProvider(MapperFactory.class)); + } + + @Test + void testGetProviderSuccess() { + ParserFactory parserFactory = ServiceManager.getProvider(ParserFactory.class); + assertTrue(parserFactory instanceof TestServiceClass); + } + + @Test + void testListServices() { + Set serviceSet = ServiceManager.listServices(); + + assertEquals(1, serviceSet.size()); + assertTrue(serviceSet.iterator().next() instanceof TestServiceClass); + } + + @Test + void testListServiceProviders() { + assertTrue(ServiceManager.listProviders(ElementFactory.class).isEmpty()); + assertTrue(ServiceManager.listProviders(JsonPathFactory.class).isEmpty()); + + Set serviceProviders = ServiceManager.listProviders(ParserFactory.class); + assertEquals(1, serviceProviders.size()); + assertTrue(serviceProviders.iterator().next() instanceof TestServiceClass); + } + +} diff --git a/jsonfactory/src/test/java/io/github/xmljim/json/factory/test/TestServiceClass.java b/jsonfactory/src/test/java/io/github/xmljim/json/factory/test/TestServiceClass.java new file mode 100644 index 0000000..5354da7 --- /dev/null +++ b/jsonfactory/src/test/java/io/github/xmljim/json/factory/test/TestServiceClass.java @@ -0,0 +1,18 @@ +package io.github.xmljim.json.factory.test; + +import io.github.xmljim.json.factory.parser.ParserBuilder; +import io.github.xmljim.json.factory.parser.ParserFactory; +import io.github.xmljim.json.service.JsonServiceProvider; + +@JsonServiceProvider(version = "0.1.1", service = ParserFactory.class) +public class TestServiceClass implements ParserFactory { + + public TestServiceClass() { + //no-op + } + + @Override + public ParserBuilder newParserBuilder() { + return null; + } +} diff --git a/jsonfactory/src/test/java/module-info.java b/jsonfactory/src/test/java/module-info.java new file mode 100644 index 0000000..cc348ab --- /dev/null +++ b/jsonfactory/src/test/java/module-info.java @@ -0,0 +1,15 @@ +import io.github.xmljim.json.factory.parser.ParserFactory; +import io.github.xmljim.json.factory.test.TestServiceClass; + +module io.github.xmljim.json.factory.test { + + requires transitive io.github.xmljim.json.factory; + + opens io.github.xmljim.json.factory.test; + + //These dependencies are needed to run tests + requires org.junit.jupiter.api; + requires org.junit.jupiter.engine; + + provides ParserFactory with TestServiceClass; +} \ No newline at end of file diff --git a/jsonpath/src/main/java/io/github/xmljim/json/jsonpath/compiler/Compiler.java b/jsonpath/src/main/java/io/github/xmljim/json/jsonpath/compiler/Compiler.java index a525a34..34b744c 100644 --- a/jsonpath/src/main/java/io/github/xmljim/json/jsonpath/compiler/Compiler.java +++ b/jsonpath/src/main/java/io/github/xmljim/json/jsonpath/compiler/Compiler.java @@ -10,7 +10,6 @@ /** * Base class for JsonPath expression compilation - * * @param The return result argType from the compilation */ public abstract class Compiler { @@ -42,7 +41,7 @@ public abstract class Compiler { private final Deque enclosures = new ArrayDeque<>(); private final Global global; - protected Compiler(PathExpression expression, Global global) { + protected Compiler(@SuppressWarnings("ClassEscapesDefinedScope") PathExpression expression, Global global) { this.expression = expression; this.global = global; } @@ -59,6 +58,7 @@ public static Compiler> newPredicateCompiler(String expressio return new PredicateCompiler(new PathExpression(expression), global); } + @SuppressWarnings("ClassEscapesDefinedScope") public PathExpression expression() { return expression; } diff --git a/jsonpath/src/main/java/io/github/xmljim/json/jsonpath/filter/Accessor.java b/jsonpath/src/main/java/io/github/xmljim/json/jsonpath/context/Accessor.java similarity index 97% rename from jsonpath/src/main/java/io/github/xmljim/json/jsonpath/filter/Accessor.java rename to jsonpath/src/main/java/io/github/xmljim/json/jsonpath/context/Accessor.java index 72cca42..8dcd693 100644 --- a/jsonpath/src/main/java/io/github/xmljim/json/jsonpath/filter/Accessor.java +++ b/jsonpath/src/main/java/io/github/xmljim/json/jsonpath/context/Accessor.java @@ -1,4 +1,4 @@ -package io.github.xmljim.json.jsonpath.filter; +package io.github.xmljim.json.jsonpath.context; import java.util.Objects; diff --git a/jsonpath/src/main/java/io/github/xmljim/json/jsonpath/context/ArrayContext.java b/jsonpath/src/main/java/io/github/xmljim/json/jsonpath/context/ArrayContext.java index 1c016aa..6cc0ffa 100644 --- a/jsonpath/src/main/java/io/github/xmljim/json/jsonpath/context/ArrayContext.java +++ b/jsonpath/src/main/java/io/github/xmljim/json/jsonpath/context/ArrayContext.java @@ -1,6 +1,5 @@ package io.github.xmljim.json.jsonpath.context; -import io.github.xmljim.json.jsonpath.filter.Accessor; import io.github.xmljim.json.model.JsonArray; import java.util.stream.IntStream; diff --git a/jsonpath/src/main/java/io/github/xmljim/json/jsonpath/context/Context.java b/jsonpath/src/main/java/io/github/xmljim/json/jsonpath/context/Context.java index 390d1b7..205487c 100644 --- a/jsonpath/src/main/java/io/github/xmljim/json/jsonpath/context/Context.java +++ b/jsonpath/src/main/java/io/github/xmljim/json/jsonpath/context/Context.java @@ -2,7 +2,6 @@ import io.github.xmljim.json.factory.model.ElementFactory; import io.github.xmljim.json.jsonpath.JsonPathException; -import io.github.xmljim.json.jsonpath.filter.Accessor; import io.github.xmljim.json.jsonpath.util.DataType; import io.github.xmljim.json.model.JsonArray; import io.github.xmljim.json.model.JsonElement; diff --git a/jsonpath/src/main/java/io/github/xmljim/json/jsonpath/context/DateContext.java b/jsonpath/src/main/java/io/github/xmljim/json/jsonpath/context/DateContext.java index 73232da..af6c19e 100644 --- a/jsonpath/src/main/java/io/github/xmljim/json/jsonpath/context/DateContext.java +++ b/jsonpath/src/main/java/io/github/xmljim/json/jsonpath/context/DateContext.java @@ -4,7 +4,7 @@ import java.time.LocalDate; -public class DateContext extends TemporalContext { +class DateContext extends TemporalContext { public DateContext(LocalDate value) { super(value); } diff --git a/jsonpath/src/main/java/io/github/xmljim/json/jsonpath/context/DateTimeContext.java b/jsonpath/src/main/java/io/github/xmljim/json/jsonpath/context/DateTimeContext.java index da502d1..349ee03 100644 --- a/jsonpath/src/main/java/io/github/xmljim/json/jsonpath/context/DateTimeContext.java +++ b/jsonpath/src/main/java/io/github/xmljim/json/jsonpath/context/DateTimeContext.java @@ -4,7 +4,7 @@ import java.time.LocalDateTime; -public class DateTimeContext extends TemporalContext { +class DateTimeContext extends TemporalContext { public DateTimeContext(LocalDateTime value) { super(value); } diff --git a/jsonpath/src/main/java/io/github/xmljim/json/jsonpath/context/ObjectContext.java b/jsonpath/src/main/java/io/github/xmljim/json/jsonpath/context/ObjectContext.java index c97a6e8..62f3b78 100644 --- a/jsonpath/src/main/java/io/github/xmljim/json/jsonpath/context/ObjectContext.java +++ b/jsonpath/src/main/java/io/github/xmljim/json/jsonpath/context/ObjectContext.java @@ -1,6 +1,5 @@ package io.github.xmljim.json.jsonpath.context; -import io.github.xmljim.json.jsonpath.filter.Accessor; import io.github.xmljim.json.model.JsonObject; import java.util.stream.Stream; diff --git a/jsonpath/src/main/java/io/github/xmljim/json/jsonpath/context/ValueContext.java b/jsonpath/src/main/java/io/github/xmljim/json/jsonpath/context/ValueContext.java index 696d860..ade99f3 100644 --- a/jsonpath/src/main/java/io/github/xmljim/json/jsonpath/context/ValueContext.java +++ b/jsonpath/src/main/java/io/github/xmljim/json/jsonpath/context/ValueContext.java @@ -1,6 +1,5 @@ package io.github.xmljim.json.jsonpath.context; -import io.github.xmljim.json.jsonpath.filter.Accessor; import io.github.xmljim.json.model.JsonValue; import java.util.stream.Stream; diff --git a/jsonpath/src/main/java/io/github/xmljim/json/jsonpath/filter/ChildFilter.java b/jsonpath/src/main/java/io/github/xmljim/json/jsonpath/filter/ChildFilter.java index f5ed24e..55831aa 100644 --- a/jsonpath/src/main/java/io/github/xmljim/json/jsonpath/filter/ChildFilter.java +++ b/jsonpath/src/main/java/io/github/xmljim/json/jsonpath/filter/ChildFilter.java @@ -1,5 +1,6 @@ package io.github.xmljim.json.jsonpath.filter; +import io.github.xmljim.json.jsonpath.context.Accessor; import io.github.xmljim.json.jsonpath.context.Context; import io.github.xmljim.json.jsonpath.util.Global; diff --git a/jsonpath/src/main/java/io/github/xmljim/json/jsonpath/filter/SliceFilter.java b/jsonpath/src/main/java/io/github/xmljim/json/jsonpath/filter/SliceFilter.java index 3ae0925..2193e05 100644 --- a/jsonpath/src/main/java/io/github/xmljim/json/jsonpath/filter/SliceFilter.java +++ b/jsonpath/src/main/java/io/github/xmljim/json/jsonpath/filter/SliceFilter.java @@ -1,6 +1,7 @@ package io.github.xmljim.json.jsonpath.filter; import io.github.xmljim.json.jsonpath.compiler.JsonPathExpressionException; +import io.github.xmljim.json.jsonpath.context.Accessor; import io.github.xmljim.json.jsonpath.context.Context; import io.github.xmljim.json.jsonpath.util.Global; import io.github.xmljim.json.model.JsonArray; diff --git a/jsonpath/src/main/java/io/github/xmljim/json/jsonpath/filter/UnionFilter.java b/jsonpath/src/main/java/io/github/xmljim/json/jsonpath/filter/UnionFilter.java index 6421be0..a1fafe7 100644 --- a/jsonpath/src/main/java/io/github/xmljim/json/jsonpath/filter/UnionFilter.java +++ b/jsonpath/src/main/java/io/github/xmljim/json/jsonpath/filter/UnionFilter.java @@ -1,5 +1,6 @@ package io.github.xmljim.json.jsonpath.filter; +import io.github.xmljim.json.jsonpath.context.Accessor; import io.github.xmljim.json.jsonpath.context.Context; import io.github.xmljim.json.jsonpath.util.Global; import io.github.xmljim.json.model.JsonArray; diff --git a/jsonpath/src/main/java/io/github/xmljim/json/jsonpath/function/FunctionFactory.java b/jsonpath/src/main/java/io/github/xmljim/json/jsonpath/function/FunctionFactory.java index efff626..e8e0447 100644 --- a/jsonpath/src/main/java/io/github/xmljim/json/jsonpath/function/FunctionFactory.java +++ b/jsonpath/src/main/java/io/github/xmljim/json/jsonpath/function/FunctionFactory.java @@ -7,14 +7,28 @@ import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; -import java.util.*; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Deque; +import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; public class FunctionFactory { - private static final String FUNCTION_PATTERN = "(?[a-z0-9\\-]+)\\((?.*)\\)"; - private static final String ARGS_PATTERN = "(([@$.a-zA-Z\\d_\\-'{}*#]+(\\[[,:a-z\\d'-]+])*)(\\s[!<>=a-z]+\\s\\2?)?(\\s?[&|]{2}\\s?)?)+";//"(([@$.a-zA-Z0-9_\\-'{}#\\[,:\\]]+)(\\s?[=!<>a-z]+\\s)?([@$.a-zA-Z0-9_\\-'{}#\\[,:\\]]+)?(\\s?[&|]+\\s?)?([@$.a-zA-Z0-9_\\-'{}#\\[,:\\]]+)(\\s?[=!<>a-z]+\\s)?([@$.a-zA-Z0-9_\\-'{}#\\[,\\]]+)?)(,\\s?((([@$.a-zA-Z0-9_\\-'{}#\\[,:\\]]+)(\\s?[=!<>a-z]+\\s)?([@$.a-zA-Z0-9_\\-'{}#\\[,:\\]]+)?(\\s?[&|]+\\s?)?([@$.a-zA-Z0-9_\\-'{}#\\[,:\\]]+)(\\s?[=!<>a-z]+\\s)?([@$.a-zA-Z0-9_\\-'{}#\\[,:\\]]+)?)))*"; + private static final String FUNCTION_PATTERN = "(?[a-zA-Z0-9_\\-]+)\\((?.*)\\)"; + private static final String PATH_PATTERN = "([@$.a-zA-Z\\d_\\-'{}*#]+(\\[[,:a-z\\d'-]+])*)"; + private static final String OPERATOR_PATTERN = "(\\s[!<>=a-z]+\\s\\2?)?"; + private static final String JUNCTION_PATTERN = "(\\s?[&|]{2}\\s?)?"; + private static final String MATH_OPERATOR_PATTERN = "(\\s?[+-*/%^])?"; + private static final String ARG_SEPARATOR_PATTERN = "(,\\s?)?"; + private static final String ARGUMENTS_PATTERN = "(?(" + PATH_PATTERN + MATH_OPERATOR_PATTERN + OPERATOR_PATTERN + JUNCTION_PATTERN + ARG_SEPARATOR_PATTERN + ")*)"; + private static final String FUNCTION_NAME = "(?[a-zA-Z0-9_\\-]+)"; + private static final String FULL_PATTERN = FUNCTION_NAME + "\\(" + ARGUMENTS_PATTERN + "\\)"; + private static final String ARGS_PATTERN = "(([@$.a-zA-Z\\d_\\-'{}*#]+(\\[[,:a-z\\d'-]+])*)(\\s[!<>=~a-z]+\\s\\2?)?(\\s?[&|]{2}\\s?)?)+";//"(([@$.a-zA-Z0-9_\\-'{}#\\[,:\\]]+)(\\s?[=!<>a-z]+\\s)?([@$.a-zA-Z0-9_\\-'{}#\\[,:\\]]+)?(\\s?[&|]+\\s?)?([@$.a-zA-Z0-9_\\-'{}#\\[,:\\]]+)(\\s?[=!<>a-z]+\\s)?([@$.a-zA-Z0-9_\\-'{}#\\[,\\]]+)?)(,\\s?((([@$.a-zA-Z0-9_\\-'{}#\\[,:\\]]+)(\\s?[=!<>a-z]+\\s)?([@$.a-zA-Z0-9_\\-'{}#\\[,:\\]]+)?(\\s?[&|]+\\s?)?([@$.a-zA-Z0-9_\\-'{}#\\[,:\\]]+)(\\s?[=!<>a-z]+\\s)?([@$.a-zA-Z0-9_\\-'{}#\\[,:\\]]+)?)))*"; + public static JsonPathFunction createFunction(String expression, Global global) { Pattern functionPattern = Pattern.compile(FUNCTION_PATTERN); @@ -24,8 +38,8 @@ public static JsonPathFunction createFunction(String expression, Global global) String fxName = matcher.group("function"); FunctionInfo functionInfo = global.getFunctionRegistry() - .getFunctionInfo(fxName) - .orElseThrow(() -> new JsonPathException("Function not found: " + fxName)); + .getFunctionInfo(fxName) + .orElseThrow(() -> new JsonPathException("Function not found: " + fxName)); String argsString = matcher.group("args"); @@ -50,8 +64,8 @@ public static JsonPathFunction createFunction(String expression, Global global) while (matcher.find()) { if (matcher.group() != null && !"".equals(matcher.group())) { argExpressions.add(matcher.group().strip().endsWith(",") ? - matcher.group().strip().substring(0, matcher.group().strip().length() - 1) : - matcher.group().strip()); + matcher.group().strip().substring(0, matcher.group().strip().length() - 1) : + matcher.group().strip()); } } if (functionInfo.arguments().length != 0) { diff --git a/jsonpath/src/main/java/io/github/xmljim/json/jsonpath/predicate/IsNotNullPredicate.java b/jsonpath/src/main/java/io/github/xmljim/json/jsonpath/predicate/IsNotNullPredicate.java index d7c5331..774d9db 100644 --- a/jsonpath/src/main/java/io/github/xmljim/json/jsonpath/predicate/IsNotNullPredicate.java +++ b/jsonpath/src/main/java/io/github/xmljim/json/jsonpath/predicate/IsNotNullPredicate.java @@ -1,7 +1,7 @@ package io.github.xmljim.json.jsonpath.predicate; +import io.github.xmljim.json.jsonpath.context.Accessor; import io.github.xmljim.json.jsonpath.context.Context; -import io.github.xmljim.json.jsonpath.filter.Accessor; import io.github.xmljim.json.jsonpath.filter.Filter; import io.github.xmljim.json.jsonpath.filter.FilterType; import io.github.xmljim.json.jsonpath.predicate.expression.Expression; diff --git a/jsonpath/src/main/java/io/github/xmljim/json/jsonpath/predicate/LessThanOrEqualPredicate.java b/jsonpath/src/main/java/io/github/xmljim/json/jsonpath/predicate/LessThanOrEqualPredicate.java index 9152b04..31be110 100644 --- a/jsonpath/src/main/java/io/github/xmljim/json/jsonpath/predicate/LessThanOrEqualPredicate.java +++ b/jsonpath/src/main/java/io/github/xmljim/json/jsonpath/predicate/LessThanOrEqualPredicate.java @@ -21,7 +21,7 @@ public boolean test(Context context) { int size = leftSide().size(context); List vals = leftSide().values(context); if ((leftSide().size(context) == 1 && leftSide().getContextType(context).isNumeric()) - && (rightSide().size(context) == 1 && rightSide().getContextType(context).isNumeric())) { + && (rightSide().size(context) == 1 && rightSide().getContextType(context).isNumeric())) { Number left = (Number) leftSide().getValue(context).orElse(0); Number right = (Number) rightSide().getValue(context).orElse(-1); diff --git a/jsonpath/src/main/java/io/github/xmljim/json/jsonpath/predicate/expression/FunctionExpression.java b/jsonpath/src/main/java/io/github/xmljim/json/jsonpath/predicate/expression/FunctionExpression.java index a27a4a2..7701f2d 100644 --- a/jsonpath/src/main/java/io/github/xmljim/json/jsonpath/predicate/expression/FunctionExpression.java +++ b/jsonpath/src/main/java/io/github/xmljim/json/jsonpath/predicate/expression/FunctionExpression.java @@ -1,18 +1,16 @@ package io.github.xmljim.json.jsonpath.predicate.expression; +import io.github.xmljim.json.jsonpath.context.Context; import io.github.xmljim.json.jsonpath.function.FunctionFactory; import io.github.xmljim.json.jsonpath.function.JsonPathFunction; -import io.github.xmljim.json.jsonpath.util.Global; -import io.github.xmljim.json.jsonpath.context.Context; import io.github.xmljim.json.jsonpath.util.DataType; +import io.github.xmljim.json.jsonpath.util.Global; -import java.util.List; -import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Stream; public class FunctionExpression extends CachedExpression { private final JsonPathFunction function; - private final ConcurrentHashMap> concurrentHashMap = new ConcurrentHashMap<>(); + //private final ConcurrentHashMap> concurrentHashMap = new ConcurrentHashMap<>(); public FunctionExpression(String expression, Global global) { super(expression, global); diff --git a/jsonpath/src/test/java/io/github/xmljim/json/jsonpath/test/compiler/CompilerTest.java b/jsonpath/src/test/java/io/github/xmljim/json/jsonpath/test/compiler/CompilerTest.java index 14f15a4..4ed2d5c 100644 --- a/jsonpath/src/test/java/io/github/xmljim/json/jsonpath/test/compiler/CompilerTest.java +++ b/jsonpath/src/test/java/io/github/xmljim/json/jsonpath/test/compiler/CompilerTest.java @@ -14,8 +14,8 @@ class CompilerTest extends JsonPathTestBase { @Test - @DisplayName("Given an expression with a root select and bracketed property key, then return a sequence" + - "containing a RootPathOperator and a ChildPathOperator") + @DisplayName("Given an expression with a root select and bracketed property key, then return a sequence " + + "containing a RootPathOperator and a ChildPathOperator") void testChildPathWithBrackets() { String expression = "$['foo']"; FilterStream sequence = getFilterStream(expression); @@ -27,7 +27,7 @@ void testChildPathWithBrackets() { @Test @DisplayName("Given an expression with a root selector and 'dot' property key, then return a sequence" + - "containing a RootPathOperator and a ChildPathOperator") + "containing a RootPathOperator and a ChildPathOperator") void testChildPathWithDot() { String expression = "$.foo"; FilterStream sequence = getFilterStream(expression); diff --git a/jsonpath/src/test/java/io/github/xmljim/json/jsonpath/test/filter/AccessorTest.java b/jsonpath/src/test/java/io/github/xmljim/json/jsonpath/test/filter/AccessorTest.java index 39bbaf2..dc16a80 100644 --- a/jsonpath/src/test/java/io/github/xmljim/json/jsonpath/test/filter/AccessorTest.java +++ b/jsonpath/src/test/java/io/github/xmljim/json/jsonpath/test/filter/AccessorTest.java @@ -1,6 +1,6 @@ package io.github.xmljim.json.jsonpath.test.filter; -import io.github.xmljim.json.jsonpath.filter.Accessor; +import io.github.xmljim.json.jsonpath.context.Accessor; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; diff --git a/jsonpath/src/test/java/io/github/xmljim/json/jsonpath/test/function/FunctionTests.java b/jsonpath/src/test/java/io/github/xmljim/json/jsonpath/test/function/FunctionTests.java index 3a23ddd..c8d0cce 100644 --- a/jsonpath/src/test/java/io/github/xmljim/json/jsonpath/test/function/FunctionTests.java +++ b/jsonpath/src/test/java/io/github/xmljim/json/jsonpath/test/function/FunctionTests.java @@ -11,7 +11,10 @@ import java.util.List; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; @DisplayName("Built-in Function Tests") class FunctionTests extends JsonPathTestBase { @@ -70,17 +73,17 @@ void testCountIfFunctionCompoundTest_GreaterThanThree() { //now the ugly way: JsonArray compoundArray = getCompoundTest(); List result = compoundArray.jsonValues().map(JsonElement::asJsonObject) - .filter(jsonObject -> jsonObject.getValue("assignments").asJsonArray().jsonValues() - .filter(assignment -> { - JsonObject assigned = assignment.asJsonObject(); - return (assigned.getValue("doc_type_id").asNumber().intValue() == 19 && - assigned.getValue("doc_status").asString().equals("PROCESSED")); - }).count() >= 3) - .toList(); + .filter(jsonObject -> jsonObject.getValue("assignments").asJsonArray().jsonValues() + .filter(assignment -> { + JsonObject assigned = assignment.asJsonObject(); + return (assigned.getValue("doc_type_id").asNumber().intValue() == 19 && + assigned.getValue("doc_status").asString().equals("PROCESSED")); + }).count() >= 3) + .toList(); assertEquals(array.size(), result.size()); assertEquals(result.get(0).value("id").orElseThrow(() -> new Exception("Bad Expected Value")), - array.getValue(0).asJsonObject().value("id").orElseThrow()); + array.getValue(0).asJsonObject().value("id").orElseThrow()); } catch (Exception e) { fail(e); @@ -96,17 +99,17 @@ void testCountIfFunctionCompoundTest_LessThanThree() { //now the ugly way: JsonArray compoundArray = getCompoundTest(); List result = compoundArray.jsonValues().map(JsonElement::asJsonObject) - .filter(jsonObject -> jsonObject.getValue("assignments").asJsonArray().jsonValues() - .filter(assignment -> { - JsonObject assigned = assignment.asJsonObject(); - return (assigned.getValue("doc_type_id").asNumber().intValue() == 19 && - assigned.getValue("doc_status").asString().equals("PROCESSED")); - }).count() < 3) - .toList(); + .filter(jsonObject -> jsonObject.getValue("assignments").asJsonArray().jsonValues() + .filter(assignment -> { + JsonObject assigned = assignment.asJsonObject(); + return (assigned.getValue("doc_type_id").asNumber().intValue() == 19 && + assigned.getValue("doc_status").asString().equals("PROCESSED")); + }).count() < 3) + .toList(); assertEquals(array.size(), result.size()); assertEquals(result.get(0).value("id").orElseThrow(() -> new Exception("Bad Expected Value")), - array.getValue(0).asJsonObject().value("id").orElseThrow()); + array.getValue(0).asJsonObject().value("id").orElseThrow()); } catch (Exception e) { fail(e); @@ -232,6 +235,9 @@ void testEndsWithFunction() { expr = "$.teams.*[?(@.id == 'COL')].stadium.ends-with('Field')"; assertTrue((boolean) jsonPath.select(baseball, expr).get(0)); + boolean endsWithField = jsonPath.selectValue(baseball, expr); + assertTrue(endsWithField); + } catch (Exception e) { fail(e); } diff --git a/jsonpath/src/test/java/module-info.java b/jsonpath/src/test/java/module-info.java index b5f6171..e70605f 100644 --- a/jsonpath/src/test/java/module-info.java +++ b/jsonpath/src/test/java/module-info.java @@ -19,4 +19,5 @@ opens io.github.xmljim.json.jsonpath.test.predicate.expression; opens io.github.xmljim.json.jsonpath.test.function.node; opens io.github.xmljim.json.jsonpath.test.filter; + opens io.github.xmljim.json.jsonpath.test.util; } \ No newline at end of file diff --git a/mapper/pom.xml b/mapper/pom.xml index afbbfb8..516577e 100644 --- a/mapper/pom.xml +++ b/mapper/pom.xml @@ -23,12 +23,14 @@ jsonfactory ${project.version} + io.github.xmljim.json elementfactory ${project.version} test + diff --git a/mapper/src/main/java/io/github/xmljim/json/mapper/AnnotationUtils.java b/mapper/src/main/java/io/github/xmljim/json/mapper/AnnotationUtils.java index 3782172..dce8c55 100644 --- a/mapper/src/main/java/io/github/xmljim/json/mapper/AnnotationUtils.java +++ b/mapper/src/main/java/io/github/xmljim/json/mapper/AnnotationUtils.java @@ -13,7 +13,7 @@ import java.util.Map; import java.util.Optional; -interface AnnotationUtils { +abstract class AnnotationUtils { /** * Find and return an Optional containing an Annotation * @@ -22,7 +22,7 @@ interface AnnotationUtils { * @param classMember The class member (either a Field, Method or Class) * @return an Optional containing the Annotation */ - static Optional findAnnotation(Class annotationClass, AnnotatedElement classMember) { + public static Optional findAnnotation(Class annotationClass, AnnotatedElement classMember) { T annotation = null; if (classMember.isAnnotationPresent(annotationClass)) { @@ -32,7 +32,7 @@ static Optional findAnnotation(Class anno return Optional.ofNullable(annotation); } - static Optional> getConvertToJson(AnnotatedElement element) { + public static Optional> getConvertToJson(AnnotatedElement element) { ValueConverter convert = null; final Optional valueConverter = findAnnotation(ConvertValue.class, element); @@ -46,7 +46,7 @@ static Optional> getConvertToJson(AnnotatedElement element) { return Optional.ofNullable(convert); } - static Optional> getConvertToValue(AnnotatedElement element) { + public static Optional> getConvertToValue(AnnotatedElement element) { ValueConverter convert = null; final Optional valueConverter = findAnnotation(ConvertValue.class, element); @@ -59,11 +59,11 @@ static Optional> getConvertToValue(AnnotatedElement element) { return Optional.ofNullable(convert); } - static boolean findJsonElementIgnore(AnnotatedElement element) { + public static boolean findJsonElementIgnore(AnnotatedElement element) { return findAnnotation(JsonElement.class, element).map(JsonElement::ignore).orElse(false); } - static Optional findJsonElementKey(AnnotatedElement element) { + public static Optional findJsonElementKey(AnnotatedElement element) { return findAnnotation(JsonElement.class, element).filter(a -> !"".equals(a.key())).map(JsonElement::key); } @@ -71,11 +71,11 @@ static Optional findJsonElementSetter(AnnotatedElement element) { return findAnnotation(JsonElement.class, element).filter(a -> !"".equals(a.setterMethod())).map(JsonElement::setterMethod); } - static Optional findJsonElementGetter(AnnotatedElement element) { + public static Optional findJsonElementGetter(AnnotatedElement element) { return findAnnotation(JsonElement.class, element).filter(a -> !"".equals(a.getterMethod())).map(JsonElement::getterMethod); } - static Optional> findConvertClass(AnnotatedElement element) { + public static Optional> findConvertClass(AnnotatedElement element) { return findAnnotation(ConvertClass.class, element).map(ConvertClass::target); } } diff --git a/mapper/src/main/java/io/github/xmljim/json/mapper/ClassMappingImpl.java b/mapper/src/main/java/io/github/xmljim/json/mapper/ClassMappingImpl.java new file mode 100644 index 0000000..027de29 --- /dev/null +++ b/mapper/src/main/java/io/github/xmljim/json/mapper/ClassMappingImpl.java @@ -0,0 +1,265 @@ +package io.github.xmljim.json.mapper; + +import io.github.xmljim.json.factory.mapper.*; +import io.github.xmljim.json.factory.model.ElementFactory; +import io.github.xmljim.json.mapper.exception.JsonMapperException; +import io.github.xmljim.json.model.JsonObject; + +import java.lang.reflect.*; +import java.util.*; +import java.util.stream.Stream; + +class ClassMappingImpl implements ClassMapping { + + + private final ClassConfig classConfig; + + private boolean initialized; + private final Set ignoredKeys = new HashSet<>(); + + private Class sourceClass; + private Class targetClass; + private boolean isMemberClass; + + private boolean isRecord; + private boolean isPublic; + + private final List constructorKeys = new ArrayList<>(); + + private final MapperFactory mapperFactory; + private final Mapping mapping; + + private final List memberMappings = new ArrayList<>(); + + public ClassMappingImpl(Mapping mapping, ClassConfig config) { + this.mapping = mapping; + this.classConfig = config; + this.mapperFactory = mapping.getMapperFactory(); + this.sourceClass = config.getSourceClass(); + this.targetClass = config.getTargetClass(); + initialize(); + } + + + @Override + public ClassConfig getClassConfig() { + return classConfig; + } + + @Override + public Mapping getMapping() { + return this.mapping; + } + + @Override + @SuppressWarnings("unchecked") + public Class getSourceClass() { + return (Class) sourceClass; + } + + protected void setTargetClass(Class targetClass) { + this.targetClass = targetClass; + } + + @Override + @SuppressWarnings("unchecked") + public Class getTargetClass() { + return (Class) targetClass; + } + + @Override + public boolean isPublic() { + return this.isPublic; + } + + + @Override + public boolean isRecord() { + return this.isRecord; + } + + @Override + public List getConstructorKeys() { + return constructorKeys; + } + + @Override + public MemberMapping getMemberMapping(Field field) { + return getMemberMappings().filter(memberMapping -> memberMapping.getFieldName().equals(field.getName())) + .findFirst().orElse(null); + } + + @Override + public MemberMapping getMemberMapping(String jsonKey) { + return getMemberMappings().filter(memberMapping -> memberMapping.getJsonKey().equals(jsonKey)) + .findFirst() + .orElse(null); + } + + @Override + public Set getIgnoredKeys() { + return ignoredKeys; + } + + @Override + public void appendMemberMapping(MemberMapping memberMapping) { + memberMappings.add(memberMapping); + } + + private void initialize() { + if (initialized) { + return; + } + + if (getClassConfig().getSourceClass() == null) { + throw new JsonMapperException("Invalid ClassMapping: Source Class is required"); + } + + if (getClassConfig().getTargetClass().isInterface() || Modifier.isAbstract(getTargetClass().getModifiers())) { + throw new JsonMapperException("Invalid target class [" + getTargetClass().getName() + "]. Target cannot be" + + " an interface or abstract class"); + } + + if (getClassConfig().getSourceClass() != null) { + //class must be same or inheritable from source + if (!getClassConfig().getSourceClass().isAssignableFrom(getTargetClass())) { + throw new JsonMapperException("Invalid target class [" + getTargetClass().getName() + "]. Target class " + + "must be assignable from source class [" + getTargetClass().getName() + "]"); + } + } + + this.isMemberClass = getTargetClass().isMemberClass(); + this.isRecord = getTargetClass().isRecord(); + this.isPublic = Modifier.isPublic(getTargetClass().getModifiers()); + + + scanClass(this.targetClass); + + if (isRecord()) { + getMemberMappings().forEach(memberMapping -> this.constructorKeys.add(memberMapping.getJsonKey())); + } + + initialized = true; + } + + /** + * Create a new JSONObject from a class source + * + * @param source the class instance + * @param elementFactory The element factory that will be used to create the source + * @param mapperFactory the mapper factory + * @param mapper the mapper + * @param the class type + * @return a new JsonObject + */ + public JsonObject apply(T source, ElementFactory elementFactory, MapperFactory mapperFactory, Mapper mapper) { + JsonObject newObject = elementFactory.newObject(); + getMemberMappings().forEach(memberMapping -> memberMapping.applyToJson(source, newObject)); + return newObject; + } + + public T apply(JsonObject source, ElementFactory elementFactory, MapperFactory mapperFactory, Mapper mapper) { + return null; + } + + private void scanClass(Class classToScan) { + Arrays.stream(classToScan.getDeclaredFields()).forEach(field -> addEntry(classToScan, field)); + getSuperclass(classToScan).ifPresent(this::scanClass); + } + + private Optional> getSuperclass(Class clazz) { + @SuppressWarnings("unchecked") final Class superclass = (Class) clazz.getSuperclass(); + final Class willSend = superclass != Object.class ? superclass : null; + return Optional.ofNullable(willSend); + } + + private void addEntry(Class owningClass, Field field) { + if (!hasMemberMapping(owningClass, field)) { + + MemberConfig config = MemberConfig.with() + .containerClass(owningClass) + .field(field) + .build(); + + MemberMapping memberMapping = mapperFactory.newMemberMapping(this, config); + + //MemberMappingImpl memberMapping = new MemberMappingImpl(this, owningClass, field); + memberMappings.add(memberMapping); + if (memberMapping.isIgnored()) { + ignoredKeys.add(memberMapping.getJsonKey()); + } + } + } + + private boolean hasMemberMapping(Class owningClass, Field field) { + return getMemberMappings().anyMatch(memberMapping -> memberMapping.getContainerClass().equals(owningClass) + && memberMapping.getFieldName().equals(field.getName())); + } + + @Override + public Stream getMemberMappings() { + return memberMappings.stream();//return memberMappings.stream().filter(memberMapping -> !memberMapping.isIgnored()); + } + + + @SuppressWarnings("unchecked") + public Constructor getConstructor() { + Class[] argTypes = getConstructorKeys().stream() + .map(this::getMemberMapping) + .map(memberMapping -> getRawType(memberMapping.getFieldType())).toList() + .toArray(new Class[]{}); + + + try { + return (Constructor) targetClass.getDeclaredConstructor(argTypes); + } catch (NoSuchMethodException e) { + StringBuilder builder = new StringBuilder(); + Arrays.stream(argTypes).forEach(c -> builder.append(c.toString()).append(", ")); + throw new JsonMapperException("No constructor found with args: [" + builder.toString().substring(0, builder.toString().length() - 2) + "]"); + } + } + + @SuppressWarnings("unchecked") + private Class getRawType(Type type) { + if (type instanceof ParameterizedType) { + return (Class) ((ParameterizedType) type).getRawType(); + } + return (Class) type; + } + + @Override + public T toClass(JsonObject jsonObject) { + T classInstance = newInstance(jsonObject); + List assignable = getAssignableMembers().toList(); + getAssignableMembers().forEach(memberMapping -> memberMapping.applyToClass(jsonObject, classInstance)); + return classInstance; + + } + + private Stream getAssignableMembers() { + return memberMappings.stream() + .filter(memberMapping -> !memberMapping.isIgnored()) + .filter(memberMapping -> !getIgnoredKeys().contains(memberMapping.getJsonKey())) + .filter(memberMapping -> !getConstructorKeys().contains(memberMapping.getJsonKey())); + } + + public T newInstance(JsonObject jsonObject) { + Object[] args = getConstructorArgs(jsonObject); + try { + Constructor constructor = getConstructor(); + return constructor.newInstance(args); + } catch (SecurityException | InstantiationException | IllegalAccessException | + IllegalArgumentException | InvocationTargetException e) { + throw new JsonMapperException("Error creating class: " + targetClass + ": " + e.getMessage(), e); + } + } + + private Object[] getConstructorArgs(JsonObject jsonObject) { + return getConstructorKeys().stream() + .map(this::getMemberMapping) + .map(memberMapping -> memberMapping.getValue(jsonObject)) + .toList() + .toArray(new Object[]{}); + } + +} diff --git a/mapper/src/main/java/io/github/xmljim/json/mapper/ClassMember.java b/mapper/src/main/java/io/github/xmljim/json/mapper/ClassMember.java index dbd7d25..6fdb9bb 100644 --- a/mapper/src/main/java/io/github/xmljim/json/mapper/ClassMember.java +++ b/mapper/src/main/java/io/github/xmljim/json/mapper/ClassMember.java @@ -69,7 +69,7 @@ private MethodReference findMethod(Class clazz, Field field, boolean setter) final Type returnType = setter ? Void.TYPE : field.getType();//field.getGenericType() != null ? (ParameterizedType)field.getGenericType() : field.getType(); final List methods = findMethods(clazz, methodName, paramCount, returnType); - Optional m = Optional.empty(); + Optional m; if (setter) { m = methods.stream().filter(method -> getMethodParameterType(method).equals(getFieldType(field))).findFirst(); diff --git a/mapper/src/main/java/io/github/xmljim/json/mapper/ClassUtils.java b/mapper/src/main/java/io/github/xmljim/json/mapper/ClassUtils.java index fd44ff4..2f3ad0d 100644 --- a/mapper/src/main/java/io/github/xmljim/json/mapper/ClassUtils.java +++ b/mapper/src/main/java/io/github/xmljim/json/mapper/ClassUtils.java @@ -9,7 +9,7 @@ import java.util.Map; import java.util.Optional; -interface ClassUtils { +abstract class ClassUtils { /** * Create a new class instance * @@ -18,7 +18,7 @@ interface ClassUtils { * @return a new instance of the class; * @throws JsonMapperException thrown if an error occurs creating the class */ - static T createInstance(Class classToCreate) { + public static T createInstance(Class classToCreate) { final Optional> constructor = getDefaultConstructor(classToCreate); if (constructor.isPresent()) { @@ -47,7 +47,7 @@ private static Optional> getDefaultConstructor(Class class .filter(constructor -> constructor.getParameterCount() == 0).findFirst(); } - static T createInstance(Class classToCreate, Class[] argTypes, Object... args) { + public static T createInstance(Class classToCreate, Class[] argTypes, Object... args) { try { Constructor constructor; constructor = classToCreate.getConstructor(argTypes); @@ -58,14 +58,14 @@ static T createInstance(Class classToCreate, Class[] argTypes, Object. } } - static ValueConverter createValueConverter(Class classToCreate, Map args) { + public static ValueConverter createValueConverter(Class classToCreate, Map args) { if (classToCreate == null) { return null; } try { Constructor con; - con = classToCreate.getConstructor(String[].class); + con = classToCreate.getConstructor(Map.class); con.setAccessible(true); return (ValueConverter) con.newInstance(args); } catch (NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException | @@ -74,4 +74,6 @@ static ValueConverter createValueConverter(Class classToCreate, Map map) { JsonObject jsonObject = elementFactory.newObject(); @@ -82,19 +88,19 @@ public List toList(JsonArray jsonArray) { @SuppressWarnings("unchecked") public JsonValue toValue(Object value, JsonElement parent) { if (value == null) { - return elementFactory.newValue(null, parent); + return elementFactory.newValue(null); } else if (value instanceof String v) { - return elementFactory.newValue(v, parent); + return elementFactory.newValue(v); } else if (value instanceof Boolean v) { - return elementFactory.newValue(v, parent); + return elementFactory.newValue(v); } else if (value instanceof Number v) { - return elementFactory.newValue(v, parent); + return elementFactory.newValue(v); } else if (value instanceof Map v) { - return elementFactory.newValue(toJson(v), parent); + return elementFactory.newValue(toJson(v)); } else if (value instanceof Collection v) { - return elementFactory.newValue(toJson(v), parent); + return elementFactory.newValue(toJson(v)); } else { - return elementFactory.newValue(toJson(value), parent); + return elementFactory.newValue(toJson(value)); } } diff --git a/mapper/src/main/java/io/github/xmljim/json/mapper/MapperBuilderImpl.java b/mapper/src/main/java/io/github/xmljim/json/mapper/MapperBuilderImpl.java deleted file mode 100644 index 2799759..0000000 --- a/mapper/src/main/java/io/github/xmljim/json/mapper/MapperBuilderImpl.java +++ /dev/null @@ -1,67 +0,0 @@ -package io.github.xmljim.json.mapper; - -import io.github.xmljim.json.factory.mapper.*; -import io.github.xmljim.json.mapper.exception.JsonMapperConfigurationException; - -import java.util.Arrays; -import java.util.Collection; -import java.util.Objects; -import java.util.stream.Collectors; - -class MapperBuilderImpl implements MapperBuilder { - private final MapperConfigImpl mapperConfig = new MapperConfigImpl(); - private final MapperFactory factory; - - public MapperBuilderImpl(MapperFactory factory) { - this.factory = factory; - } - - @Override - public Mapper build() { - return new DefaultMapper(mapperConfig, factory); - } - - @Override - public MapperBuilder setTargetClass(Class targetClass) { - if (targetClass != null) { - if (mapperConfig.getValueConverter().isPresent()) { - throw new JsonMapperConfigurationException("Cannot assign a target class when a ValueConverter is already assigned"); - } - mapperConfig.setTargetClass(Objects.requireNonNull(targetClass)); - } else { - setTargetClass(null); - } - - return this; - } - - @Override - public MapperBuilder setValueConverter(ValueConverter valueConverter) { - if (valueConverter != null) { - if (mapperConfig.getTargetClass().isPresent()) { - throw new JsonMapperConfigurationException("Cannot assign a ValueConverter when a Target class is already assigned"); - } - mapperConfig.setValueConverter(Objects.requireNonNull(valueConverter)); - } else { - mapperConfig.setValueConverter(null); - } - return this; - } - - @Override - public MapperBuilder setKeyNameCase(KeyNameCase keyNameCase) { - mapperConfig.setKeyNameCase(keyNameCase); - return this; - } - - @Override - public MapperBuilder setIgnoreKeys(Collection ignoreKeys) { - mapperConfig.setIgnoreKeys(ignoreKeys); - return this; - } - - @Override - public MapperBuilder setIgnoreKeys(String... ignoreKeys) { - return setIgnoreKeys(Arrays.stream(ignoreKeys).collect(Collectors.toSet())); - } -} diff --git a/mapper/src/main/java/io/github/xmljim/json/mapper/MapperFactoryImpl.java b/mapper/src/main/java/io/github/xmljim/json/mapper/MapperFactoryImpl.java index f7eb4cc..a2ac3a8 100644 --- a/mapper/src/main/java/io/github/xmljim/json/mapper/MapperFactoryImpl.java +++ b/mapper/src/main/java/io/github/xmljim/json/mapper/MapperFactoryImpl.java @@ -1,14 +1,50 @@ package io.github.xmljim.json.mapper; -import io.github.xmljim.json.factory.mapper.MapperBuilder; -import io.github.xmljim.json.factory.mapper.MapperFactory; +import io.github.xmljim.json.factory.mapper.*; +import io.github.xmljim.json.factory.mapper.parser.MappingParser; +import io.github.xmljim.json.factory.parser.Parser; +import io.github.xmljim.json.factory.parser.ParserBuilder; +import io.github.xmljim.json.factory.parser.ParserFactory; +import io.github.xmljim.json.mapper.parser.MappingParserImpl; import io.github.xmljim.json.service.JsonServiceProvider; +import io.github.xmljim.json.service.ServiceManager; @JsonServiceProvider(service = MapperFactory.class, isNative = true, version = "1.0.1") public class MapperFactoryImpl implements MapperFactory { @Override - public MapperBuilder newBuilder() { - return new MapperBuilderImpl(this); + public Mapper newMapper() { + return new MapperImpl(this, newMapping(MappingConfig.empty())); + } + + public Mapper newMapper(Mapping mapping) { + return new MapperImpl(this, mapping); + } + + @Override + public ClassMapping newClassMapping(Mapping mapping, ClassConfig classConfig) { + return new ClassMappingImpl(mapping, classConfig); + } + + @Override + public MemberMapping newMemberMapping(ClassMapping classMapping, MemberConfig memberConfig) { + return new MemberMappingImpl(classMapping, memberConfig); + } + + @Override + public Mapping newMapping(MappingConfig mappingConfig) { + return new MappingImpl(this, mappingConfig); + } + + @Override + public MappingParser newMappingParser(MappingParserConfig mappingParserConfig) { + ParserFactory parserFactory = ServiceManager.getProvider(ParserFactory.class); + ParserBuilder parserBuilder = parserFactory.newParserBuilder(); + parserBuilder.mergeSettings(mappingParserConfig); + Parser parser = parserBuilder.build(); + + Mapper mapper = newMapper(mappingParserConfig); + + return new MappingParserImpl(mapper, parser); } } diff --git a/mapper/src/main/java/io/github/xmljim/json/mapper/MapperImpl.java b/mapper/src/main/java/io/github/xmljim/json/mapper/MapperImpl.java new file mode 100644 index 0000000..a134bf9 --- /dev/null +++ b/mapper/src/main/java/io/github/xmljim/json/mapper/MapperImpl.java @@ -0,0 +1,131 @@ +package io.github.xmljim.json.mapper; + +import io.github.xmljim.json.factory.mapper.*; +import io.github.xmljim.json.factory.model.ElementFactory; +import io.github.xmljim.json.mapper.exception.JsonMapperException; +import io.github.xmljim.json.model.JsonArray; +import io.github.xmljim.json.model.JsonElement; +import io.github.xmljim.json.model.JsonObject; +import io.github.xmljim.json.model.JsonValue; +import io.github.xmljim.json.service.ServiceManager; + +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +class MapperImpl implements Mapper { + private final Mapping mapping; + private final MapperFactory mapperFactory; + + private final ElementFactory elementFactory; + + public MapperImpl(MapperFactory mapperFactory, Mapping mapping) { + this.mapperFactory = mapperFactory; + this.mapping = mapping; + if (ServiceManager.isServiceAvailable(ElementFactory.class)) { + this.elementFactory = ServiceManager.getProvider(ElementFactory.class); + } else { + throw new JsonMapperException(ElementFactory.class.getSimpleName() + " service provider is not available"); + } + } + + @Override + public MapperConfig getConfig() { + return null; + } + + @Override + public Mapping getMapping() { + return mapping; + } + + @Override + public JsonObject toJson(Map map) { + JsonObject jsonObject = elementFactory.newObject(); + map.forEach((key, value) -> { + jsonObject.put(key, toValue(value, jsonObject)); + }); + + return jsonObject; + } + + @Override + public JsonObject toJson(Object o) { + ClassMapping classMapping = mapping.getClassMapping(o.getClass()); + JsonObject jsonObject = elementFactory.newObject(); + classMapping.getMemberMappings().forEach(memberMapping -> memberMapping.applyToJson(o, jsonObject)); + return jsonObject; + } + + @Override + public JsonArray toJson(Collection collection) { + JsonArray jsonArray = elementFactory.newArray(); + collection.forEach(item -> jsonArray.add(toValue(item, jsonArray))); + return jsonArray; + } + + @Override + public Map toMap(JsonObject jsonObject) { + Map newMap = new HashMap<>(); + + jsonObject.keys().forEach(key -> { + if (!getConfig().getIgnoreKeys().contains(key)) { + newMap.put(key, convertJsonValue(jsonObject.value(key).orElse(elementFactory.newValue(null)))); + } + }); + + return newMap; + } + + @Override + public List toList(JsonArray jsonArray) { + return jsonArray.jsonValues().map(this::convertJsonValue).toList(); + } + + @Override + @SuppressWarnings("unchecked") + public JsonValue toValue(Object value, JsonElement parent) { + if (value == null) { + return elementFactory.newValue(null); + } else if (value instanceof String v) { + return elementFactory.newValue(v); + } else if (value instanceof Boolean v) { + return elementFactory.newValue(v); + } else if (value instanceof Number v) { + return elementFactory.newValue(v); + } else if (value instanceof Map v) { + return elementFactory.newValue(toJson(v)); + } else if (value instanceof Collection v) { + return elementFactory.newValue(toJson(v)); + } else { + return elementFactory.newValue(toJson(value)); + } + } + + @Override + public T toClass(JsonObject jsonObject) { + ClassMapping classMapping = mapping.getClassMappings().findFirst().orElseThrow(() -> new JsonMapperException("No Target Class Defined")); + return convertToClass(jsonObject, classMapping); + } + + @Override + public T toClass(JsonObject jsonObject, Class targetClass) { + ClassMapping classMapping = mapping.getClassMapping(targetClass); + return convertToClass(jsonObject, classMapping); + } + + private T convertToClass(JsonObject jsonObject, ClassMapping classMapping) { + return classMapping.toClass(jsonObject); + } + + private Object convertJsonValue(JsonValue jsonValue) { + if (jsonValue.type().isPrimitive()) { + return jsonValue.get(); + } else if (jsonValue.type().isArray()) { + return toList((JsonArray) jsonValue.get()); + } else { + return toMap((JsonObject) jsonValue.get()); + } + } +} diff --git a/mapper/src/main/java/io/github/xmljim/json/mapper/MappingImpl.java b/mapper/src/main/java/io/github/xmljim/json/mapper/MappingImpl.java new file mode 100644 index 0000000..216415b --- /dev/null +++ b/mapper/src/main/java/io/github/xmljim/json/mapper/MappingImpl.java @@ -0,0 +1,79 @@ +package io.github.xmljim.json.mapper; + +import io.github.xmljim.json.factory.mapper.*; + +import java.util.HashSet; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Stream; + +class MappingImpl implements Mapping { + + private final MapperFactory mapperFactory; + + private final Set classMappingSet = new HashSet<>(); + + public MappingImpl(MapperFactory mapperFactory, MappingConfig config) { + this.mapperFactory = mapperFactory; + config.getClassConfigurations().forEach(classConfig -> append(mapperFactory.newClassMapping(this, classConfig))); + } + + @Override + public MapperFactory getMapperFactory() { + return mapperFactory; + } + + @Override + public Stream getClassMappings() { + return classMappingSet.stream(); + } + + @Override + public ClassMapping getClassMapping(Class mappedClass) { + return + classMappingSet.stream() + .filter(containsMapping(mappedClass)) + .findFirst() + .orElseGet(() -> createClassMapping(mappedClass)); + } + + @Override + public boolean containsClassMapping(Class mappedClass) { + return classMappingSet.stream().anyMatch(containsMapping(mappedClass)); + + } + + + private Predicate containsMapping(Class mappedClass) { + return containsSourceMapping(mappedClass).or(containsTargetMapping(mappedClass)); + } + + private Predicate containsSourceMapping(Class mappedClass) { + + return classMapping -> { + System.out.println(classMapping.getSourceClass().getName().equals(mappedClass.getName())); + return classMapping.getSourceClass().getName().equals(mappedClass.getName()); + }; + } + + private Predicate containsTargetMapping(Class mappedClass) { + return classMapping -> classMapping.getTargetClass().getName().equals(mappedClass.getName()); + } + + private ClassMapping createClassMapping(Class mappedClass) { + ClassConfig config = ClassConfig.with().sourceClass(mappedClass).build(); + ClassMapping classMapping = getMapperFactory().newClassMapping(this, config); + append(classMapping); + return classMapping; + } + + @Override + public void append(ClassMapping classMapping) { + classMappingSet.add(classMapping); + } + + @Override + public void append(Class classReference) { + append(createClassMapping(classReference)); + } +} diff --git a/mapper/src/main/java/io/github/xmljim/json/mapper/MemberHandler.java b/mapper/src/main/java/io/github/xmljim/json/mapper/MemberHandler.java index 02593c9..d75c1d0 100644 --- a/mapper/src/main/java/io/github/xmljim/json/mapper/MemberHandler.java +++ b/mapper/src/main/java/io/github/xmljim/json/mapper/MemberHandler.java @@ -1,7 +1,6 @@ package io.github.xmljim.json.mapper; import io.github.xmljim.json.factory.mapper.Mapper; -import io.github.xmljim.json.factory.mapper.MapperBuilder; import io.github.xmljim.json.factory.mapper.MapperFactory; import io.github.xmljim.json.mapper.exception.JsonMapperException; import io.github.xmljim.json.model.JsonArray; @@ -31,7 +30,7 @@ public ClassMember getClassMember() { } private Optional handleJSONValue(JsonValue value) { - Object returnValue = null; + Object returnValue; if (getClassMember().isIgnored()) { return Optional.empty(); @@ -44,9 +43,10 @@ private Optional handleJSONValue(JsonValue value) { } else { final Class targetClass = getClassMember().getTargetClass(); if (value.type().isArray()) { + assert buildTargetMapper() != null; returnValue = buildTargetMapper().toList((JsonArray) value.get()); //.toList((JsonArray) value.value(), buildMapperConfig()); } else { - returnValue = getMapper().toClass((JsonObject) value.get(), targetClass); //getMapper().convertToClass((JsonObject) value.value(), targetClass); + returnValue = getMapper().toClass((JsonObject) value.get(), targetClass); //getValueMapperFunction().convertToClass((JsonObject) value.value(), targetClass); } } @@ -57,20 +57,25 @@ private MapperFactory getFactory() { return factory; } + /* private MapperBuilder getMapperBuilder() { - return getFactory().newBuilder(); + return null;// getFactory().newBuilder(); } - +*/ private Mapper getMapper() { return mapper; } private Mapper buildTargetMapper() { + return null; + /* return getFactory().newBuilder() .merge(getMapper()) .setValueConverter(null) .setTargetClass(getClassMember().getTargetClass()) .build(); + + */ } public void setMemberValue(JsonValue value) { diff --git a/mapper/src/main/java/io/github/xmljim/json/mapper/MemberMappingImpl.java b/mapper/src/main/java/io/github/xmljim/json/mapper/MemberMappingImpl.java new file mode 100644 index 0000000..9f5b286 --- /dev/null +++ b/mapper/src/main/java/io/github/xmljim/json/mapper/MemberMappingImpl.java @@ -0,0 +1,392 @@ +package io.github.xmljim.json.mapper; + +import io.github.xmljim.json.factory.mapper.ClassMapping; +import io.github.xmljim.json.factory.mapper.Converter; +import io.github.xmljim.json.factory.mapper.MemberConfig; +import io.github.xmljim.json.factory.mapper.MemberMapping; +import io.github.xmljim.json.mapper.exception.JsonMapperException; +import io.github.xmljim.json.model.JsonArray; +import io.github.xmljim.json.model.JsonObject; +import io.github.xmljim.json.model.JsonValue; +import io.github.xmljim.json.model.NodeType; + +import java.lang.reflect.*; +import java.util.*; +import java.util.stream.Stream; + +class MemberMappingImpl implements MemberMapping { + private String jsonKey; + private NodeType jsonNodeType; + private Class containerClass; + private String setterMethodName; + private String getterMethodName; + private Method getterMethod; + private Method setterMethod; + private Converter jsonConverter; + private Converter fieldConverter; + private Class elementTargetClass; + private boolean ignored; + private Type fieldType; + + private String fieldName; + + private Field field; + + private final ClassMapping classMapping; + + private final MemberConfig memberConfig; + + protected MemberMappingImpl(ClassMapping classMapping, MemberConfig memberConfig) { + this.classMapping = classMapping; + this.memberConfig = memberConfig; + initialize(); + + } + + private Method findMethod(Field field, boolean setter) { + + //short-circuit if the class is a record and we're looking for a setter (there are no setters on Records, sparky) + if (classMapping.isRecord() && setter) { + return null; + } + + final int paramCount = setter ? 1 : 0; + final Type returnType = setter ? Void.TYPE : field.getType();//field.getGenericType() != null ? (ParameterizedType)field.getGenericType() : field.getType(); + boolean isBoolean = returnType.equals(Boolean.class) || returnType.equals(boolean.class); + + final String methodName = setter ? + (memberConfig.getSetterMethodName() == null ? + AnnotationUtils.findJsonElementSetter(field).orElse(getMappedMethodName(field.getName(), true, isBoolean)) : + memberConfig.getSetterMethodName()) : + (this.getterMethodName == null ? + AnnotationUtils.findJsonElementGetter(field).orElse(getMappedMethodName(field.getName(), false, isBoolean)) : + this.getterMethodName); + + + Stream methodStream = Arrays.stream(memberConfig.getContainerClass().getDeclaredMethods()) + .filter(method -> method.getName().equals(methodName)) + .filter(method -> method.getParameterCount() == paramCount) + .filter(method -> method.getReturnType().equals(returnType)); + + Optional methodRef = setter ? + methodStream.filter(method -> fieldAndMethodParameterTypesMatch(field, method)).findFirst() : + methodStream.findFirst(); + + return methodRef.orElse(null); + } + + private Method findMethodByKey(boolean setter) { + //short-circuit if the class is a record and we're looking for a setter (there are no setters on Records, sparky) + if (this.getClassMapping().isRecord() && setter) { + return null; + } + final int paramCount = setter ? 1 : 0; + + final String methodName = setter ? + (memberConfig.getSetterMethodName() == null ? + getMappedMethodName(getJsonKey(), true, getNodeType() == NodeType.BOOLEAN) : + this.getSetterMethodName()) : + (memberConfig.getGetterMethodName() == null ? + getMappedMethodName(getJsonKey(), false, getNodeType() == NodeType.BOOLEAN) : + this.getGetterMethodName()); + + return Arrays.stream(getContainerClass().getMethods()) + .filter(method -> method.getName().equals(methodName)) + .filter(method -> method.getParameterCount() == paramCount) + .filter(method -> isCompatibleReturnType(method, getNodeType(), setter, getElementTargetClass())) + .findFirst() + .orElseThrow(() -> new JsonMapperException("No " + (setter ? "setter" : "getter") + " found for key: " + getJsonKey())); + + } + + private boolean isCompatibleReturnType(Method method, NodeType type, boolean setter, Class elementTargetClass) { + if (setter) { + return method.getReturnType().equals(Void.TYPE); + } else { + return switch (type) { + case STRING -> method.getReturnType().isAssignableFrom(String.class); + case BOOLEAN -> + method.getReturnType().equals(Boolean.class) || method.getReturnType().equals(boolean.class); + case LONG -> method.getReturnType().equals(Long.class) || method.getReturnType().equals(long.class); + case INTEGER -> + method.getReturnType().equals(Integer.class) || method.getReturnType().equals(int.class); + case DOUBLE -> + method.getReturnType().equals(Double.class) || method.getReturnType().equals(double.class); + case ARRAY -> Collection.class.isAssignableFrom(method.getReturnType()) && + method.getGenericReturnType().equals(elementTargetClass); + case OBJECT -> elementTargetClass.isAssignableFrom(method.getReturnType()); + default -> false; + }; + } + } + + + private Optional findField(String jsonKey) { + return Arrays.stream(this.getContainerClass().getDeclaredFields()) + .filter(field -> field.getName().equals(getMappedNamed(jsonKey))) + .findFirst(); + } + + + private boolean fieldAndMethodParameterTypesMatch(Field field, Method method) { + return getMethodParameterType(method).equals(getFieldType(field)); + } + + private Type getMethodParameterType(Method m) { + final Type[] baseParam = m.getParameterTypes(); + final Type[] genericParam = m.getGenericParameterTypes(); + + if (genericParam.length == 1) { + return genericParam[0]; + } else { + return baseParam[0]; + } + } + + + /** + * Reflection magic to determine the Field's type + * + * @param field the field to evaluate + * @return the Field type + */ + private Type getFieldType(Field field) { + + return field.getGenericType(); + } + + /** + * Utility to build a setterMethod name from a string + * + * @param key the key (typically a json key, but could be a field name) + * @param setter flag indicating method name is a setter (true) or getter (false) + * @param isBoolean flag indicating the method return or parameter type is a boolean value + * @return a bean-compliant method name + */ + private String getMappedMethodName(String key, boolean setter, boolean isBoolean) { + + final StringBuilder builder = new StringBuilder(); + if (!classMapping.isRecord()) { //only append get/set/is to "classes" that are not record types + if (setter) { + builder.append("set"); + } else { + builder.append(isBoolean ? "is" : "get"); + } + } + + builder.append(this.classMapping.isRecord() ? key : getMappedNamed(key)); + return builder.toString(); + } + + private String getMappedNamed(String jsonKey) { + final StringBuilder builder = new StringBuilder(); + for (final String token : jsonKey.split("[_-]")) { + for (int i = 0; i < token.length(); i++) { + if (i == 0) { + builder.append(Character.toTitleCase(token.charAt(i))); + } else { + builder.append(token.charAt(i)); + } + } + } + + return builder.toString(); + } + + /** + * Some reflection magic to handle Generic types + * + * @param field the field + * @return the underlying field type + */ + private Class evalTargetClass(Field field) { + if (field.getGenericType() instanceof ParameterizedType) { + return (Class) ((ParameterizedType) field.getGenericType()).getActualTypeArguments()[0]; + } + return field.getType(); + } + + @Override + public String getJsonKey() { + return jsonKey; + } + + @Override + public boolean isIgnored() { + if (this.field != null) { + return memberConfig.isIgnored() ? memberConfig.isIgnored() : AnnotationUtils.findJsonElementIgnore(field); + } + + return memberConfig.isIgnored(); + } + + @Override + public boolean isAccessible() { + return false; + } + + + @Override + @SuppressWarnings("unchecked") + public Class getContainerClass() { + return (Class) containerClass; + } + + + @Override + public String getSetterMethodName() { + return setterMethodName; + } + + + @Override + public String getGetterMethodName() { + return getterMethodName; + } + + @Override + @SuppressWarnings("unchecked") + public Class getElementTargetClass() { + return (Class) elementTargetClass; + } + + @Override + public Converter getJsonConverter() { + return jsonConverter; + } + + @Override + public Converter getFieldConverter() { + return fieldConverter; + } + + @Override + public NodeType getNodeType() { + return jsonNodeType; + } + + @Override + public String getFieldName() { + return this.fieldName; + } + + + @Override + public Type getFieldType() { + return this.fieldType; + } + + private void initialize() { + if (memberConfig.getContainerClass() == null) { + throw new JsonMapperException("Invalid MemberMapping: Container Class is required"); + } + + if (memberConfig.getField() == null && memberConfig.getJsonKey() == null) { + throw new JsonMapperException("Invalid MemberMapping: Require either field or jsonKey (or both)"); + } + + if (memberConfig.getField() == null) { + Optional optionalField = findField(memberConfig.getJsonKey()); + if (optionalField.isPresent()) { + this.field = optionalField.get(); + this.setterMethod = findMethod(field, true); + this.getterMethod = findMethod(field, false); + } else { + + this.setterMethod = findMethodByKey(true); + this.getterMethod = findMethodByKey(false); + } + } else { + this.field = memberConfig.getField(); + this.setterMethod = findMethod(memberConfig.getField(), true); + this.getterMethod = findMethod(memberConfig.getField(), false); + } + + this.setterMethodName = memberConfig.getSetterMethodName() == null ? (setterMethod != null ? setterMethod.getName() : null) : memberConfig.getSetterMethodName(); + this.getterMethodName = memberConfig.getGetterMethodName() == null ? (getterMethod != null ? getterMethod.getName() : null) : memberConfig.getGetterMethodName(); + this.jsonConverter = memberConfig.getJsonConverter() != null ? memberConfig.getJsonConverter() : new PassThroughConverter(); + this.fieldConverter = memberConfig.getFieldConverter() != null ? memberConfig.getFieldConverter() : new PassThroughConverter(); + this.jsonKey = memberConfig.getJsonKey() == null ? AnnotationUtils.findJsonElementKey(field).orElse(field.getName()) : jsonKey; + this.jsonNodeType = jsonNodeType != null ? jsonNodeType : NodeType.fromClassType(field.getType()); + this.fieldName = field != null ? field.getName() : null; + this.fieldType = field != null ? field.getGenericType() : (getterMethod != null ? getterMethod.getGenericReturnType() : null); + this.elementTargetClass = elementTargetClass != null ? elementTargetClass : + AnnotationUtils.findConvertClass(field).orElse(evalTargetClass(field)); + this.ignored = memberConfig.isIgnored() ? memberConfig.isIgnored() : + (field != null && AnnotationUtils.findJsonElementIgnore(field)); + this.containerClass = memberConfig.getContainerClass(); + } + + @Override + public void applyToClass(JsonObject jsonObject, T instance) { + try { + Object value = convertValue(jsonObject.getValue(getJsonKey())); + setterMethod.invoke(instance, value); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new JsonMapperException(e); + } + } + + @SuppressWarnings("unchecked") + private T convertValue(JsonValue value) { + if (value.type().isPrimitive()) { + return getJsonConverter().convert(value.get()); + } else if (value.type().isArray()) { + if (List.class.isAssignableFrom(getRawType(getFieldType()))) { + return (T) createList(getElementTargetClass(), value.asJsonArray()); + } else { + return (T) createSet(getElementTargetClass(), value.asJsonArray()); + } + } else if (value.type().isObject()) { + return getClassMapping().getMapping().getClassMapping(getElementTargetClass()).toClass(value.asJsonObject()); + } + + return null; + } + + private List createList(Class type, JsonArray json) { + List list = new ArrayList<>(); + json.jsonValues().forEach(jsonValue -> list.add(convertValue(jsonValue))); + return list; + } + + private Set createSet(Class type, JsonArray json) { + Set set = new HashSet<>(); + json.jsonValues().forEach(jsonValue -> set.add(convertValue(jsonValue))); + return set; + } + + + @Override + public void applyToJson(T instance, JsonObject jsonObject) { + Object value = getValue(instance); + jsonObject.put(getJsonKey(), value); + } + + @Override + @SuppressWarnings("unchecked") + public T getValue(JsonObject jsonObject) { + return (T) getJsonConverter().convert(jsonObject.get(getJsonKey())); + } + + @Override + public ClassMapping getClassMapping() { + return this.classMapping; + } + + public V getValue(T instance) { + try { + return getFieldConverter().convert(getterMethod.invoke(instance)); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + @SuppressWarnings("unchecked") + private Class getRawType(Type type) { + if (type instanceof ParameterizedType) { + return (Class) ((ParameterizedType) type).getRawType(); + } + return (Class) type; + } +} diff --git a/mapper/src/main/java/io/github/xmljim/json/mapper/MethodReference.java b/mapper/src/main/java/io/github/xmljim/json/mapper/MethodReference.java index 3764fce..50a503d 100644 --- a/mapper/src/main/java/io/github/xmljim/json/mapper/MethodReference.java +++ b/mapper/src/main/java/io/github/xmljim/json/mapper/MethodReference.java @@ -1,5 +1,8 @@ package io.github.xmljim.json.mapper; +import io.github.xmljim.json.mapper.exception.JsonMapperException; + +import java.lang.reflect.Method; import java.lang.reflect.Type; class MethodReference { @@ -49,6 +52,18 @@ public Type getReturnType() { } } + public Method getMethodFromInstance(T instance) { + try { + if (getMethodType() == MethodType.SETTER) { + return instance.getClass().getMethod(getName(), contextType.getClass()); + } else { + return instance.getClass().getMethod(getName()); + } + } catch (NoSuchMethodException nsme) { + throw new JsonMapperException(nsme); + } + } + public enum MethodType { GETTER, SETTER diff --git a/mapper/src/main/java/io/github/xmljim/json/mapper/PassThroughConverter.java b/mapper/src/main/java/io/github/xmljim/json/mapper/PassThroughConverter.java new file mode 100644 index 0000000..e5a2730 --- /dev/null +++ b/mapper/src/main/java/io/github/xmljim/json/mapper/PassThroughConverter.java @@ -0,0 +1,18 @@ +package io.github.xmljim.json.mapper; + +import io.github.xmljim.json.factory.mapper.Converter; + +import java.util.Map; + +class PassThroughConverter implements Converter { + @Override + @SuppressWarnings("unchecked") + public R convert(T value) { + return (R) value; + } + + @Override + public Map getArguments() { + return null; + } +} diff --git a/mapper/src/main/java/io/github/xmljim/json/mapper/config/ClassConfigImpl.java b/mapper/src/main/java/io/github/xmljim/json/mapper/config/ClassConfigImpl.java new file mode 100644 index 0000000..f166f45 --- /dev/null +++ b/mapper/src/main/java/io/github/xmljim/json/mapper/config/ClassConfigImpl.java @@ -0,0 +1,103 @@ +package io.github.xmljim.json.mapper.config; + +import io.github.xmljim.json.factory.config.AbstractConfiguration; +import io.github.xmljim.json.factory.mapper.ClassConfig; +import io.github.xmljim.json.factory.mapper.MemberConfig; +import io.github.xmljim.json.service.JsonServiceProvider; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; + +public class ClassConfigImpl extends AbstractConfiguration implements ClassConfig { + private final List memberConfigs = new ArrayList<>(); + + private ClassConfigImpl() { + //no-args constructor; + } + + @Override + @SuppressWarnings("unchecked") + public Class getTargetClass() { + return (Class) get(ClassFields.TARGET_CLASS); + } + + @Override + @SuppressWarnings("unchecked") + public Class getSourceClass() { + return (Class) get(ClassFields.SOURCE_CLASS); + } + + @Override + public Set getIgnoredKeys() { + return get(ClassFields.IGNORE_KEYS); + } + + @Override + public List getConstructorKeys() { + return get(ClassFields.CONSTRUCTOR_KEYS); + } + + @Override + public Stream getMemberConfigurations() { + return memberConfigs.stream(); + } + + private void appendMemberConfig(MemberConfig memberConfig) { + memberConfigs.add(memberConfig); + } + + @JsonServiceProvider(service = ClassConfig.Builder.class, version = "1.0.1", isNative = true) + public static class ClassConfigBuilder implements ClassConfig.Builder { + private final ClassConfigImpl classConfig = new ClassConfigImpl(); + + @Override + public Builder targetClass(Class targetClass) { + classConfig.put(ClassFields.TARGET_CLASS, targetClass); + return this; + } + + @Override + public Builder sourceClass(Class sourceClass) { + classConfig.put(ClassFields.SOURCE_CLASS, sourceClass); + return this; + } + + @Override + public Builder ignoreKeys(Collection keys) { + classConfig.put(ClassFields.IGNORE_KEYS, keys); + return this; + } + + @Override + public Builder constructorKeys(List keys) { + classConfig.put(ClassFields.CONSTRUCTOR_KEYS, keys); + return this; + } + + @Override + public Builder appendMemberConfig(MemberConfig memberConfig) { + classConfig.appendMemberConfig(memberConfig); + return this; + } + + @Override + public ClassConfig build() { + //TODO: validate? + if (classConfig.getTargetClass() == null) { + classConfig.put(ClassFields.TARGET_CLASS, classConfig.get(ClassFields.SOURCE_CLASS)); + } + + return classConfig; + } + } + + private enum ClassFields { + TARGET_CLASS, + SOURCE_CLASS, + IGNORE_KEYS, + CONSTRUCTOR_KEYS + } +} diff --git a/mapper/src/main/java/io/github/xmljim/json/mapper/config/MappingConfigImpl.java b/mapper/src/main/java/io/github/xmljim/json/mapper/config/MappingConfigImpl.java new file mode 100644 index 0000000..0a09f78 --- /dev/null +++ b/mapper/src/main/java/io/github/xmljim/json/mapper/config/MappingConfigImpl.java @@ -0,0 +1,38 @@ +package io.github.xmljim.json.mapper.config; + +import io.github.xmljim.json.factory.config.AbstractConfiguration; +import io.github.xmljim.json.factory.mapper.ClassConfig; +import io.github.xmljim.json.factory.mapper.MappingConfig; +import io.github.xmljim.json.service.JsonServiceProvider; + +import java.util.ArrayList; +import java.util.List; + +public class MappingConfigImpl extends AbstractConfiguration implements MappingConfig { + private final List classConfigs = new ArrayList<>(); + + private MappingConfigImpl() { + + } + + @Override + public List getClassConfigurations() { + return classConfigs; + } + + @JsonServiceProvider(service = MappingConfig.Builder.class, version = "1.0.1", isNative = true) + public static class MappingConfigBuilder implements MappingConfig.Builder { + private MappingConfigImpl mappingConfig = new MappingConfigImpl(); + + @Override + public Builder appendClassConfig(ClassConfig classConfig) { + mappingConfig.classConfigs.add(classConfig); + return this; + } + + @Override + public MappingConfig build() { + return mappingConfig; + } + } +} diff --git a/mapper/src/main/java/io/github/xmljim/json/mapper/config/MappingParserConfigImpl.java b/mapper/src/main/java/io/github/xmljim/json/mapper/config/MappingParserConfigImpl.java new file mode 100644 index 0000000..9d331c5 --- /dev/null +++ b/mapper/src/main/java/io/github/xmljim/json/mapper/config/MappingParserConfigImpl.java @@ -0,0 +1,205 @@ +package io.github.xmljim.json.mapper.config; + +import io.github.xmljim.json.factory.config.AbstractConfiguration; +import io.github.xmljim.json.factory.mapper.ClassConfig; +import io.github.xmljim.json.factory.mapper.MappingParserConfig; +import io.github.xmljim.json.factory.parser.*; +import io.github.xmljim.json.factory.parser.event.Assembler; +import io.github.xmljim.json.factory.parser.event.EventHandler; +import io.github.xmljim.json.factory.parser.event.Processor; +import io.github.xmljim.json.service.JsonServiceProvider; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +public class MappingParserConfigImpl extends AbstractConfiguration implements MappingParserConfig { + + private final List classConfigurations = new ArrayList<>(); + + @Override + public List getClassConfigurations() { + return classConfigurations; + } + + @Override + public boolean enableStatistics() { + return getOrDefault(MappingParserConfigFields.ENABLE_STATISTICS, MappingParserConfigFields.ENABLE_STATISTICS.defaultValue()); + } + + @Override + public NumericValueType fixedNumberStrategy() { + return getOrDefault(MappingParserConfigFields.FIXED_NUMBER_STRATEGY, MappingParserConfigFields.FIXED_NUMBER_STRATEGY.defaultValue()); + } + + @Override + public NumericValueType floatingNumberStrategy() { + return getOrDefault(MappingParserConfigFields.FLOATING_NUMBER_STRATEGY, MappingParserConfigFields.FLOATING_NUMBER_STRATEGY.defaultValue()); + } + + @Override + public Processor getProcessor() { + return getOrDefault(MappingParserConfigFields.PROCESSOR, MappingParserConfigFields.PROCESSOR.defaultValue()); + } + + @Override + public EventHandler getEventHandler() { + return getOrDefault(MappingParserConfigFields.EVENT_HANDLER, MappingParserConfigFields.EVENT_HANDLER.defaultValue()); + } + + @Override + public Assembler getAssembler() { + return getOrDefault(MappingParserConfigFields.ASSEMBLER, MappingParserConfigFields.ASSEMBLER.defaultValue()); + } + + @Override + public Charset getCharacterSet() { + return getOrDefault(MappingParserConfigFields.CHARACTER_SET, MappingParserConfigFields.CHARACTER_SET.defaultValue()); + } + + @Override + public int getMaxEventBufferCapacity() { + return getOrDefault(MappingParserConfigFields.MAX_EVENT_BUFFER_CAPACITY, MappingParserConfigFields.MAX_EVENT_BUFFER_CAPACITY.defaultValue()); + } + + @Override + public long getRequestNextLength() { + return getOrDefault(MappingParserConfigFields.REQUEST_NEXT_LENGTH, MappingParserConfigFields.REQUEST_NEXT_LENGTH.defaultValue()); + } + + @Override + public int getBlockCount() { + return getOrDefault(MappingParserConfigFields.BLOCK_COUNT, MappingParserConfigFields.BLOCK_COUNT.defaultValue()); + } + + @Override + public boolean useStrict() { + return getOrDefault(MappingParserConfigFields.USE_STRICT, MappingParserConfigFields.USE_STRICT.defaultValue()); + } + + @Override + public T getSetting(String name) { + return get(name); + } + + @Override + public ParserSettings merge(ParserSettings settings) { + return this; + } + + @JsonServiceProvider(service = MappingParserConfig.Builder.class, version = "1.0.1", isNative = true) + public static class MappingParserConfigBuilder implements MappingParserConfig.Builder { + + private final MappingParserConfigImpl mappingParserConfig = new MappingParserConfigImpl(); + + @Override + public Builder appendClassConfig(ClassConfig classConfig) { + mappingParserConfig.classConfigurations.add(classConfig); + return this; + } + + @Override + public Builder assembler(Assembler assembler) { + mappingParserConfig.put(MappingParserConfigFields.ASSEMBLER, assembler); + return this; + } + + @Override + public Builder blockCount(int blockCount) { + mappingParserConfig.put(MappingParserConfigFields.BLOCK_COUNT, blockCount); + return this; + } + + @Override + public Builder characterSet(Charset charset) { + mappingParserConfig.put(MappingParserConfigFields.CHARACTER_SET, charset); + return this; + } + + @Override + public Builder enableStatistics(boolean enableStatistics) { + mappingParserConfig.put(MappingParserConfigFields.ENABLE_STATISTICS, enableStatistics); + return this; + } + + @Override + public Builder eventHandler(EventHandler eventHandler) { + mappingParserConfig.put(MappingParserConfigFields.EVENT_HANDLER, eventHandler); + return this; + } + + @Override + public Builder fixedNumberStrategy(NumericValueType fixedNumberStrategy) { + mappingParserConfig.put(MappingParserConfigFields.FIXED_NUMBER_STRATEGY, fixedNumberStrategy); + return this; + } + + @Override + public Builder floatingNumberStrategy(NumericValueType floatingNumberStrategy) { + mappingParserConfig.put(MappingParserConfigFields.FLOATING_NUMBER_STRATEGY, floatingNumberStrategy); + return this; + } + + @Override + public Builder maxEventBufferCapacity(int maxEventBufferCapacity) { + mappingParserConfig.put(MappingParserConfigFields.MAX_EVENT_BUFFER_CAPACITY, maxEventBufferCapacity); + return this; + } + + @Override + public Builder useStrict(boolean useStrict) { + mappingParserConfig.put(MappingParserConfigFields.USE_STRICT, useStrict); + return this; + } + + @Override + public Builder parser(Parser parser) { + mappingParserConfig.put(MappingParserConfigFields.PARSER, parser); + return this; + } + + @Override + public Builder processor(Processor processor) { + mappingParserConfig.put(MappingParserConfigFields.PROCESSOR, processor); + return this; + } + + @Override + public Builder requestNextLength(int requestNextLength) { + mappingParserConfig.put(MappingParserConfigFields.REQUEST_NEXT_LENGTH, requestNextLength); + return this; + } + + @Override + public MappingParserConfig build() { + return mappingParserConfig; + } + } + + private enum MappingParserConfigFields { + ASSEMBLER(null), + BLOCK_COUNT(4), + CHARACTER_SET(StandardCharsets.UTF_8), + EVENT_HANDLER(null), + + ENABLE_STATISTICS(true), + FIXED_NUMBER_STRATEGY(FixedNumberValueType.INTEGER), + FLOATING_NUMBER_STRATEGY(FloatingNumberValueType.DOUBLE), + MAX_EVENT_BUFFER_CAPACITY(32), + PARSER(null), + PROCESSOR(null), + REQUEST_NEXT_LENGTH(1), + USE_STRICT(true); + private final Object defaultValue; + + @SuppressWarnings("unchecked") + public T defaultValue() { + return (T) defaultValue; + } + + MappingParserConfigFields(T defaultValue) { + this.defaultValue = defaultValue; + } + } +} diff --git a/mapper/src/main/java/io/github/xmljim/json/mapper/config/MemberConfigImpl.java b/mapper/src/main/java/io/github/xmljim/json/mapper/config/MemberConfigImpl.java new file mode 100644 index 0000000..35429e6 --- /dev/null +++ b/mapper/src/main/java/io/github/xmljim/json/mapper/config/MemberConfigImpl.java @@ -0,0 +1,152 @@ +package io.github.xmljim.json.mapper.config; + +import io.github.xmljim.json.factory.config.AbstractConfiguration; +import io.github.xmljim.json.factory.mapper.Converter; +import io.github.xmljim.json.factory.mapper.MemberConfig; +import io.github.xmljim.json.model.NodeType; +import io.github.xmljim.json.service.JsonServiceProvider; + +import java.lang.reflect.Field; + +public class MemberConfigImpl extends AbstractConfiguration implements MemberConfig { + private MemberConfigImpl() { + //no-arg constructor; + } + + @Override + public Field getField() { + return get(MemberFields.FIELD); + } + + @Override + public String getJsonKey() { + return get(MemberFields.JSON_KEY); + } + + @Override + public NodeType getNodeType() { + return get(MemberFields.NODE_TYPE); + } + + @Override + public Class getContainerClass() { + return get(MemberFields.CONTAINER_CLASS); + } + + @Override + public Class getElementTargetClass() { + return get(MemberFields.ELEMENT_TARGET_CLASS); + } + + @Override + public Converter getFieldConverter() { + return get(MemberFields.FIELD_CONVERTER); + } + + @Override + public Converter getJsonConverter() { + return get(MemberFields.JSON_CONVERTER); + } + + @Override + public String getGetterMethodName() { + return get(MemberFields.GETTER_METHOD_NAME); + } + + @Override + public String getSetterMethodName() { + return get(MemberFields.SETTER_METHOD_NAME); + } + + @Override + public boolean isIgnored() { + if (containsKey(MemberFields.IGNORED)) { + return get(MemberFields.IGNORED); + } + return false; + } + + @JsonServiceProvider(service = MemberConfig.Builder.class, version = "1.0.1") + public static class MemberConfigBuilder implements MemberConfig.Builder { + MemberConfigImpl memberConfig = new MemberConfigImpl(); + + @Override + public Builder field(Field field) { + memberConfig.put(MemberFields.FIELD, field); + return this; + } + + @Override + public Builder jsonKey(String jsonKey) { + memberConfig.put(MemberFields.JSON_KEY, jsonKey); + return this; + } + + @Override + public Builder nodeType(NodeType nodeType) { + memberConfig.put(MemberFields.NODE_TYPE, nodeType); + return this; + } + + @Override + public Builder containerClass(Class containerClass) { + memberConfig.put(MemberFields.CONTAINER_CLASS, containerClass); + return this; + } + + @Override + public Builder elementTargetClass(Class elementTargetClass) { + memberConfig.put(MemberFields.ELEMENT_TARGET_CLASS, elementTargetClass); + return this; + } + + @Override + public Builder fieldConverter(Converter fieldConverter) { + memberConfig.put(MemberFields.FIELD_CONVERTER, fieldConverter); + return this; + } + + @Override + public Builder jsonConverter(Converter jsonConverter) { + memberConfig.put(MemberFields.JSON_CONVERTER, jsonConverter); + return this; + } + + @Override + public Builder getterMethodName(String getterMethodName) { + memberConfig.put(MemberFields.GETTER_METHOD_NAME, getterMethodName); + return this; + } + + @Override + public Builder setterMethodName(String setterMethodName) { + memberConfig.put(MemberFields.SETTER_METHOD_NAME, setterMethodName); + return this; + } + + @Override + public Builder ignored(boolean ignored) { + memberConfig.put(MemberFields.IGNORED, ignored); + return this; + } + + @Override + public MemberConfig build() { + //TODO: Add some validations? + return memberConfig; + } + } + + private enum MemberFields { + FIELD, + JSON_KEY, + NODE_TYPE, + CONTAINER_CLASS, + ELEMENT_TARGET_CLASS, + FIELD_CONVERTER, + JSON_CONVERTER, + SETTER_METHOD_NAME, + GETTER_METHOD_NAME, + IGNORED + } +} diff --git a/mapper/src/main/java/io/github/xmljim/json/mapper/parser/MappingParserImpl.java b/mapper/src/main/java/io/github/xmljim/json/mapper/parser/MappingParserImpl.java new file mode 100644 index 0000000..0e0de73 --- /dev/null +++ b/mapper/src/main/java/io/github/xmljim/json/mapper/parser/MappingParserImpl.java @@ -0,0 +1,23 @@ +package io.github.xmljim.json.mapper.parser; + +import io.github.xmljim.json.factory.mapper.Mapper; +import io.github.xmljim.json.factory.mapper.parser.MappingParser; +import io.github.xmljim.json.factory.parser.InputData; +import io.github.xmljim.json.factory.parser.Parser; +import io.github.xmljim.json.model.JsonObject; + +public class MappingParserImpl implements MappingParser { + private final Mapper mapper; + private final Parser parser; + + public MappingParserImpl(Mapper mapper, Parser parser) { + this.mapper = mapper; + this.parser = parser; + } + + @Override + public T parse(InputData inputData, Class targetClass) { + JsonObject jsonObject = parser.parse(inputData); + return mapper.toClass(jsonObject, targetClass); + } +} diff --git a/mapper/src/main/java/module-info.java b/mapper/src/main/java/module-info.java index 0190a20..3a9b9db 100644 --- a/mapper/src/main/java/module-info.java +++ b/mapper/src/main/java/module-info.java @@ -1,11 +1,32 @@ -import io.github.xmljim.json.factory.mapper.MapperFactory; +import io.github.xmljim.json.factory.mapper.*; import io.github.xmljim.json.factory.model.ElementFactory; import io.github.xmljim.json.mapper.MapperFactoryImpl; +import io.github.xmljim.json.mapper.config.ClassConfigImpl; +import io.github.xmljim.json.mapper.config.MappingConfigImpl; +import io.github.xmljim.json.mapper.config.MappingParserConfigImpl; +import io.github.xmljim.json.mapper.config.MemberConfigImpl; + module io.github.xmljim.json.mapper { + //dependencies requires transitive io.github.xmljim.json.factory; + + //expose packages for reflection and service creation opens io.github.xmljim.json.mapper; + opens io.github.xmljim.json.mapper.config; + opens io.github.xmljim.json.mapper.parser; + + //targeted exports (only make them available to specific modules) exports io.github.xmljim.json.mapper to io.xmljim.json.mapper.test; + exports io.github.xmljim.json.mapper.config to io.xmljim.json.mapper.test; + + //Service providers provides MapperFactory with MapperFactoryImpl; + provides MappingConfig.Builder with MappingConfigImpl.MappingConfigBuilder; + provides ClassConfig.Builder with ClassConfigImpl.ClassConfigBuilder; + provides MemberConfig.Builder with MemberConfigImpl.MemberConfigBuilder; + provides MappingParserConfig.Builder with MappingParserConfigImpl.MappingParserConfigBuilder; + + //Services consumed uses ElementFactory; } \ No newline at end of file diff --git a/mapper/src/test/java/io/xmljim/json/mapper/test/ClassMappingImplTest.java b/mapper/src/test/java/io/xmljim/json/mapper/test/ClassMappingImplTest.java new file mode 100644 index 0000000..b3729e6 --- /dev/null +++ b/mapper/src/test/java/io/xmljim/json/mapper/test/ClassMappingImplTest.java @@ -0,0 +1,62 @@ +package io.xmljim.json.mapper.test; + +import io.github.xmljim.json.factory.mapper.*; +import io.github.xmljim.json.mapper.MapperFactoryImpl; +import io.xmljim.json.mapper.test.testclasses.Person; +import io.xmljim.json.mapper.test.testclasses.TestRecord; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; + +import static org.junit.jupiter.api.Assertions.*; + +class ClassMappingImplTest { + + @Test + void testBasicClassMapping() { + MapperFactory mapperFactory = new MapperFactoryImpl(); + MappingConfig mappingConfig = MappingConfig.with() + .build(); + Mapping mapping = mapperFactory.newMapping(mappingConfig); + + ClassConfig classConfig = ClassConfig.with().sourceClass(Person.class).build(); + ClassMapping classMapping = mapperFactory.newClassMapping(mapping, classConfig); + assertNotNull(classMapping); + assertEquals(2L, classMapping.getMemberMappings().count()); + } + + @Test + void testRenamedJsonKeysBasicMapping() throws NoSuchFieldException { + + + Field lastNameField = Person.class.getDeclaredField("lastName"); + Field firstNameField = Person.class.getDeclaredField("firstName"); + + } + + @Test + void testWithRecord() { + MapperFactory factory = new MapperFactoryImpl(); + Mapper mapper = factory.newMapper(MappingConfig.with() + .appendClassConfig(ClassConfig.with() + .sourceClass(TestRecord.class) + .build()) + .build() + ); + + ClassMapping classMapping = mapper.getMapping().getClassMapping(TestRecord.class); + assertNotNull(classMapping); + assertTrue(classMapping.isRecord()); + assertEquals(4, classMapping.getMemberMappings().count()); + + classMapping.getConstructorKeys().forEach(key -> { + MemberMapping memberMapping = classMapping.getMemberMapping(key); + assertNotNull(memberMapping); + }); + + Constructor constructor = classMapping.getConstructor(); + assertNotNull(constructor); + + } +} \ No newline at end of file diff --git a/mapper/src/test/java/io/xmljim/json/mapper/test/MapperTest.java b/mapper/src/test/java/io/xmljim/json/mapper/test/MapperTest.java index fdebc74..2bbfca4 100644 --- a/mapper/src/test/java/io/xmljim/json/mapper/test/MapperTest.java +++ b/mapper/src/test/java/io/xmljim/json/mapper/test/MapperTest.java @@ -2,6 +2,7 @@ import io.github.xmljim.json.factory.mapper.Mapper; import io.github.xmljim.json.factory.mapper.MapperFactory; +import io.github.xmljim.json.factory.mapper.MappingConfig; import io.github.xmljim.json.factory.model.ElementFactory; import io.github.xmljim.json.model.JsonArray; import io.github.xmljim.json.model.JsonObject; @@ -122,7 +123,11 @@ void toClass() { basic.put("subclass", subClass); MapperFactory factory = ServiceManager.getProvider(MapperFactory.class); - Mapper mapper = factory.newBuilder().setTargetClass(BasicTestClass.class).build(); + + Mapper mapper = factory.newMapper( + MappingConfig.with().withClass(BasicTestClass.class).build()); + + //Mapper mapper = factory.newBuilder().setTargetClass(BasicTestClass.class).build(); BasicTestClass testClass = mapper.toClass(basic); assertNotNull(testClass); diff --git a/mapper/src/test/java/io/xmljim/json/mapper/test/MemberMappingImplTest.java b/mapper/src/test/java/io/xmljim/json/mapper/test/MemberMappingImplTest.java new file mode 100644 index 0000000..1ff5fe6 --- /dev/null +++ b/mapper/src/test/java/io/xmljim/json/mapper/test/MemberMappingImplTest.java @@ -0,0 +1,65 @@ +package io.xmljim.json.mapper.test; + +import io.github.xmljim.json.factory.mapper.*; +import io.github.xmljim.json.mapper.MapperFactoryImpl; +import io.xmljim.json.mapper.test.testclasses.Person; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Field; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class MemberMappingImplTest { + + @Test + void getJsonKey() throws NoSuchFieldException { + MapperFactory mapperFactory = new MapperFactoryImpl(); + MappingConfig mappingConfig = MappingConfig.with() + .appendClassConfig(ClassConfig.with(). + sourceClass(Person.class) + .build()) + .build(); + + Mapping mapping = mapperFactory.newMapping(mappingConfig); + ClassMapping classMapping = mapping.getClassMapping(Person.class); + + Field firstNameField = Person.class.getDeclaredField("firstName"); + MemberMapping memberMapping = classMapping.getMemberMapping(firstNameField); + + assertNotNull(memberMapping); + assertEquals("firstName", memberMapping.getJsonKey()); + } + + @Test + void isIgnored() { + } + + @Test + void getContainerClass() { + } + + @Test + void getSetterMethod() { + } + + @Test + void getGetterMethod() { + } + + @Test + void getElementTargetClass() { + } + + @Test + void getJsonConverter() { + } + + @Test + void getFieldConverter() { + } + + @Test + void getNodeType() { + } +} \ No newline at end of file diff --git a/mapper/src/test/java/io/xmljim/json/mapper/test/testclasses/BasicTestClass.java b/mapper/src/test/java/io/xmljim/json/mapper/test/testclasses/BasicTestClass.java index f7cbe1c..a6ce8ab 100644 --- a/mapper/src/test/java/io/xmljim/json/mapper/test/testclasses/BasicTestClass.java +++ b/mapper/src/test/java/io/xmljim/json/mapper/test/testclasses/BasicTestClass.java @@ -11,6 +11,7 @@ public class BasicTestClass { private Long count; private List valueSet; //@JSONTargetClass(TestSubclassImpl.class) + private TestSubclassImpl subclass; /** diff --git a/mapper/src/test/java/io/xmljim/json/mapper/test/testclasses/TestRecord.java b/mapper/src/test/java/io/xmljim/json/mapper/test/testclasses/TestRecord.java new file mode 100644 index 0000000..2c35ff9 --- /dev/null +++ b/mapper/src/test/java/io/xmljim/json/mapper/test/testclasses/TestRecord.java @@ -0,0 +1,7 @@ +package io.xmljim.json.mapper.test.testclasses; + +import java.util.Collection; + +public record TestRecord(String name, int number, boolean isSet, Collection values) { + +} diff --git a/mapper/src/test/java/module-info.java b/mapper/src/test/java/module-info.java index 079c03f..07680c0 100644 --- a/mapper/src/test/java/module-info.java +++ b/mapper/src/test/java/module-info.java @@ -5,4 +5,5 @@ requires org.junit.jupiter.engine; opens io.xmljim.json.mapper.test; opens io.xmljim.json.mapper.test.testclasses; + } \ No newline at end of file diff --git a/mapper/src/test/resources/marshallingTest2.json b/mapper/src/test/resources/marshallingTest2.json new file mode 100644 index 0000000..837e3d7 --- /dev/null +++ b/mapper/src/test/resources/marshallingTest2.json @@ -0,0 +1,14 @@ +{ + "stringValue": "This is a String value", + "booleanValue": true, + "nullValue": null, + "numberValue": 12345678909, + "doubleValue": 3.1415927, + "primitiveArray" : ["a", "b", "c", "d", "e", "f"], + "simpleObject" : {"firstName": "Jim", "lastName": "Earley"}, + "complexArray" : [ + {"team" : "Colorado Rockies", "league": "NL"}, + {"team" : "Boston Red Sox", "league": "AL"}, + {"team" : "New York Yankees", "league": "NL"} + ] +} \ No newline at end of file diff --git a/merger/src/main/java/io/github/xmljim/json/merger/MergeConfigImpl.java b/merger/src/main/java/io/github/xmljim/json/merger/MergeConfigImpl.java index 2320928..10ca9e4 100644 --- a/merger/src/main/java/io/github/xmljim/json/merger/MergeConfigImpl.java +++ b/merger/src/main/java/io/github/xmljim/json/merger/MergeConfigImpl.java @@ -4,6 +4,8 @@ import io.github.xmljim.json.factory.merge.strategy.ArrayConflictStrategy; import io.github.xmljim.json.factory.merge.strategy.MergeResultStrategy; import io.github.xmljim.json.factory.merge.strategy.ObjectConflictStrategy; +import io.github.xmljim.json.merger.conflict.ArrayConflictStrategies; +import io.github.xmljim.json.merger.conflict.ObjectConflictStrategies; class MergeConfigImpl implements MergeConfig { private ArrayConflictStrategy arrayConflictStrategy = ArrayConflictStrategies.APPEND; diff --git a/merger/src/main/java/io/github/xmljim/json/merger/ArrayConflictStrategies.java b/merger/src/main/java/io/github/xmljim/json/merger/conflict/ArrayConflictStrategies.java similarity index 86% rename from merger/src/main/java/io/github/xmljim/json/merger/ArrayConflictStrategies.java rename to merger/src/main/java/io/github/xmljim/json/merger/conflict/ArrayConflictStrategies.java index f0111d7..6b2e3c0 100644 --- a/merger/src/main/java/io/github/xmljim/json/merger/ArrayConflictStrategies.java +++ b/merger/src/main/java/io/github/xmljim/json/merger/conflict/ArrayConflictStrategies.java @@ -1,4 +1,4 @@ -package io.github.xmljim.json.merger; +package io.github.xmljim.json.merger.conflict; import io.github.xmljim.json.factory.merge.MergeProcessor; import io.github.xmljim.json.factory.merge.strategy.ArrayConflictStrategy; @@ -25,8 +25,8 @@ public void apply(JsonArray context, Integer propertyValue, JsonValue primary context.add(mergedArray); } } else { - context.add(primaryValue); context.add(secondaryValue); + context.add(primaryValue); } } } @@ -62,24 +62,16 @@ public void apply(JsonArray context, Integer propertyValue, JsonValue primary context.add(primaryValue); } else { if (primaryValue.type().equals(secondaryValue.type())) { - if (primaryValue.type().isPrimitive()) { - if (propertyValue == 0 || propertyValue == context.size()) { - context.add(primaryValue); - context.add(secondaryValue); - } else { - context.insert(propertyValue, primaryValue); - context.add(secondaryValue); - } - } else if (primaryValue.type().isObject()) { + if (primaryValue.type().isObject()) { context.add(processor.merge((JsonObject) primaryValue.get(), (JsonObject) secondaryValue.get())); - } else { + } else if (primaryValue.type().isArray()) { context.add(processor.merge((JsonArray) primaryValue.get(), (JsonArray) secondaryValue.get())); + } else { + context.add(primaryValue); + context.add(secondaryValue); } - } else if (propertyValue == 0 || propertyValue == context.size()) { - context.add(primaryValue); - context.add(secondaryValue); } else { - context.insert(propertyValue, primaryValue); + context.add(primaryValue); context.add(secondaryValue); } } diff --git a/merger/src/main/java/io/github/xmljim/json/merger/ObjectConflictStrategies.java b/merger/src/main/java/io/github/xmljim/json/merger/conflict/ObjectConflictStrategies.java similarity index 95% rename from merger/src/main/java/io/github/xmljim/json/merger/ObjectConflictStrategies.java rename to merger/src/main/java/io/github/xmljim/json/merger/conflict/ObjectConflictStrategies.java index 9747802..36c0b3c 100644 --- a/merger/src/main/java/io/github/xmljim/json/merger/ObjectConflictStrategies.java +++ b/merger/src/main/java/io/github/xmljim/json/merger/conflict/ObjectConflictStrategies.java @@ -1,4 +1,4 @@ -package io.github.xmljim.json.merger; +package io.github.xmljim.json.merger.conflict; import io.github.xmljim.json.factory.merge.MergeProcessor; import io.github.xmljim.json.factory.merge.strategy.ObjectConflictStrategy; @@ -32,15 +32,15 @@ public void apply(JsonObject context, String propertyValue, JsonValue primary @Override public void apply(JsonObject context, String propertyValue, JsonValue primaryValue, JsonValue secondaryValue, MergeProcessor processor) { if (primaryValue.isEquivalent(secondaryValue)) { - if (primaryValue.type().isPrimitive()) { - context.put(propertyValue, secondaryValue); - } else if (primaryValue.type().isObject()) { + context.put(propertyValue, secondaryValue); + } else { + if (primaryValue.type().isObject()) { context.put(propertyValue, processor.merge((JsonObject) primaryValue.get(), (JsonObject) secondaryValue.get())); - } else { + } else if (primaryValue.type().isArray()) { context.put(propertyValue, processor.merge((JsonArray) primaryValue.get(), (JsonArray) secondaryValue.get())); + } else { + context.put(propertyValue, secondaryValue); } - } else { - context.put(propertyValue, secondaryValue); } } }, diff --git a/merger/src/main/java/module-info.java b/merger/src/main/java/module-info.java index fce78bd..30e4818 100644 --- a/merger/src/main/java/module-info.java +++ b/merger/src/main/java/module-info.java @@ -5,6 +5,8 @@ requires transitive io.github.xmljim.json.factory; opens io.github.xmljim.json.merger to io.github.xmljim.json.factory; exports io.github.xmljim.json.merger to io.github.xmljim.json.merger.test; + exports io.github.xmljim.json.merger.conflict; + opens io.github.xmljim.json.merger.conflict to io.github.xmljim.json.factory; provides MergeFactory with MergeFactoryImpl; } \ No newline at end of file diff --git a/merger/src/test/java/io/github/xmljim/json/merger/test/MergerTests.java b/merger/src/test/java/io/github/xmljim/json/merger/test/MergerTests.java index 45f29e5..723dbf0 100644 --- a/merger/src/test/java/io/github/xmljim/json/merger/test/MergerTests.java +++ b/merger/src/test/java/io/github/xmljim/json/merger/test/MergerTests.java @@ -4,6 +4,8 @@ import io.github.xmljim.json.factory.merge.MergeProcessor; import io.github.xmljim.json.factory.parser.InputData; import io.github.xmljim.json.factory.parser.ParserFactory; +import io.github.xmljim.json.merger.conflict.ArrayConflictStrategies; +import io.github.xmljim.json.merger.conflict.ObjectConflictStrategies; import io.github.xmljim.json.model.JsonArray; import io.github.xmljim.json.model.JsonObject; import io.github.xmljim.json.service.ServiceManager; @@ -13,7 +15,9 @@ import java.io.IOException; import java.io.InputStream; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.fail; @DisplayName("Merger Tests") class MergerTests { @@ -59,21 +63,201 @@ void whenMergingArraysWithNoConflicts_andDefaultConfig_ShouldReturnAllElements() } } + @Test + @DisplayName("Test Array Conflict Strategy: Append") + void testArrayAppendConflictStrategy() { + String primaryArray = """ + ["a", "b", {"foo": "bar"}, 1, 1, 3, ["first", "last"]] + """; + + String secondaryArray = """ + ["z", "b", {"foo": "bar"}, 1, 5, 6, ["first", "last"]] + """; + + String expectedArray = """ + ["a", "z", "b", {"foo":"bar"}, 1, 1, 5, 3, 6, ["first", "last"]] + """; + + ParserFactory factory = ServiceManager.getProvider(ParserFactory.class); + + JsonArray primary = factory.newParser().parse(InputData.of(primaryArray)); + JsonArray secondary = factory.newParser().parse(InputData.of(secondaryArray)); + JsonArray expected = factory.newParser().parse(InputData.of(expectedArray)); + MergeFactory mergeFactory = ServiceManager.getProvider(MergeFactory.class); + + + JsonArray actual = mergeFactory.newMergeBuilder() + .setArrayConflictStrategy(ArrayConflictStrategies.APPEND) + .build() + .merge(primary, secondary); + assertEquals(expected, actual); + } + + @Test + @DisplayName("Test Array Conflict Strategy: Append, Primary bigger than Secondary") + void testArrayAppendConflictStrategyPrimaryBiggerThanSecondary() { + String primaryArray = """ + ["a", "b", {"foo": "bar"}, 1, 1, 3, ["first", "last"], 99] + """; + + String secondaryArray = """ + ["z", "b", {"foo": "bar"}, 1, 5, 6, ["first", "last"]] + """; + + String expectedArray = """ + ["a", "z", "b", {"foo":"bar"}, 1, 1, 5, 3, 6, ["first", "last"], 99] + """; + + ParserFactory factory = ServiceManager.getProvider(ParserFactory.class); + + JsonArray primary = factory.newParser().parse(InputData.of(primaryArray)); + JsonArray secondary = factory.newParser().parse(InputData.of(secondaryArray)); + JsonArray expected = factory.newParser().parse(InputData.of(expectedArray)); + MergeFactory mergeFactory = ServiceManager.getProvider(MergeFactory.class); + + + JsonArray actual = mergeFactory.newMergeBuilder() + .setArrayConflictStrategy(ArrayConflictStrategies.APPEND) + .build() + .merge(primary, secondary); + assertEquals(expected, actual); + } + + @Test + @DisplayName("Test Array Conflict Strategy: Append, Primary bigger than Secondary") + void testArrayAppendConflictStrategyPrimarySmallerThanSecondary() { + String primaryArray = """ + ["a", "b", {"foo": "bar"}, 1, 1, 3, ["first", "last"]] + """; + + String secondaryArray = """ + ["z", "b", {"foo": "bar"}, 1, 5, 6, ["first", "last"], 99] + """; + + String expectedArray = """ + ["a", "z", "b", {"foo":"bar"}, 1, 1, 5, 3, 6, ["first", "last"], 99] + """; + + ParserFactory factory = ServiceManager.getProvider(ParserFactory.class); + + JsonArray primary = factory.newParser().parse(InputData.of(primaryArray)); + JsonArray secondary = factory.newParser().parse(InputData.of(secondaryArray)); + JsonArray expected = factory.newParser().parse(InputData.of(expectedArray)); + MergeFactory mergeFactory = ServiceManager.getProvider(MergeFactory.class); + + + JsonArray actual = mergeFactory.newMergeBuilder() + .setArrayConflictStrategy(ArrayConflictStrategies.APPEND) + .build() + .merge(primary, secondary); + assertEquals(expected, actual); + } + + @Test + @DisplayName("Test Array Conflict Strategy: Deduplicate") + void testDeduplicateArrayStrategy() { + String primaryArray = """ + ["a", "b", {"foo": "bar"}, 1, 1, [1,3,5], true] + """; + + String secondaryArray = """ + ["z", "b", {"foo": "bar"}, 1, 5, [1,3,6], "a"] + """; + + String expectedArray = """ + ["a", "z", "b", {"foo":"bar"}, 1, 5, [1,3,5,6], true] + """; + ParserFactory factory = ServiceManager.getProvider(ParserFactory.class); + + JsonArray primary = factory.newParser().parse(InputData.of(primaryArray)); + JsonArray secondary = factory.newParser().parse(InputData.of(secondaryArray)); + JsonArray expected = factory.newParser().parse(InputData.of(expectedArray)); + + MergeFactory mergeFactory = ServiceManager.getProvider(MergeFactory.class); + + + JsonArray actual = mergeFactory.newMergeBuilder() + .setArrayConflictStrategy(ArrayConflictStrategies.DEDUPLICATE) + .build() + .merge(primary, secondary); + assertEquals(expected, actual); + } + + @Test + @DisplayName("Test Array Conflict Strategy: InsertBefore") + void testArrayConflictStrategyInsertBefore() { + String primaryArray = """ + ["first", "next-to-last", true, {"simple": "object"}, {"a":true}, [1,2],1,2,3] + """; + + String secondaryArray = """ + ["second", "last", false, ["simple", "array"], {"b": false}, [3,4], 1,2,4] + """; + + String expectedArray = """ + ["second", "first", "last","next-to-last", false, true, ["simple", "array"],{"simple": "object"}, {"a": true, "b": false}, [3,1,4,2] 1,2,4,3] + """; + ParserFactory factory = ServiceManager.getProvider(ParserFactory.class); + + JsonArray primary = factory.newParser().parse(InputData.of(primaryArray)); + JsonArray secondary = factory.newParser().parse(InputData.of(secondaryArray)); + JsonArray expected = factory.newParser().parse(InputData.of(expectedArray)); + + MergeFactory mergeFactory = ServiceManager.getProvider(MergeFactory.class); + + + JsonArray actual = mergeFactory.newMergeBuilder() + .setArrayConflictStrategy(ArrayConflictStrategies.INSERT_BEFORE) + .build() + .merge(primary, secondary); + assertEquals(expected, actual); + } + + @Test + @DisplayName("Test Array Conflict Strategy: InsertAfter") + void testArrayConflictStrategyInsertAfter() { + String primaryArray = """ + ["first", "next-to-last", true, {"simple": "object"}, {"a":true}, [1,2],1,2,3] + """; + + String secondaryArray = """ + ["second", "last", false, ["simple", "array"],{"b": false}, [3,4], 1,2,4] + """; + + String expectedArray = """ + ["first", "second", "next-to-last", "last", true, false, {"simple": "object"}, ["simple", "array"],{"a": true, "b": false}, [1,3,2,4], 1,2,3,4] + """; + ParserFactory factory = ServiceManager.getProvider(ParserFactory.class); + + JsonArray primary = factory.newParser().parse(InputData.of(primaryArray)); + JsonArray secondary = factory.newParser().parse(InputData.of(secondaryArray)); + JsonArray expected = factory.newParser().parse(InputData.of(expectedArray)); + + MergeFactory mergeFactory = ServiceManager.getProvider(MergeFactory.class); + + + JsonArray actual = mergeFactory.newMergeBuilder() + .setArrayConflictStrategy(ArrayConflictStrategies.INSERT_AFTER) + .build() + .merge(primary, secondary); + assertEquals(expected, actual); + } + @Test @DisplayName("Simple Object Merge - Defaults") void whenMergingObjectsWithNoConflicts_AndDefaultConfig_ShouldSeeAllElements() { String primaryJson = """ - {"foo": "bar", "bar": "baz"} - """; + {"foo": "bar", "bar": "baz"} + """; String secondaryJson = """ - {"type": "dog", "color": "black"} - """; + {"type": "dog", "color": "black"} + """; String expectedResult = """ - {"foo": "bar", "bar": "baz", "type": "dog", "color": "black"} - """; + {"foo": "bar", "bar": "baz", "type": "dog", "color": "black"} + """; ParserFactory factory = ServiceManager.getProvider(ParserFactory.class); @@ -88,4 +272,102 @@ void whenMergingObjectsWithNoConflicts_AndDefaultConfig_ShouldSeeAllElements() { assertEquals(expected, actual); } + @Test + @DisplayName("Test Object Conflict Strategy: Accept Primary") + void testObjectConflictStrategyAcceptPrimary() { + String primaryObject = """ + {"a": true, "b": [1,2], "c": [3,4], "d": {"foo": "bar"}, "e": false} + """; + + String secondaryObject = """ + {"a": false, "b": [1,2], "c": [5,6], "d": {"bar": "baz"}, "e": null} + """; + + String expectedObject = """ + {"a": true, "b": [1,2], "c": [3,5,4,6], "d": {"foo": "bar", "bar": "baz"}, "e": false} + """; + + ParserFactory factory = ServiceManager.getProvider(ParserFactory.class); + + JsonObject primary = factory.newParser().parse(InputData.of(primaryObject)); + JsonObject secondary = factory.newParser().parse(InputData.of(secondaryObject)); + JsonObject expected = factory.newParser().parse(InputData.of(expectedObject)); + + JsonObject actual = ServiceManager.getProvider(MergeFactory.class) + .newMergeBuilder() + .setObjectConflictStrategy(ObjectConflictStrategies.ACCEPT_PRIMARY) + .build() + .merge(primary, secondary); + + assertEquals(expected, actual); + } + + @Test + @DisplayName("Test Object Conflict Strategy: Accept Secondary") + void testObjectConflictStrategyAcceptSecondary() { + String primaryObject = """ + {"a": true, "b": [1,2], "c": [3,4], "d": {"foo": "bar"}, "e": false} + """; + + String secondaryObject = """ + {"a": false, "b": [1,2], "c": [5,6], "d": {"bar": "baz"}, "e": null} + """; + + String expectedObject = """ + {"a": false, "b": [1,2], "c": [3,5,4,6], "d": {"foo": "bar", "bar": "baz"}, "e": null} + """; + + ParserFactory factory = ServiceManager.getProvider(ParserFactory.class); + + JsonObject primary = factory.newParser().parse(InputData.of(primaryObject)); + JsonObject secondary = factory.newParser().parse(InputData.of(secondaryObject)); + JsonObject expected = factory.newParser().parse(InputData.of(expectedObject)); + + JsonObject actual = ServiceManager.getProvider(MergeFactory.class) + .newMergeBuilder() + .setObjectConflictStrategy(ObjectConflictStrategies.ACCEPT_SECONDARY) + .build() + .merge(primary, secondary); + + assertEquals(expected, actual); + } + + @Test + @DisplayName("Test Object Conflict Strategy: Append") + void testObjectConflictStrategyAppend() { + String primaryObject = """ + {"a": true, "b": [1,2], "c": [3,4], "d": {"foo": "bar"}, "e": false} + """; + + String secondaryObject = """ + {"a": false, "b": [1,2], "c": [5,6], "d": {"bar": "baz"}, "e": null} + """; + + String expectedObject = """ + { "a": true, + "a_appended": false, + "b": [1,2], + "c": [3,5,4,6], + "d": {"foo": "bar", "bar": "baz"}, + "e": false, + "e_appended": null + } + """; + + ParserFactory factory = ServiceManager.getProvider(ParserFactory.class); + + JsonObject primary = factory.newParser().parse(InputData.of(primaryObject)); + JsonObject secondary = factory.newParser().parse(InputData.of(secondaryObject)); + JsonObject expected = factory.newParser().parse(InputData.of(expectedObject)); + + JsonObject actual = ServiceManager.getProvider(MergeFactory.class) + .newMergeBuilder() + .setObjectConflictStrategy(ObjectConflictStrategies.APPEND) + .setMergeAppendKey("_appended") + .build() + .merge(primary, secondary); + + assertEquals(expected, actual); + } + } diff --git a/merger/src/test/resources/simple-default-array-merge-result.json b/merger/src/test/resources/simple-default-array-merge-result.json index ca90220..fd8255d 100644 --- a/merger/src/test/resources/simple-default-array-merge-result.json +++ b/merger/src/test/resources/simple-default-array-merge-result.json @@ -1,6 +1,6 @@ [ "Apple", - 123, "Banana", + 123, false ] \ No newline at end of file diff --git a/model/src/main/java/io/github/xmljim/json/model/JsonArray.java b/model/src/main/java/io/github/xmljim/json/model/JsonArray.java index 2aed7d3..f163d5e 100644 --- a/model/src/main/java/io/github/xmljim/json/model/JsonArray.java +++ b/model/src/main/java/io/github/xmljim/json/model/JsonArray.java @@ -4,55 +4,186 @@ import java.util.Optional; import java.util.stream.Stream; +/** + * A container for a list of values + */ public non-sealed interface JsonArray extends JsonNode { default NodeType type() { return NodeType.ARRAY; } + /** + * Add a value to the array. Internally it should convert the value to a + * {@link JsonValue} type + * + * @param value the value + * @param the value type + * @return the value + */ boolean add(V value); + /** + * A a collection of values + * + * @param collection the value collection + * @return true if the collection of values was added + */ boolean addAll(Collection collection); + /** + * A a collection of values from a stream + * + * @param stream the value stream + * @return true if the values were added + */ boolean addAll(Stream stream); + /** + * Insert a value a specified index + * + * @param index the index position. Must be < the length -1 + * @param value the value to insert + * @param the value type + * @return true if the value was inserted + */ boolean insert(int index, V value); + /** + * Insert a collection of values, starting at a given index position + * + * @param index the index position + * @param collection the collect to add + * @return true if the collection of values were added + */ boolean insertAll(int index, Collection collection); + /** + * Insert a collection stream of values, starting at a given index position + * + * @param index the index position + * @param stream the collection stream + * @return true if the values were inserted + */ boolean insertAll(int index, Stream stream); + /** + * Set/Update a value at a given index position + * + * @param index the index position + * @param value the value + * @param the value type + * @return the value, converted to a {@link JsonValue} instance + */ JsonValue set(int index, V value); + /** + * Return an optional JsonValue from a given index + * + * @param index the index position + * @param the value type + * @return returns an Optional of the value, which may be empty if + * no value exists at the given index + */ Optional> value(int index); + /** + * Return an optional (raw) value from a given index + * + * @param index the index position + * @param the value type + * @return an Optional of the value, which may be empty if the + * value was null, or no value exists at a given index + */ @SuppressWarnings("unchecked") default Optional getOptional(int index) { return (Optional) value(index).map(JsonValue::get); } + /** + * Get the JsonValue at a given index + * + * @param index the index position + * @param the value type + * @return the JsonValue or null if no value exists at a given index + */ @SuppressWarnings("unchecked") default JsonValue getValue(int index) { return (JsonValue) value(index).orElse(null); } + /** + * Get the (raw) value at a given index + * + * @param index the index position + * @param the value type + * @return the value or null if the value is either null or + * does not exist at a given index + */ @SuppressWarnings("unchecked") default V get(int index) { return (V) getOptional(index).orElse(null); } + /** + * Evaluates the list of data to see if it contains the raw value + * + * @param value the value + * @param the value type + * @return true if the list contains the value; false otherwise + */ boolean contains(V value); + /** + * Evaluates the list of JsonValues to see if it contains a JsonValue + * with this value + * + * @param value the JsonValue + * @return true if the contains the JsonValue; false otherwise + */ boolean containsValue(JsonValue value); + /** + * Remove a value at a given index + * + * @param index the index position + * @param the value type + * @return the removed JsonValue + */ JsonValue remove(int index); + /** + * Splice the array to return values from a given start position + * + * @param start the start position + * @return all values beginning with the start position until the end + * of the array + */ Stream> splice(int start); + /** + * Splice the array to return values from a given starting position + * containing up to the specified length. + * + * @param start the start position + * @param length the length + * @return an array of values beginning with the start index and up + * to the length of values subsequently. + */ Stream> splice(int start, int length); + /** + * Returns a stream of JsonValues in the array + * + * @return a stream of JsonValues + */ Stream> jsonValues(); + /** + * Returns a stream of raw values in the array + * + * @return a stream of raw values in the array + */ Stream values(); } diff --git a/model/src/main/java/io/github/xmljim/json/model/JsonElement.java b/model/src/main/java/io/github/xmljim/json/model/JsonElement.java index a1ff843..7b4a7e9 100644 --- a/model/src/main/java/io/github/xmljim/json/model/JsonElement.java +++ b/model/src/main/java/io/github/xmljim/json/model/JsonElement.java @@ -2,32 +2,92 @@ import io.github.xmljim.json.exception.JsonException; +/** + * Root interface for all Json types + */ public sealed interface JsonElement permits JsonNode, JsonValue { + /** + * The node type for the element + * + * @return the node type for the element + */ NodeType type(); + /** + * @return the parent node + * @deprecated Will be removed in a future version. + */ + @Deprecated JsonElement parent(); + /** + * Return the JSON string representation for this element + * + * @return the JSON string representation for this element + */ String toJsonString(); + /** + * Pretty print the JSON string with indentation + * + * @return the indented JSON string + */ String prettyPrint(); + /** + * Pretty print the JSON string with the specified indentation + * + * @param indent the number of spaces to index + * @return Pretty printed JSON string with the specified indentation + */ String prettyPrint(int indent); + /** + * Evaluates whether the element is a JsonNode (i.e., a JsonObject or JsonArray) + * + * @return true if the element is a JsonNode + */ default boolean isNode() { return !type().isPrimitive(); } + /** + * Evaluates whether the element is a JsonValue (leaf node) + * + * @return true if the element is a leaf node + */ default boolean isValue() { return type().isPrimitive(); } + /** + * Evaluates whether another element is equivalent to the current element. + * Equivalence means that JsonObject key-value pairs are the same, though they + * may not be in the same order; likewise, JsonArray values must be in the same + * order and each value must be equal + * + * @param other the other element + * @return true if they are equivalent + */ boolean isEquivalent(JsonElement other); + /** + * Cast the element to a JsonValue + * + * @return the element as a JsonValue + * @throws ClassCastException if the element is not a JsonValue + */ default JsonValue asJsonValue() { return (JsonValue) this; } + /** + * Cast the element as a JsonArray + * + * @return the element as a JsonArray + * @throws ClassCastException if the element is not a JsonArray + */ default JsonArray asJsonArray() { if (this instanceof JsonValue arrayValue) { return (JsonArray) arrayValue.get(); @@ -36,6 +96,12 @@ default JsonArray asJsonArray() { return (JsonArray) this; } + /** + * Cast the element as a JsonObject + * + * @return the element as a JsonObject + * @throws ClassCastException if the element is not a JsonObject + */ default JsonObject asJsonObject() { if (this instanceof JsonValue arrayValue) { return (JsonObject) arrayValue.get(); @@ -45,6 +111,13 @@ default JsonObject asJsonObject() { return null; } + /** + * Return the element as a String value. The element must be + * a {@link JsonValue} with a {@link NodeType} equal to {@link NodeType#STRING} + * + * @return the string value + * @throws JsonException if the element is not a string value + */ default String asString() { if (this instanceof JsonValue value && this.type() == NodeType.STRING) { return value.value(); @@ -53,6 +126,14 @@ default String asString() { } } + /** + * Return the element as a Number value. The element must be + * a {@link JsonValue} with a {@link NodeType} equal to {@link NodeType#NUMBER} + * (or return {@link NodeType#isNumeric()} equal to {@code true}) + * + * @return the string value + * @throws JsonException if the element is not a number value + */ default Number asNumber() { if (this instanceof JsonValue value && this.type().isNumeric()) { return value.value(); @@ -61,6 +142,13 @@ default Number asNumber() { } } + /** + * Return the element as a Number value. The element must be + * a {@link JsonValue} with a {@link NodeType} equal to {@link NodeType#BOOLEAN} + * + * @return the string value + * @throws JsonException if the element is not a boolean value + */ default Boolean asBoolean() { if (this instanceof JsonValue value && this.type() == NodeType.BOOLEAN) { return value.value(); diff --git a/model/src/main/java/io/github/xmljim/json/model/JsonNode.java b/model/src/main/java/io/github/xmljim/json/model/JsonNode.java index 91d95f7..17b145b 100644 --- a/model/src/main/java/io/github/xmljim/json/model/JsonNode.java +++ b/model/src/main/java/io/github/xmljim/json/model/JsonNode.java @@ -1,11 +1,27 @@ package io.github.xmljim.json.model; +/** + * Parent interface for Json containers (JsonArray and JsonObject) + */ public sealed interface JsonNode extends JsonElement, Comparable permits JsonObject, JsonArray { + /** + * Return the number of elements within the container + * + * @return the number of elements in the container + */ int size(); + /** + * Clear the elements in the container + */ void clear(); + /** + * Returns whether the container is empty + * + * @return true if no elements are contained within + */ default boolean isEmpty() { return size() == 0; } diff --git a/model/src/main/java/io/github/xmljim/json/model/JsonObject.java b/model/src/main/java/io/github/xmljim/json/model/JsonObject.java index 941b27d..7a09f01 100644 --- a/model/src/main/java/io/github/xmljim/json/model/JsonObject.java +++ b/model/src/main/java/io/github/xmljim/json/model/JsonObject.java @@ -4,41 +4,129 @@ import java.util.Optional; import java.util.stream.Stream; +/** + * Represents an associative array of elements with key-value pairs + */ public non-sealed interface JsonObject extends JsonNode { + /** + * Returns whether the associative array contains a specified key + * + * @param key the key to locate + * @return true if the key is found + */ boolean containsKey(String key); + /** + * Add/update a specified value in the associative array + * + * @param key the key, cannot be null + * @param value the value. Will be converted to a {@link JsonValue} + * @param The Json value type + * @param the raw value type + * @return the JsonValue of the specified value assigned to the key + */ JsonValue put(String key, T value); + /** + * Add a new key-value pair, if the key does not exist + * + * @param key the key + * @param value the value + * @param the JsonValue type + * @param the raw value type + * @return a JsonValue of the specified raw value + */ JsonValue putIfAbsent(String key, T value); + /** + * Append key-value pairs from a map + * + * @param map the map instance + */ void putAll(Map map); + /** + * Remove a value by its key + * + * @param key the key + * @param the value type + * @return the removed value + */ JsonValue remove(String key); + /** + * Return a raw value from the associative array from the specified key + * + * @param key the key + * @param the raw value type + * @return the raw value + */ @SuppressWarnings("unchecked") default V get(String key) { - return (V) getOptional(key).orElse(null); + Object value = getOptional(key).orElse(null); + return (V) value; } + /** + * Return the optional value for a given key + * + * @param key the key + * @param the value type + * @return The Optional for the given key. May be empty. + */ @SuppressWarnings("unchecked") default Optional getOptional(String key) { - return (Optional) value(key).map(JsonValue::get); + Optional> valueOptional = value(key); + return valueOptional.map(objectJsonValue -> (Optional) Optional.of(objectJsonValue.get())).orElseGet(() -> Optional.ofNullable(null)); + } + /** + * Return the given value for a key, or a default value if the key does not exist + * + * @param key the key + * @param defaultValue the default value if the key doesn't exist + * @param the value type + * @return the retrieved or default value + */ @SuppressWarnings("unchecked") default V getOrDefault(String key, V defaultValue) { return (V) getOptional(key).orElse(defaultValue); } + /** + * Return the optional JsonValue for a given key + * + * @param key the key + * @param the value type + * @return the Optional of the JsonValue. May be empty if the key does not exist + */ Optional> value(String key); + /** + * Return the JsonValue for a given key + * + * @param key the key + * @param the value type + * @return the JsonValue or null if the key doesn't exist + */ @SuppressWarnings("unchecked") default JsonValue getValue(String key) { return (JsonValue) value(key).orElse(null); } + /** + * Return a stream of the associative array's keys + * + * @return stream of keys + */ Stream keys(); + /** + * Return a stream of the associative array's values + * + * @return the stream of raw values + */ Stream values(); } diff --git a/model/src/main/java/io/github/xmljim/json/model/JsonValue.java b/model/src/main/java/io/github/xmljim/json/model/JsonValue.java index 2ab126c..d54a069 100644 --- a/model/src/main/java/io/github/xmljim/json/model/JsonValue.java +++ b/model/src/main/java/io/github/xmljim/json/model/JsonValue.java @@ -1,9 +1,25 @@ package io.github.xmljim.json.model; +/** + * Represents a value contained by a JsonNode container instance + * + * @param The value type + */ public non-sealed interface JsonValue extends JsonElement, Comparable> { + /** + * Return the raw value + * + * @return the raw value + */ T get(); + /** + * Return the casted value + * + * @param the value type + * @return the casted value + */ @SuppressWarnings("unchecked") default V value() { return (V) get(); diff --git a/model/src/main/java/io/github/xmljim/json/model/NodeType.java b/model/src/main/java/io/github/xmljim/json/model/NodeType.java index d4a8a81..4d2efbb 100644 --- a/model/src/main/java/io/github/xmljim/json/model/NodeType.java +++ b/model/src/main/java/io/github/xmljim/json/model/NodeType.java @@ -1,16 +1,56 @@ package io.github.xmljim.json.model; +import java.util.Collection; + +/** + * An enumeration of node/value types + */ public enum NodeType { + /** + * A JsonArray + */ ARRAY(false, false), + /** + * A JsonObject + */ OBJECT(false, false), + /** + * A boolean value + */ BOOLEAN(false, true), + /** + * A double value + */ DOUBLE(true, true), + /** + * A float value + * + * @deprecated may remove in favor of using double + */ FLOAT(true, true), + /** + * An integer value + */ INTEGER(true, true), + /** + * A long value + */ LONG(true, true), + /** + * A numeric value ("parent" of all numeric value types) + */ NUMBER(true, true), + /** + * A null value + */ NULL(false, true), + /** + * A string value + */ STRING(false, true), + /** + * INTERNAL: Undefined value + */ UNDEFINED(false, false); private final boolean numeric; @@ -21,23 +61,84 @@ public enum NodeType { this.primitive = primitive; } + /** + * returns true if the node type is a number type + * + * @return true if the node type is a number type + */ public boolean isNumeric() { return this.numeric; } + /** + * Returns true if the value is a primitive value type (String, Number, Boolean) + * + * @return true if the value is a primitive value type + */ public boolean isPrimitive() { return this.primitive; } + /** + * Returns true if the value is a JsonArray + * + * @return true if the value is a JsonArray + */ public boolean isArray() { return this.name().equals(ARRAY.name()); } + /** + * Returns true if the value is a JsonObject + * + * @return true if the value is a JsonObject + */ public boolean isObject() { return this.name().equals(OBJECT.name()); } + /** + * Do not use + * + * @return do not use + */ public boolean isUndefined() { return this.name().equals(UNDEFINED.name()); } + + public static NodeType fromClassType(Class classType) { + if (classType.equals(String.class)) { + return STRING; + } + + if (classType.equals(int.class) || classType.equals(Integer.class)) { + return INTEGER; + } + + if (classType.equals(long.class) || classType.equals(Long.class)) { + return LONG; + } + + if (classType.equals(double.class) || classType.equals(Double.class)) { + return DOUBLE; + } + + if (classType.equals(boolean.class) || classType.equals(Boolean.class)) { + return BOOLEAN; + } + + if (Collection.class.isAssignableFrom(classType)) { + return ARRAY; + } + + if (classType.isArray()) { + return ARRAY; + } + + if (!classType.isPrimitive()) { + return OBJECT; + } + + return UNDEFINED; + } } diff --git a/parser/src/main/java/io/github/xmljim/json/parser/DefaultParser.java b/parser/src/main/java/io/github/xmljim/json/parser/DefaultParser.java index 33d14ba..610b97e 100644 --- a/parser/src/main/java/io/github/xmljim/json/parser/DefaultParser.java +++ b/parser/src/main/java/io/github/xmljim/json/parser/DefaultParser.java @@ -21,6 +21,8 @@ public ParserSettings getSettings() { @Override public void setSettings(ParserSettings settings) { + if (settings != null) { + } this.settings = settings; } } diff --git a/parser/src/main/java/io/github/xmljim/json/parser/ParserSettingsImpl.java b/parser/src/main/java/io/github/xmljim/json/parser/ParserSettingsImpl.java index c39b09b..6aaeab1 100644 --- a/parser/src/main/java/io/github/xmljim/json/parser/ParserSettingsImpl.java +++ b/parser/src/main/java/io/github/xmljim/json/parser/ParserSettingsImpl.java @@ -148,8 +148,8 @@ public static ParserSettingsImpl defaultSettings() { settings.setFloatingNumberStrategy(FloatingNumberValueType.DOUBLE); settings.setBlockCount(4); settings.setEnableStatistics(true); - settings.setMaxBufferEventCapacity(32); - settings.setNextRequestLength(1); + settings.setMaxBufferEventCapacity(419200); + settings.setNextRequestLength(104800); settings.setCharacterSet(StandardCharsets.UTF_8); settings.setAssembler(Assemblers.newDefaultAssembler()); settings.setEventHandler(EventHandlers.defaultEventHandler(settings)); @@ -157,4 +157,28 @@ public static ParserSettingsImpl defaultSettings() { return settings; } + + public ParserSettings merge(ParserSettings parserSettings) { + + this.assembler = parserSettings.getAssembler() == null ? this.assembler : ( + this.assembler.getClass().getName().equals(parserSettings.getAssembler().getClass().getName()) ? this.assembler : + parserSettings.getAssembler()); + this.blockCount = parserSettings.getBlockCount() != blockCount ? parserSettings.getBlockCount() : blockCount; + this.charset = parserSettings.getCharacterSet() == null ? this.charset : (this.charset.equals(parserSettings.getCharacterSet()) ? + this.charset : parserSettings.getCharacterSet()); + this.enableStatistics = this.enableStatistics != parserSettings.enableStatistics() ? parserSettings.enableStatistics() : this.enableStatistics; + this.eventHandler = parserSettings.getEventHandler() == null ? this.eventHandler : (this.eventHandler.getClass().getName().equals(parserSettings.getEventHandler().getClass().getName()) ? + parserSettings.getEventHandler() : this.eventHandler); + this.fixedNumberStrategy = parserSettings.fixedNumberStrategy() == null ? this.fixedNumberStrategy : + (parserSettings.fixedNumberStrategy().equals(this.fixedNumberStrategy) ? this.fixedNumberStrategy : parserSettings.fixedNumberStrategy()); + this.floatingNumberStrategy = parserSettings.floatingNumberStrategy() == null ? this.floatingNumberStrategy : + (parserSettings.floatingNumberStrategy().equals(this.floatingNumberStrategy) ? this.floatingNumberStrategy : parserSettings.floatingNumberStrategy()); + this.maxBufferEventCapacity = this.maxBufferEventCapacity == parserSettings.getMaxEventBufferCapacity() ? + this.maxBufferEventCapacity : parserSettings.getMaxEventBufferCapacity(); + this.useStrict = this.useStrict == parserSettings.useStrict() ? useStrict : parserSettings.useStrict(); + this.processor = parserSettings.getProcessor() == null ? this.processor : + (this.processor.getClass().getName().equals(parserSettings.getProcessor().getClass().getName()) ? this.processor : + parserSettings.getProcessor()); + return this; + } } diff --git a/parser/src/main/java/io/github/xmljim/json/parser/event/BaseEventProcessor.java b/parser/src/main/java/io/github/xmljim/json/parser/event/BaseEventProcessor.java index 14fe916..c291fbe 100644 --- a/parser/src/main/java/io/github/xmljim/json/parser/event/BaseEventProcessor.java +++ b/parser/src/main/java/io/github/xmljim/json/parser/event/BaseEventProcessor.java @@ -17,6 +17,8 @@ abstract class BaseEventProcessor extends SubmissionPublisher impleme private final Timer eventTimer = new Timer<>(); private ParserSettings settings; + private JsonEvent lastEvent; + public BaseEventProcessor(final ParserSettings settings) { super(ForkJoinPool.commonPool(), settings.getMaxEventBufferCapacity()); this.settings = settings; @@ -43,6 +45,11 @@ public long getSendTime() { */ protected void sendEvent(JsonEvent event) { submit(event); + lastEvent = event; + } + + protected JsonEvent getLastEvent() { + return lastEvent; } protected void fireArrayEndEvent(int lineNumber, int column) { diff --git a/parser/src/main/java/io/github/xmljim/json/parser/event/BufferedEventProcessor.java b/parser/src/main/java/io/github/xmljim/json/parser/event/BufferedEventProcessor.java index e5705bb..29bed14 100644 --- a/parser/src/main/java/io/github/xmljim/json/parser/event/BufferedEventProcessor.java +++ b/parser/src/main/java/io/github/xmljim/json/parser/event/BufferedEventProcessor.java @@ -3,12 +3,16 @@ import io.github.xmljim.json.factory.parser.JsonEventParserException; import io.github.xmljim.json.factory.parser.ParserSettings; import io.github.xmljim.json.factory.parser.Statistics; +import io.github.xmljim.json.factory.parser.event.EventType; import io.github.xmljim.json.parser.util.*; import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.NoSuchElementException; import java.util.concurrent.TimeUnit; import static io.github.xmljim.json.parser.event.ActionConstants.*; @@ -46,6 +50,12 @@ class BufferedEventProcessor extends BaseEventProcessor { private boolean documentStarted = false; private boolean escapeFlag = false; private long byteCount; + + private final Deque expectedDelimiters = new ArrayDeque<>(); + private final Deque containerStack = new ArrayDeque<>(); + + private MarkerInfo lastMarker; + private final Timer processorTimer = new Timer<>(); public BufferedEventProcessor(ParserSettings settings) { @@ -77,7 +87,7 @@ public void process(InputStream inputStream) { throw new JsonEventParserException(e); } - + checkDocumentEnd(); fireDocumentEndEvent(lineNumber, column); processorTimer.stop(this); @@ -102,66 +112,119 @@ public Statistics getStatistics() { return statistics; } + /** + * Change state for start of a string token and fire event + */ private void notifyStringTokenStart() { setTokenState(TOKEN_STATE_STRING); fireStringStartEvent(lineNumber, column); } + /** + * Change state for end of string token and fire event + * + * @param tokenValue The token data to pass to event + */ private void notifyStringTokenEnd(ByteBuffer tokenValue) { resetToken(); fireStringEndEvent(tokenValue, lineNumber, column); } + /** + * Change state for start of boolean token and fire event + */ private void notifyBooleanTokenStart() { setTokenState(TOKEN_STATE_BOOLEAN); fireBooleanStartEvent(lineNumber, column); } + /** + * Change state for end of boolean token and fire event + * + * @param tokenValue the boolean token value to pass to event + */ private void notifyBooleanTokenEnd(ByteBuffer tokenValue) { validateBoolean(tokenValue); resetToken(); fireBooleanEndEvent(tokenValue, lineNumber, column); } + /** + * Change state for number start token and fire event + */ private void notifyNumberTokenStart() { setTokenState(TOKEN_STATE_NUMBER); fireNumberStartEvent(lineNumber, column); } + /** + * Change state for end of number token and fire event + * + * @param tokenValue the number token value to pass to event + */ private void notifyNumberTokenEnd(ByteBuffer tokenValue) { resetToken(); fireNumberEndEvent(tokenValue, lineNumber, column); } + /** + * Change state for start of null token and fire event; + */ private void notifyNullTokenStart() { setTokenState(TOKEN_STATE_NULL); fireNullStartEvent(lineNumber, column); } + /** + * Change state for end of null token and fire event + * + * @param tokenValue the null token value (should be equivalent of 'null' ByteSequence) + */ private void notifyNullTokenEnd(ByteBuffer tokenValue) { validateNull(tokenValue); resetToken(); fireNullEndEvent(tokenValue, lineNumber, column); } + /** + * Change state for end of entity element (object or array) and fire event + */ private void notifyEntityEnd() { resetToken(); fireEntityEndEvent(lineNumber, column); } + /** + * Change state for end of object key and fire event + */ private void notifyKeyEnd() { resetToken(); fireKeyEndEvent(lineNumber, column); } + /** + * Get current line number + * + * @return the line number + */ public int getLineNumber() { return lineNumber; } + /** + * Get current column number + * + * @return the column number + */ public int getColumn() { return column; } + /** + * Process a block of data + * + * @param block the block of data + */ private void processBlock(ByteBuffer block) { if (block != null) { while (block.hasRemaining()) { @@ -169,10 +232,16 @@ private void processBlock(ByteBuffer block) { handleByte(b); } } else { + checkDocumentEnd(); fireDocumentEndEvent(lineNumber, column); } } + /** + * Process a byte. Evaluate current state and call specific handle method + * + * @param b the byte + */ private void handleByte(byte b) { byteCount++; @@ -201,10 +270,21 @@ private void handleByte(byte b) { } + /** + * Return whether current byte is a whitespace value + * + * @param b the current byte + * @return {@code true} if the byte is a known whitespace value; false otherwise + */ private boolean isWhitespace(byte b) { return hasByte(b, whitespaceRange); } + /** + * Handle a whitespace byte + * + * @param b the whitespace byte + */ private void handleWhitespace(byte b) { switch (b) { case ByteConstants.SPACE -> { @@ -228,10 +308,21 @@ private void handleWhitespace(byte b) { } } + /** + * Evaluates whether the current byte is a numeric value + * + * @param byt the current byte + * @return {@code true} if the current byte is a known numeric value; false otherwise + */ private boolean isNumeric(byte byt) { return hasByte(byt, numberRange) && (!inToken() || inNumberToken()); } + /** + * Handle the current byte as a numeric character value + * + * @param byt the current byte + */ private void handleNumeric(byte byt) { if (!inToken()) { notifyNumberTokenStart(); @@ -252,11 +343,22 @@ private void handleNumeric(byte byt) { } } + /** + * Evaluates if the current byte should be handled as a boolean value + * + * @param byt the current byte + * @return {@code true} if the current byte should be handled as a boolean value; false otherwise + */ private boolean isBoolean(byte byt) { // final boolean isBool = (hasByte(byt, booleanRange) && ((!inToken() && ((byt == t) || (byt == f)) )|| inBooleanToken())); return hasByte(byt, booleanRange) && (!inToken() && (byt == ByteConstants.t || byt == ByteConstants.f) || inBooleanToken()); } + /** + * Handle the current byte as a boolean value + * + * @param byt the current byte + */ private void handleBoolean(byte byt) { if (!inToken()) { notifyBooleanTokenStart(); @@ -266,10 +368,21 @@ private void handleBoolean(byte byt) { } + /** + * Evaluate if the current by should be handled as a null value + * + * @param byt the current byte + * @return {@code true} if the current byte should be handled as null value; false otherwise + */ private boolean isNull(byte byt) { return hasByte(byt, nullRange) && (!inToken() && byt == ByteConstants.n || inNullToken()); } + /** + * Handle the current byte as a null value token + * + * @param byt the current byte + */ private void handleNull(byte byt) { if (!inToken()) { notifyNullTokenStart(); @@ -278,11 +391,31 @@ private void handleNull(byte byt) { appendToken(byt); } + /** + * Evaluate if the current byte is a delimiter (comma or colon) and should be handled as a delimiter + * + * @param byt the current byte + * @return {@code true} if the current byte should be handled as a delimiter; false otherwise; + */ private boolean isDelimiter(byte byt) { return hasByte(byt, delimiterRange) && !inStringToken(); } + /** + * Handle delimiter between key/value pairs and entity elements + * + * @param byt the current byte + */ private void handleDelimiter(byte byt) { + //mark the delimiter + lastMarker = new MarkerInfo(byt, getLineNumber(), getColumn() + 1); + //if not the next expected delimiter, throw error + + byte expectedDelimiter = getExpectedDelimiter(); + if (byt != expectedDelimiter) { + throwError("Unexpected delimiter: '" + (char) byt + "'"); + } + notifyEndToken(); if (byt == ByteConstants.COMMA) { @@ -291,13 +424,26 @@ private void handleDelimiter(byte byt) { notifyKeyEnd(); } + pushNextExpectedDelimiter(expectedDelimiter); + incrementColumn(); } + /** + * Evaluate if the current byte should be handled as a container marker + * + * @param byt the current byte + * @return {@code true} if the byte should be handled as a container marker; false otherwise + */ private boolean isContainer(byte byt) { - return hasByte(byt, containerRange); + return hasByte(byt, containerRange) && !inStringToken(); } + /** + * Handle the current byte as a container marker + * + * @param byt the current byte + */ private void handleContainer(byte byt) { if (inStringToken()) { @@ -316,9 +462,24 @@ private void handleContainer(byte byt) { } else { fireArrayStartEvent(lineNumber, column); } + //push container to stack + setContainer(byt); } - case ByteConstants.END_ARRAY -> fireArrayEndEvent(lineNumber, column); + case ByteConstants.END_ARRAY -> { + MarkerInfo markerInfo = containerStack.peek(); + byte expectedContainer = getContainer(); + byte expectedDelimiter = getExpectedDelimiter(); + if (expectedContainer == ByteConstants.START_ARRAY && expectedDelimiter == ByteConstants.COMMA) { + fireArrayEndEvent(lineNumber, column); + } else { + if (expectedContainer != ByteConstants.START_ARRAY) { + throwError("Expected an object closure ('}'), but read array closure ']' [" + markerInfo + "]"); + } else { + throwError("Expected last delimiter to be a comma, but read key delimiter ':'"); + } + } + } case ByteConstants.START_MAP -> { if (!documentStarted) { fireDocumentStartEvent(ByteBuffer.allocate(1).put(byt), lineNumber, column); @@ -327,8 +488,26 @@ private void handleContainer(byte byt) { } else { fireMapStartEvent(lineNumber, column); } + setContainer(byt); + } + case ByteConstants.END_MAP -> { + MarkerInfo markerInfo = containerStack.peek(); + byte expectedContainer = getContainer(); + byte expectedDelimiter = getExpectedDelimiter(); + + if (expectedContainer == ByteConstants.START_MAP && expectedDelimiter == ByteConstants.COMMA + && getLastEvent().getEventType() != EventType.KEY_END) { + fireMapEndEvent(lineNumber, column); + } else { + if (getLastEvent().getEventType() == EventType.KEY_END || getLastEvent().getEventType() == EventType.STRING_END) { + throwError("Object entity missing value. Last valid event: " + getLastEvent()); + } else if (expectedContainer != ByteConstants.START_MAP) { + throwError("Expected an array closure (']'), but read object closure '}' [" + markerInfo + "]"); + } else { + throwError("Extra comma delimiter: [" + lastMarker + "]"); + } + } } - case ByteConstants.END_MAP -> fireMapEndEvent(lineNumber, column); } incrementColumn(); @@ -336,10 +515,21 @@ private void handleContainer(byte byt) { } } + /** + * Evaluate if the current byte should be handled as a special character (i.e., backslash, quote, or start of boolean or null value) + * + * @param byt the current byte + * @return {@code true} if the current byte should be handled as a special character; false otherwise; + */ private boolean isSpecialCharacter(byte byt) { return inStringToken() && escapeRange.contains(byt); } + /** + * Handle special character + * + * @param byt the current character + */ private void handleSpecialCharacters(byte byt) { if (escapeFlag) { @@ -363,6 +553,11 @@ private void handleSpecialCharacters(byte byt) { } + /** + * Handle string value + * + * @param byt the current byte to processed as a string + */ private void handleString(byte byt) { if (!inToken()) { if (byt == ByteConstants.QUOTE) { @@ -382,6 +577,9 @@ private void handleString(byte byt) { } + /** + * Fire event at the end of token processing and reset state + */ private void notifyEndToken() { switch (tokenState) { case TOKEN_STATE_BOOLEAN -> notifyBooleanTokenEnd(token.toByteBuffer()); @@ -393,33 +591,67 @@ private void notifyEndToken() { resetToken(); } + /** + * Reset token state + */ private void resetToken() { token.clear(); setTokenState(TOKEN_STATE_EMPTY); } + /** + * Evaluate a byte against a defined {@link ByteRange} + * + * @param byt the current byte + * @param range the defined ByteRange + * @return {@code true} if the range contains this byte; false otherwise + */ private boolean hasByte(byte byt, ByteRange range) { return range.contains(byt); } + /** + * Syntactic sugar for {@link #hasByte(byte, ByteRange)}. Just wraps the ByteBuffer in a ByteRange + * + * @param byt the current byte + * @param buffer the current ByteBuffer + * @return {@code true} if the range contains this byte; false otherwise + */ private boolean hasByte(byte byt, ByteBuffer buffer) { return ByteRange.startWith(buffer.array()).contains(byt); } + /** + * Append a byte to the current token + * + * @param byt the current byte to append + */ private void appendToken(byte byt) { token.add(byt); incrementColumn(); } + /** + * Increment the column cursor + */ private void incrementColumn() { column++; } + /** + * Increment the line cursor + */ private void incrementLine() { lineNumber++; column = 0; } + /** + * Read a block of data + * + * @return a ByteBuffer containing a block of data + * @throws IOException thrown if an error occurs + */ private ByteBuffer readBlock() throws IOException { final byte[] byteBuffer = new byte[getSettings().getBlockSizeBytes()]; final int readResult = getStream().read(byteBuffer); @@ -435,34 +667,74 @@ private ByteBuffer readBlock() throws IOException { return buffer; } + /** + * Return the underlying inputstream holding the JSON data + * + * @return the underlying inputStream; + */ private InputStream getStream() { return bis; } + /** + * Evaluate if the current state is processing a string + * + * @return {@code true} if the current state is processing a string + */ private boolean inStringToken() { return tokenState == TOKEN_STATE_STRING; } + /** + * Evaluate if the current state is processing a boolean + * + * @return {@code true} if the current state is processing a boolean + */ private boolean inBooleanToken() { return tokenState == TOKEN_STATE_BOOLEAN; } + /** + * Evaluate if the current state is processing a number + * + * @return {@code true} if the current state is processing a number + */ private boolean inNumberToken() { return tokenState == TOKEN_STATE_NUMBER; } + /** + * Evaluate if the current state is processing a null + * + * @return {@code true} if the current state is processing a null + */ private boolean inNullToken() { return tokenState == TOKEN_STATE_NULL; } + /** + * Evaluate if the current state is processing anything + * + * @return {@code true} if the current state is processing anything + */ private boolean inToken() { return tokenState != TOKEN_STATE_EMPTY; } + /** + * Apply a token state + * + * @param tokenState the token state to apply + */ private void setTokenState(int tokenState) { this.tokenState = tokenState; } + /** + * Validate if the current byte buffer is a boolean token + * + * @param booleanData the byte array containing the boolean data + */ private void validateBoolean(ByteBuffer booleanData) { ByteSequence sequenceToUse = null; if (booleanData.array()[0] == ByteConstants.t) { @@ -484,6 +756,11 @@ private void validateBoolean(ByteBuffer booleanData) { } } + /** + * Validate if the current byte buffer is a null token + * + * @param nullData the byte array containing the null data + */ private void validateNull(ByteBuffer nullData) { if (!nullSequence.matches(nullData.array())) { throwError("Unexpected null value: expected " + nullSequence @@ -491,7 +768,164 @@ private void validateNull(ByteBuffer nullData) { } } + /** + * Throw an error + * + * @param message the error message + */ private void throwError(String message) { throw new JsonEventParserException(getLineNumber(), getColumn(), message); } + + /** + * pop and return the last pushed delimiter byte value. + * + * @return the last pushed delimiter byte value. + */ + private byte getExpectedDelimiter() { + try { + return expectedDelimiters.pop(); + } catch (NoSuchElementException e) { + String message = "Expecting to find a delimiter, but not found"; + throwError(message); + + } + + return 0; + } + + /** + * push new delimiter to stack if the container stack is not empty and evaluate the last delimiter + * based on the container. + * + * @param lastDelimiter the last delimiter popped. + */ + private void pushNextExpectedDelimiter(byte lastDelimiter) { + if (containerNotEmpty() && peekContainer() == ByteConstants.START_ARRAY) { + setExpectedDelimiter(ByteConstants.COMMA); + } else if (containerNotEmpty() && peekContainer() == ByteConstants.START_MAP) { + if (lastDelimiter == ByteConstants.COLON) { + setExpectedDelimiter(ByteConstants.COMMA); + } else if (lastDelimiter == ByteConstants.NULL || lastDelimiter == ByteConstants.COMMA) { + setExpectedDelimiter(ByteConstants.COLON); + } else { + throwError("Unexpected delimiter: " + (char) lastDelimiter); + } + } + } + + /** + * Push a delimiter to stack + * + * @param delimiter the delimiter + */ + private void setExpectedDelimiter(byte delimiter) { + if (delimiter == ByteConstants.COLON || delimiter == ByteConstants.COMMA) { + expectedDelimiters.push(delimiter); + } else { + throwError("Unexpected delimiter: " + (char) delimiter); + } + } + + /** + * Evaluate if container stack is not empty + * + * @return {@code true} if not empty + */ + private boolean containerNotEmpty() { + return !containerStack.isEmpty(); + + } + + /** + * Peek and return the last container + * + * @return the last container without popping stack + */ + private byte peekContainer() { + if (containerStack.isEmpty()) { + throwError("Unexpected empty entity container"); + } + return containerStack.peek().containerByte(); + } + + /** + * Pop and return the last container + * + * @return the last container + */ + private byte getContainer() { + try { + return containerStack.pop().containerByte(); + } catch (NoSuchElementException e) { + throwError("Missing entity container"); + } + + return 0; + } + + /** + * Push a new container marker to stack + * + * @param containerByte the container byte + */ + private void setContainer(byte containerByte) { + if (containerByte == ByteConstants.START_ARRAY || containerByte == ByteConstants.START_MAP) { + containerStack.push(new MarkerInfo(containerByte, getLineNumber(), getColumn())); + //we'll set the next delimiter based on the container + pushNextExpectedDelimiter(ByteConstants.NULL); + } else { + throwError("Unexpected container " + (char) containerByte); + } + } + + /** + * Last check after processing all bytes to ensure that there are no "dangling" containers + */ + private void checkDocumentEnd() { + if (!containerStack.isEmpty()) { + MarkerInfo info = containerStack.peek(); + if (info.isArray()) { + throw new JsonEventParserException(info.row(), info.column(), "Array container missing closing character ']'"); + } else { + throw new JsonEventParserException(info.row(), info.column(), "Map container missing closing character '}'"); + } + } + } + + /** + * Record class holding marker data + * + * @param containerByte the marker byte + * @param row the line/row + * @param column the column + */ + private record MarkerInfo(byte containerByte, int row, int column) { + /** + * Return true if the marker byte is a map + * + * @return true if map byte; false otherwise + */ + public boolean isMap() { + return containerByte() == ByteConstants.START_MAP; + } + + /** + * Evaluate if byte is an array marker + * + * @return true if an array marker + */ + public boolean isArray() { + return containerByte() == ByteConstants.START_ARRAY; + } + + /** + * Return record as a string + * + * @return record as a string + */ + public String toString() { + return "Marker [char='" + (char) containerByte() + "'; row=" + row() + "; column=" + column() + "]"; + } + } } diff --git a/parser/src/main/java/io/github/xmljim/json/parser/event/DataEvent.java b/parser/src/main/java/io/github/xmljim/json/parser/event/DataEvent.java index de5dbb2..b66c300 100644 --- a/parser/src/main/java/io/github/xmljim/json/parser/event/DataEvent.java +++ b/parser/src/main/java/io/github/xmljim/json/parser/event/DataEvent.java @@ -1,6 +1,7 @@ package io.github.xmljim.json.parser.event; import io.github.xmljim.json.factory.parser.event.EventType; +import io.github.xmljim.json.parser.util.ByteSequence; import java.nio.ByteBuffer; import java.util.Objects; @@ -33,7 +34,7 @@ public ByteBuffer getData() { public String toString() { return new StringJoiner(", ", DataEvent.class.getSimpleName() + "[", "]") .add("eventType=" + eventType) - .add("data=" + data) + .add("data=" + ByteSequence.withStartingSequence(data.array())) .add("line=" + super.getLineNumber()) .add("column=" + super.getColumn()) .toString(); diff --git a/parser/src/main/java/io/github/xmljim/json/parser/event/JsonAssemblerImpl.java b/parser/src/main/java/io/github/xmljim/json/parser/event/JsonAssemblerImpl.java index 9cf0bdc..b416282 100644 --- a/parser/src/main/java/io/github/xmljim/json/parser/event/JsonAssemblerImpl.java +++ b/parser/src/main/java/io/github/xmljim/json/parser/event/JsonAssemblerImpl.java @@ -31,7 +31,7 @@ public void documentStart(NodeType type) { public void jsonArrayStart(String key) { assemblerTimer.start(this); - JsonArray array = getElementFactory().newArray(stack.peek()); + JsonArray array = getElementFactory().newArray(); appendToCurrent(key, array); assemblerTimer.stop(this); } @@ -47,7 +47,7 @@ public void jsonArrayEnd(String key) { @Override public void jsonObjectStart(String key) { assemblerTimer.start(this); - JsonObject object = getElementFactory().newObject(stack.peek()); + JsonObject object = getElementFactory().newObject(); appendToCurrent(key, object); assemblerTimer.stop(this); } @@ -139,7 +139,7 @@ public Statistics getStatistics() { } private void createAndAppendValue(String key, Object value) { - JsonValue val = getElementFactory().newValue(value, stack.peek()); + JsonValue val = getElementFactory().newValue(value); appendToCurrent(key, val); } diff --git a/parser/src/main/java/io/github/xmljim/json/parser/util/ByteRange.java b/parser/src/main/java/io/github/xmljim/json/parser/util/ByteRange.java index 97b092b..a11ba9f 100644 --- a/parser/src/main/java/io/github/xmljim/json/parser/util/ByteRange.java +++ b/parser/src/main/java/io/github/xmljim/json/parser/util/ByteRange.java @@ -293,23 +293,28 @@ private void addToArray(byte start, byte end) { } private void addToArray(byte newByte) { - growArray(1); - rangeData[cursor++] = newByte; - Arrays.sort(rangeData); + if (!contains(newByte)) { + growArray(1); + rangeData[cursor++] = newByte; + Arrays.sort(rangeData); + } } private void addToArray(byte[] byteArray) { growArray(byteArray.length); for (final byte b : byteArray) { - rangeData[cursor++] = b; + if (!isInRangeLocal(b)) { + rangeData[cursor++] = b; + } } Arrays.sort(rangeData); } private boolean isInRangeLocal(byte b) { - return Arrays.binarySearch(rangeData, b) >= 0; + int searchVal = Arrays.binarySearch(rangeData, b);// >= 0; + return searchVal >= 0; } @Override diff --git a/parser/src/main/java/io/github/xmljim/json/parser/util/ByteSequence.java b/parser/src/main/java/io/github/xmljim/json/parser/util/ByteSequence.java index c5ea893..504a48e 100644 --- a/parser/src/main/java/io/github/xmljim/json/parser/util/ByteSequence.java +++ b/parser/src/main/java/io/github/xmljim/json/parser/util/ByteSequence.java @@ -306,7 +306,7 @@ private void add(byte b) { * Adds a varargs of bytes to a ByteRange, and adds the ByteRange * as the next sequence * - * @param anyOf + * @param anyOf an array of bytes to add */ private void add(byte... anyOf) { final ByteRange range = ByteRange.empty(); @@ -332,7 +332,7 @@ private void add(byte start, byte end) { * and its Cardinality. * * @param cbt blurgh - * @category Not fully implemented yet. Cardinality needs additional code + * @apiNote Not fully implemented yet. Cardinality needs additional code */ private void add(CardinalityByteRange cbt) { sequence.add(cbt); @@ -341,7 +341,7 @@ private void add(CardinalityByteRange cbt) { /** * Appends the sequence from another ByteSequence * - * @param sequence + * @param sequence The ByteSequence */ private void add(ByteSequence sequence) { this.sequence.addAll(sequence.getSequenceItems()); @@ -350,12 +350,22 @@ private void add(ByteSequence sequence) { /** * Adds a new sequence from a ByteRange * - * @param range + * @param range the range of bytes */ private void add(ByteRange range) { add(new CardinalityByteRange(range)); } + /** + * Return an array of bytes + * + * @return an array of bytes + */ + public byte[] getBytes() { + ArrayList byteList = new ArrayList<>(); + return null; + } + @Override public String toString() { final StringBuilder builder = new StringBuilder(); diff --git a/parser/src/main/java/io/github/xmljim/json/parser/util/ResizableByteBuffer.java b/parser/src/main/java/io/github/xmljim/json/parser/util/ResizableByteBuffer.java index 215e820..6ff83b4 100644 --- a/parser/src/main/java/io/github/xmljim/json/parser/util/ResizableByteBuffer.java +++ b/parser/src/main/java/io/github/xmljim/json/parser/util/ResizableByteBuffer.java @@ -66,7 +66,9 @@ public ResizableByteBuffer(int allocateSize) { * @param initialByteArray the initial array of bytes */ public ResizableByteBuffer(byte[] initialByteArray) { - add(initialByteArray); + for (byte b : initialByteArray) { + add(b); + } } public static ResizableByteBuffer fromStream(InputStream inputStream) { @@ -264,8 +266,7 @@ public byte previous(int lookBehind) { * @return a new ByteBuffer instance */ public ByteBuffer toByteBuffer() { - final ByteBuffer buffer = ByteBuffer.wrap(Arrays.copyOfRange(byteArray, 0, size)); - return buffer; + return ByteBuffer.wrap(Arrays.copyOfRange(byteArray, 0, size)); } /** @@ -313,7 +314,7 @@ private byte[] slice(int offset, int length) { /** * Trims the underlying array to remove any unallocated items at the end of the array. * - * @return + * @return the trimmed byte array */ private byte[] trim() { if (size == 0) { @@ -334,7 +335,7 @@ private void ensureByteCapacity() { * Make sure the array has enough space for the minimum number of items to be added. Typically this will continue to add * at least 50% more space than what was asked for to avoid repeated resizing. * - * @param minimumCapacity + * @param minimumCapacity The minimum capacity for the byte array */ private void ensureByteCapacity(int minimumCapacity) { if (byteArray.length - (size + minimumCapacity) <= 0) { diff --git a/parser/src/main/java/module-info.java b/parser/src/main/java/module-info.java index 3a97de1..b9e7e29 100644 --- a/parser/src/main/java/module-info.java +++ b/parser/src/main/java/module-info.java @@ -4,8 +4,12 @@ module io.github.xmljim.json.parser { requires transitive io.github.xmljim.json.factory; - opens io.github.xmljim.json.parser; + exports io.github.xmljim.json.parser to io.xmljim.jsonparser.test; + exports io.github.xmljim.json.parser.event to io.xmljim.jsonparser.test; + exports io.github.xmljim.json.parser.util to io.xmljim.jsonparser.test; + opens io.github.xmljim.json.parser to io.github.xmljim.json.factory; provides ParserFactory with ParserFactoryImpl; + //exports io.github.xmljim.json.parser.event to io.github.xmljim.json.mapper; uses ElementFactory; } \ No newline at end of file diff --git a/parser/src/test/java/io/xmljim/json/parsertest/ParserFactoryTest.java b/parser/src/test/java/io/xmljim/json/parsertest/ParserFactoryTest.java index c510acf..60ab154 100644 --- a/parser/src/test/java/io/xmljim/json/parsertest/ParserFactoryTest.java +++ b/parser/src/test/java/io/xmljim/json/parsertest/ParserFactoryTest.java @@ -18,7 +18,6 @@ void testCreateParserFactory() { Parser parser = factory.newParser(); assertNotNull(parser); - } } diff --git a/parser/src/test/java/io/xmljim/json/parsertest/ParserTest.java b/parser/src/test/java/io/xmljim/json/parsertest/ParserTest.java index 2b85faf..fdf9b28 100644 --- a/parser/src/test/java/io/xmljim/json/parsertest/ParserTest.java +++ b/parser/src/test/java/io/xmljim/json/parsertest/ParserTest.java @@ -1,6 +1,7 @@ package io.xmljim.json.parsertest; import io.github.xmljim.json.factory.parser.InputData; +import io.github.xmljim.json.factory.parser.JsonEventParserException; import io.github.xmljim.json.factory.parser.Parser; import io.github.xmljim.json.factory.parser.ParserFactory; import io.github.xmljim.json.model.JsonObject; @@ -11,6 +12,8 @@ import java.io.InputStream; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; class ParserTest { @@ -44,4 +47,98 @@ public void testB() { fail(ioe); } } + + @Test + void testMissingValueError() { + String json = """ + { + "foo": "bar", + "boo": + } + """; + ParserFactory factory = ServiceManager.getProvider(ParserFactory.class); + Parser parser = factory.newParser(); + JsonEventParserException e = assertThrows(JsonEventParserException.class, () -> parser.parse(InputData.of(json))); + assertTrue(e.getMessage().startsWith("Object entity missing value")); + } + + @Test + void testInvalidDelimiterError() { + String json = """ + { + "foo": "bar", + "boo", "baz" + } + """; + ParserFactory factory = ServiceManager.getProvider(ParserFactory.class); + Parser parser = factory.newParser(); + JsonEventParserException e = assertThrows(JsonEventParserException.class, () -> parser.parse(InputData.of(json))); + assertTrue(e.getMessage().contains("Unexpected delimiter:")); + } + + @Test + void testExtraClosureError() { + String json = """ + { + "test": [ + "a", "b", "c" + ]] + } + """; + + ParserFactory factory = ServiceManager.getProvider(ParserFactory.class); + Parser parser = factory.newParser(); + JsonEventParserException e = assertThrows(JsonEventParserException.class, () -> parser.parse(InputData.of(json))); + assertTrue(e.getMessage().contains("Expected an object closure ('}'), but read array closure ']'")); + } + + @Test + void testMissingEnclosureError() { + String json = """ + { + "store": { + "book": [ + { + "category": "reference", + "author": "Nigel Rees", + "title": "Sayings of the Century", + "price": 8.95 + }, + { + "category": "fiction", + "author": "Evelyn Waugh", + "title": "Sword of Honour", + "price": 12.99 + }, + { + "category": "fiction", + "author": "Herman Melville", + "title": "Moby Dick", + "isbn": "0-553-21311-3", + "price": 8.99 + }, + { + "category": "fiction", + "author": "J. R. R. Tolkien", + "title": "The Lord of the Rings", + "isbn": "0-395-19395-8", + "price": 22.99 + } + ], + "bicycle": { + "color": "red", + "price": 19.95 + } + }, + "expensive": 10 + + """; + + ParserFactory factory = ServiceManager.getProvider(ParserFactory.class); + Parser parser = factory.newParser(); + JsonEventParserException e = assertThrows(JsonEventParserException.class, () -> parser.parse(InputData.of(json))); + assertTrue(e.getMessage().contains("Map container missing closing character '}'")); + } + + } \ No newline at end of file diff --git a/parser/src/test/java/io/xmljim/json/parsertest/util/ByteRangeTest.java b/parser/src/test/java/io/xmljim/json/parsertest/util/ByteRangeTest.java new file mode 100644 index 0000000..48d470c --- /dev/null +++ b/parser/src/test/java/io/xmljim/json/parsertest/util/ByteRangeTest.java @@ -0,0 +1,121 @@ +package io.xmljim.json.parsertest.util; + +import io.github.xmljim.json.parser.util.ByteConstants; +import io.github.xmljim.json.parser.util.ByteRange; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +public class ByteRangeTest { + @Test + void empty() { + ByteRange byteRange = ByteRange.empty(); + assertEquals("", byteRange.toString()); + } + + @Test + void fromString() { + ByteRange byteRange = ByteRange.fromString("TEST"); + assertTrue(byteRange.contains(ByteConstants.T)); + assertTrue(byteRange.contains(ByteConstants.E)); + assertTrue(byteRange.contains(ByteConstants.S)); + assertFalse(byteRange.contains(ByteConstants.t)); + System.out.println(byteRange); + } + + @Test + void startWithSingleByte() { + ByteRange byteRange = ByteRange.startWith(ByteConstants.A); + assertEquals("A", byteRange.toString()); + } + + @Test + void startWithByteArray() { + ByteRange byteRange = ByteRange.startWith("JSON".getBytes()); + assertEquals(0, byteRange.getMatchingByteIndexes(new byte[]{ByteConstants.J})[0]); + assertTrue(byteRange.contains(ByteConstants.S, ByteConstants.O, ByteConstants.N)); + } + + @Test + void testStartWithRangeStartEnd() { + ByteRange byteRange = ByteRange.startWith(ByteConstants.a, ByteConstants.z); + assertTrue(byteRange.contains(ByteConstants.n)); + assertEquals(1, byteRange.getMatchingByteIndexes(new byte[]{ByteConstants.B, ByteConstants.b})[0]); + } + + @Test + void andAddFrom() { + ByteRange byteRange = ByteRange.startWith(ByteConstants.a, ByteConstants.z) + .andAddFrom(ByteConstants.A, ByteConstants.Z); + assertTrue(byteRange.contains(ByteConstants.a, ByteConstants.A, ByteConstants.D, ByteConstants.g)); + } + + @Test + void andAdd() { + ByteRange byteRange = ByteRange.startWith(ByteConstants.ZERO, ByteConstants.NINE).andAdd(ByteConstants.A); + assertTrue(byteRange.contains(ByteConstants.A)); + } + + @Test + void testAndAddByteArray() { + ByteRange byteRange = ByteRange.fromString("ABC").andAdd("abc".getBytes()); + assertEquals("ABCabc", byteRange.toString()); + } + + @Test + void testContainsVarArgsBytes() { + ByteRange byteRange = ByteRange.fromString("TEST"); + assertTrue(byteRange.contains(ByteConstants.T, ByteConstants.S)); + } + + @Test + void containsAll() { + ByteRange byteRange = ByteRange.fromString("STRAIGHT"); + assertTrue(byteRange.containsAll("STAR".getBytes())); + } + + @Test + void containsSome() { + ByteRange byteRange = ByteRange.fromString("baseball"); + assertTrue(byteRange.containsSome("bat".getBytes())); + } + + @Test + void containsCount() { + ByteRange byteRange = ByteRange.fromString("baseball"); + assertEquals(2, byteRange.containsCount("bat".getBytes())); + } + + @Test + void getMatchingByteIndexes() { + ByteRange byteRange = ByteRange.fromString("baseball"); + int[] values = byteRange.getMatchingByteIndexes("bat".getBytes()); + List actual = new ArrayList<>(); + Arrays.stream(values).forEach(actual::add); + assertEquals(List.of(0, 1), actual); + } + + @Test + void getMatchingByteValues() { + ByteRange byteRange = ByteRange.fromString("baseball"); + byte[] matching = byteRange.getMatchingByteValues("bat".getBytes()); + + List actual = new ArrayList<>(); + for (byte b : matching) { + actual.add(b); + } + + assertEquals(List.of(ByteConstants.b, ByteConstants.a), actual); + } + + @Test + void testToString() { + ByteRange byteRange = ByteRange.fromString("STAR"); + //remember that the bytes are sorted internally + assertEquals("ARST", byteRange.toString()); + } +} diff --git a/parser/src/test/java/io/xmljim/json/parsertest/util/ByteSequenceTest.java b/parser/src/test/java/io/xmljim/json/parsertest/util/ByteSequenceTest.java new file mode 100644 index 0000000..b1f3ec1 --- /dev/null +++ b/parser/src/test/java/io/xmljim/json/parsertest/util/ByteSequenceTest.java @@ -0,0 +1,83 @@ +package io.xmljim.json.parsertest.util; + +import io.github.xmljim.json.parser.util.ByteConstants; +import io.github.xmljim.json.parser.util.ByteSequence; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ByteSequenceTest { + + @Test + void emptySequence() { + ByteSequence sequence = ByteSequence.emptySequence(); + assertEquals("", sequence.toString()); + } + + @Test + void fromString() { + ByteSequence sequence = ByteSequence.fromString("T"); + assertTrue(sequence.matches(ByteConstants.T)); + + ByteSequence sequence2 = ByteSequence.fromString("Testing"); + assertTrue(sequence2.matches("Testing".getBytes())); + + assertTrue(sequence2.matches("Test".getBytes())); + } + + @Test + void startsWith() { + ByteSequence sequence = ByteSequence.startsWith(ByteConstants.GREATERTHAN); + assertEquals("[>]", sequence.toString()); + } + + @Test + void withStartingSequence() { + + } + + @Test + void testStartsWith() { + } + + @Test + void startsWithRange() { + } + + @Test + void startsWithRangeFrom() { + } + + @Test + void startsWithAnyOf() { + } + + @Test + void followedBy() { + } + + @Test + void followedByAnyOf() { + } + + @Test + void followedByRangeFrom() { + } + + @Test + void followedBySequenceOf() { + } + + @Test + void followedByRange() { + } + + @Test + void matches() { + } + + @Test + void testMatches() { + } +} \ No newline at end of file diff --git a/parser/src/test/java/io/xmljim/json/parsertest/util/ResizableByteBufferTest.java b/parser/src/test/java/io/xmljim/json/parsertest/util/ResizableByteBufferTest.java new file mode 100644 index 0000000..2a98954 --- /dev/null +++ b/parser/src/test/java/io/xmljim/json/parsertest/util/ResizableByteBufferTest.java @@ -0,0 +1,38 @@ +package io.xmljim.json.parsertest.util; + +import io.github.xmljim.json.parser.util.ByteConstants; +import io.github.xmljim.json.parser.util.ResizableByteBuffer; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class ResizableByteBufferTest { + + @Test + void testResizableBuffer() { + String testString = "This is a test"; + byte[] testBytes = testString.getBytes(); + ResizableByteBuffer rbf = new ResizableByteBuffer(testBytes); + assertEquals(testBytes.length, rbf.size()); + + byte first = rbf.first(); + assertEquals(ByteConstants.T, first, "Should be 'T'"); + assertEquals(testBytes.length, rbf.size(), "Size and length should match"); + + assertEquals(0, rbf.getPosition(), "Position should be 0"); + assertEquals(testBytes.length, rbf.getRemaining(), "Remaining value should be equal to length"); + + byte getFirst = rbf.get(); + + assertEquals(ByteConstants.T, getFirst, "Firrst byte retrieved should be 'T'"); + assertEquals(1, rbf.getPosition(), "Cursor should have incremented"); + assertEquals(testBytes.length - 1, rbf.getRemaining(), "Get remaining should be decremented"); + + assertEquals(ByteConstants.h, rbf.peek(), "Peek should return 'h'"); + rbf.get(); + assertEquals(ByteConstants.T, rbf.previous(), "Should return 'T'"); + + + } + +} \ No newline at end of file diff --git a/parser/src/test/java/io/xmljim/json/parsertest/util/TimerTest.java b/parser/src/test/java/io/xmljim/json/parsertest/util/TimerTest.java new file mode 100644 index 0000000..83a3061 --- /dev/null +++ b/parser/src/test/java/io/xmljim/json/parsertest/util/TimerTest.java @@ -0,0 +1,73 @@ +package io.xmljim.json.parsertest.util; + +import io.github.xmljim.json.parser.util.Timer; +import org.junit.jupiter.api.*; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class TimerTest { + + private static Timer testTimer; + private static List subjects = List.of("TEST_1", "TEST_2", "TEST_3", "TEST_4"); + + @BeforeAll + static void init() { + testTimer = new Timer<>(); + } + + @AfterAll + static void cleanup() { + testTimer = null; + } + + @Test + @Order(1) + void start() { + subjects.forEach(subject -> { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + String actual = testTimer.start(subject); + assertEquals(subject, actual); + }); + } + + @Test + @Order(2) + void stop() { + subjects.forEach(subject -> { + try { + Thread.sleep(100); + String actual = testTimer.stop(subject); + assertEquals(subject, actual); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }); + } + + @Test + @Order(3) + void getSequences() { + assertTrue(testTimer.getSequences().stream().map(Timer.Tick::getSubject).toList().containsAll(subjects)); + } + + @Test + @Order(4) + void getAccumulatedTime() { + assertTrue(testTimer.getAccumulatedTime() > 0); + } + + @Test + @Order(5) + void get() { + + System.out.println(testTimer); + } +} \ No newline at end of file diff --git a/parser/src/test/java/module-info.java b/parser/src/test/java/module-info.java index 8ddc1ff..007d444 100644 --- a/parser/src/test/java/module-info.java +++ b/parser/src/test/java/module-info.java @@ -4,4 +4,5 @@ requires org.junit.jupiter.api; requires org.junit.jupiter.engine; opens io.xmljim.json.parsertest; + opens io.xmljim.json.parsertest.util; } \ No newline at end of file diff --git a/pom.xml b/pom.xml index 182c22a..10afc05 100644 --- a/pom.xml +++ b/pom.xml @@ -16,6 +16,9 @@ mapper merger jsonpath + json-api + json-test-coverage + json-logging ${project.groupId}:${project.artifactId}