From 500489a9d5e86ac43deaeb4c07ed81698053b07d Mon Sep 17 00:00:00 2001 From: Ken Wenzel Date: Mon, 20 Jul 2026 12:47:27 +0200 Subject: [PATCH] Replace all usages of DOM-based JSON parser with simple pull-based parser. --- .../core/kvin/util/JsonFormatParser.java | 454 ++++++++++-------- .../core/kvin/util/JsonFormatParserTest.java | 201 ++++---- .../linkedfactory/service/KvinService.scala | 63 +-- .../service/mqtt/MqttEventBridge.scala | 55 +-- .../service/util/JsonFormatParser.scala | 183 ------- .../service/KvinServiceTest.scala | 18 +- .../service/test/JsonFormatParserTest.scala | 102 ---- 7 files changed, 417 insertions(+), 659 deletions(-) delete mode 100644 bundles/io.github.linkedfactory.service/src/main/scala/io/github/linkedfactory/service/util/JsonFormatParser.scala delete mode 100644 bundles/io.github.linkedfactory.service/src/test/scala/io/github/linkedfactory/service/test/JsonFormatParserTest.scala diff --git a/bundles/io.github.linkedfactory.core/src/main/java/io/github/linkedfactory/core/kvin/util/JsonFormatParser.java b/bundles/io.github.linkedfactory.core/src/main/java/io/github/linkedfactory/core/kvin/util/JsonFormatParser.java index 10a87d0..4930ff5 100644 --- a/bundles/io.github.linkedfactory.core/src/main/java/io/github/linkedfactory/core/kvin/util/JsonFormatParser.java +++ b/bundles/io.github.linkedfactory.core/src/main/java/io/github/linkedfactory/core/kvin/util/JsonFormatParser.java @@ -10,6 +10,7 @@ import io.github.linkedfactory.core.kvin.Kvin; import io.github.linkedfactory.core.kvin.KvinTuple; import io.github.linkedfactory.core.kvin.Record; +import net.enilink.commons.iterator.IExtendedIterator; import net.enilink.commons.iterator.NiceIterator; import net.enilink.komma.core.URI; import net.enilink.komma.core.URIs; @@ -20,210 +21,261 @@ import java.io.InputStream; import java.math.BigDecimal; import java.math.BigInteger; -import java.util.Iterator; -import java.util.Map; +import java.util.*; import java.util.regex.Pattern; public class JsonFormatParser { - final static Logger logger = LoggerFactory.getLogger(JsonFormatParser.class); + final static Logger logger = LoggerFactory.getLogger(JsonFormatParser.class); final static Pattern HAS_WHITESPACE = Pattern.compile("\\s+"); - final static JsonFactory jsonFactory = new JsonFactory().configure(Feature.AUTO_CLOSE_SOURCE, true); - final static ObjectMapper mapper = new ObjectMapper() - .configure(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN, true); - JsonParser parser; - - public JsonFormatParser(InputStream content) throws IOException { - parser = jsonFactory.createParser(content); - } - - public NiceIterator parse() { - return parse(System.currentTimeMillis()); - } - - public NiceIterator parse(long currentTime) { - return new NiceIterator<>() { - KvinTuple kvinTuple; - URI currentItem; - URI currentProperty; - State state = State.PARSE_ITEMS; - - @Override - public boolean hasNext() { - if (kvinTuple != null) { - return true; - } - try { - JsonToken token = null; - do { - switch (state) { - case PARSE_ITEMS: - while ((token = parser.nextToken()) != null) { - if (token == JsonToken.FIELD_NAME) { - try { - String itemName = parser.currentName(); - if (itemName == null || itemName.isEmpty()) { - throw new IOException("Item name is missing or empty in JSON input."); - } - currentItem = createURI(itemName); - } catch (Exception e) { - throw new IOException("Invalid item URI in JSON input: " + parser.currentName(), e); - } - state = State.PARSE_PROPERTIES; - break; - } else if (token != JsonToken.START_OBJECT && token != JsonToken.END_OBJECT) { - throw new IOException("Expected FIELD_NAME or object delimiters at items level, got: " + token); - } - } - break; - case PARSE_PROPERTIES: - while ((token = parser.nextToken()) != null) { - if (token == JsonToken.FIELD_NAME) { - try { - String propertyName = parser.currentName(); - if (propertyName == null || propertyName.isEmpty()) { - throw new IOException("Property name is missing or empty in JSON input."); - } - currentProperty = createURI(propertyName); - } catch (Exception e) { - throw new IOException("Invalid property URI in JSON input: " + parser.currentName(), e); - } - state = State.PARSE_VALUES; - break; - } else if (token == JsonToken.END_OBJECT) { - state = State.PARSE_ITEMS; - break; - } else if (token != JsonToken.START_ARRAY && token != JsonToken.START_OBJECT) { - throw new IOException("Expected FIELD_NAME or END_OBJECT at properties level, got: " + token); - } - } - break; - case PARSE_VALUES: - boolean foundValue = false; - while ((token = parser.nextToken()) != JsonToken.END_ARRAY && token != null) { - if (token == JsonToken.START_OBJECT) { - JsonNode node = mapper.readTree(parser); - if (node == null || !node.has("value")) { - throw new IOException(String.format("Missing 'value' field for item %s and property %s", currentItem, currentProperty)); - } - Object value = nodeToValue(node.get("value")); - Object seqNr = nodeToValue(node.get("seqNr")); - JsonNode timeNode = node.get("time"); - Number time = timeNode != null ? (Number) nodeToValue(timeNode) : null; - if (value != null) { - kvinTuple = new KvinTuple(currentItem, currentProperty, Kvin.DEFAULT_CONTEXT, - time != null ? time.longValue() : currentTime, - seqNr != null ? ((Number) seqNr).intValue() : 0, value); - foundValue = true; - break; - } else { - throw new IOException(String.format("Invalid null value for item %s and property %s", currentItem, currentProperty)); - } - } else if (token != JsonToken.START_ARRAY) { - throw new IOException(String.format("Unexpected token %s in values array for item %s and property %s: %s", token, currentItem, currentProperty, token)); - } - } - if (token == JsonToken.END_ARRAY) { - state = State.PARSE_PROPERTIES; - } - if (!foundValue && token == null) { - throw new IOException(String.format("Unexpected end of input while parsing values for item %s and property %s", currentItem, currentProperty)); - } - break; - } - } while (kvinTuple == null && token != null); - } catch (Exception e) { - logger.error("Exception while parsing", e); - try { - if (parser != null) { - parser.close(); - parser = null; - } - } catch (IOException ioe) { - // ignore - logger.error("Exception while closing JSON parser", ioe); - } - throw new RuntimeException("Error while parsing JSON input: " + e.getMessage(), e); - } - return kvinTuple != null; - } - - @Override - public KvinTuple next() { - KvinTuple tuple = kvinTuple; - kvinTuple = null; - return tuple; - } - - @Override - public void close() { - try { - if (parser != null) { - parser.close(); - parser = null; - } - } catch (IOException e) { - // ignore - logger.error("Exception while closing JSON parser", e); - } - } - }; - } - - private Object nodeToValue(JsonNode node) { - if (node == null) { - return null; - } - - Record value; - if (node.isObject()) { - JsonNode idNode = node.get("@id"); - if (idNode != null) { - return createURI(node.get("@id").textValue()); - } - - Iterator> records = node.fields(); - value = Record.NULL; - while (records.hasNext()) { - Map.Entry recordNode = records.next(); - value = value.append(new Record(createURI(recordNode.getKey()), nodeToValue(recordNode.getValue()))); - } - return value; - } else if (node.isDouble()) { - return node.asDouble(); - } else if (node.isFloat()) { - return Float.parseFloat(node.asText()); - } else if (node.isInt()) { - return node.asInt(); - } else if (node.isBigInteger()) { - return new BigInteger(node.asText()); - } else if (node.isBigDecimal()) { - return new BigDecimal(node.asText()); - } else if (node.isLong()) { - return node.asLong(); - } else if (node.isShort()) { - return Short.parseShort(node.asText()); - } else if (node.isBoolean()) { - return node.asBoolean(); - } else if (node.isTextual()) { - return node.textValue(); - } else { - return node; - } - } - - static URI createURI(String uriString) { - if (uriString == null || uriString.isEmpty()) { - throw new IllegalArgumentException("URI string is null or empty"); - } - if (HAS_WHITESPACE.matcher(uriString).find()) { - throw new IllegalArgumentException("URI string contains whitespace: '" + uriString + "'"); - } - // Further URI validation can be added here if needed - return URIs.createURI(uriString); - } - - enum State { - PARSE_ITEMS, PARSE_PROPERTIES, PARSE_VALUES - } + final static JsonFactory jsonFactory = new JsonFactory().configure(Feature.AUTO_CLOSE_SOURCE, true); + final static ObjectMapper mapper = new ObjectMapper().configure(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN, true); + JsonParser parser; + + public JsonFormatParser(InputStream content) throws IOException { + parser = jsonFactory.createParser(content); + } + + public IExtendedIterator parse() { + return parse(System.currentTimeMillis()); + } + + public IExtendedIterator parse(long currentTime) { + return parseInternal(currentTime, State.PARSE_ITEMS); + } + + public IExtendedIterator parseValues() { + return parseValues(System.currentTimeMillis()); + } + + public IExtendedIterator parseValues(long currentTime) { + return parseInternal(currentTime, State.PARSE_VALUES); + } + + protected IExtendedIterator parseInternal(long currentTime, State initialState) { + return new NiceIterator<>() { + KvinTuple kvinTuple; + URI currentItem; + URI currentProperty; + State state = initialState; + + final Deque> activeContexts = new ArrayDeque<>(); + + @Override + public boolean hasNext() { + if (kvinTuple != null) { + return true; + } + try { + JsonToken token = null; + do { + switch (state) { + case PARSE_ITEMS: + while ((token = parser.nextToken()) != null) { + if (token == JsonToken.FIELD_NAME) { + String itemName = parser.currentName(); + if ("@context".equals(itemName)) { + JsonToken contextToken = parser.nextToken(); + if (contextToken != JsonToken.START_OBJECT) { + throw new IOException("Expected object value for @context, got: " + contextToken); + } + JsonNode contextNode = mapper.readTree(parser); + if (contextNode != null && contextNode.isObject()) { + activeContexts.addFirst(parseContext(contextNode)); + } + continue; + } + try { + if (itemName == null || itemName.isEmpty()) { + throw new IOException("Item name is missing or empty in JSON input."); + } + currentItem = resolveUri(itemName, activeContexts); + } catch (Exception e) { + throw new IOException("Invalid item URI in JSON input: " + parser.currentName(), e); + } + state = State.PARSE_PROPERTIES; + break; + } else if (token != JsonToken.START_OBJECT && token != JsonToken.END_OBJECT) { + throw new IOException("Expected FIELD_NAME or object delimiters at items level, got: " + token); + } + } + break; + case PARSE_PROPERTIES: + while ((token = parser.nextToken()) != null) { + if (token == JsonToken.FIELD_NAME) { + try { + String propertyName = parser.currentName(); + if (propertyName == null || propertyName.isEmpty()) { + throw new IOException("Property name is missing or empty in JSON input."); + } + currentProperty = resolveUri(propertyName, activeContexts); + } catch (Exception e) { + throw new IOException("Invalid property URI in JSON input: " + parser.currentName(), e); + } + state = State.PARSE_VALUES; + break; + } else if (token == JsonToken.END_OBJECT) { + state = State.PARSE_ITEMS; + break; + } else if (token != JsonToken.START_ARRAY && token != JsonToken.START_OBJECT) { + throw new IOException("Expected FIELD_NAME or END_OBJECT at properties level, got: " + token); + } + } + break; + case PARSE_VALUES: + boolean foundValue = false; + while ((token = parser.nextToken()) != JsonToken.END_ARRAY && token != null) { + if (token == JsonToken.START_OBJECT) { + JsonNode node = mapper.readTree(parser); + if (node == null || !node.has("value")) { + throw new IOException(String.format("Missing 'value' field for item %s and property %s", currentItem, currentProperty)); + } + Object value = nodeToValue(node.get("value"), activeContexts); + Object seqNr = nodeToValue(node.get("seqNr"), activeContexts); + JsonNode timeNode = node.get("time"); + Number time = timeNode != null ? (Number) nodeToValue(timeNode, activeContexts) : null; + if (value != null) { + kvinTuple = new KvinTuple(currentItem, currentProperty, Kvin.DEFAULT_CONTEXT, time != null ? time.longValue() : currentTime, seqNr != null ? ((Number) seqNr).intValue() : 0, value); + foundValue = true; + break; + } else { + throw new IOException(String.format("Invalid null value for item %s and property %s", currentItem, currentProperty)); + } + } else if (token != JsonToken.START_ARRAY) { + throw new IOException(String.format("Unexpected token %s in values array for item %s and property %s: %s", token, currentItem, currentProperty, token)); + } + } + if (token == JsonToken.END_ARRAY) { + state = State.PARSE_PROPERTIES; + } + if (!foundValue && token == null) { + throw new IOException(String.format("Unexpected end of input while parsing values for item %s and property %s", currentItem, currentProperty)); + } + break; + } + } while (kvinTuple == null && token != null); + } catch (Exception e) { + logger.error("Exception while parsing", e); + try { + if (parser != null) { + parser.close(); + parser = null; + } + } catch (IOException ioe) { + // ignore + logger.error("Exception while closing JSON parser", ioe); + } + throw new RuntimeException("Error while parsing JSON input: " + e.getMessage(), e); + } + return kvinTuple != null; + } + + @Override + public KvinTuple next() { + KvinTuple tuple = kvinTuple; + kvinTuple = null; + return tuple; + } + + @Override + public void close() { + try { + if (parser != null) { + parser.close(); + parser = null; + } + } catch (IOException e) { + // ignore + logger.error("Exception while closing JSON parser", e); + } + } + }; + } + + protected Map parseContext(JsonNode contextNode) { + Map context = new HashMap<>(); + for (Map.Entry contextEntry : contextNode.properties()) { + JsonNode valueNode = contextEntry.getValue(); + if (valueNode.isTextual()) { + context.put(contextEntry.getKey(), valueNode.textValue()); + } + } + return context; + } + + protected URI resolveUri(String uriString, Deque> contexts) { + int colonIndex = uriString.indexOf(':'); + if (colonIndex > 0 && uriString.substring(colonIndex + 1).startsWith("//")) { + return createURI(uriString); + } + + String prefix = colonIndex >= 0 ? uriString.substring(0, colonIndex) : uriString; + for (Map context : contexts) { + String prefixValue = context.get(prefix); + if (prefixValue != null) { + String suffix = colonIndex >= 0 ? uriString.substring(colonIndex + 1) : uriString.substring(prefix.length()); + String expandedPrefix = resolveUri(prefixValue, contexts).toString(); + return createURI(expandedPrefix.concat(suffix)); + } + } + + return createURI(uriString); + } + + protected Object nodeToValue(JsonNode node, Deque> activeContexts) { + if (node == null) { + return null; + } + + Record value; + if (node.isObject()) { + JsonNode idNode = node.get("@id"); + if (idNode != null) { + return resolveUri(node.get("@id").textValue(), activeContexts); + } + + Iterator> records = node.properties().iterator(); + value = Record.NULL; + while (records.hasNext()) { + Map.Entry recordNode = records.next(); + value = value.append(new Record(resolveUri(recordNode.getKey(), activeContexts), nodeToValue(recordNode.getValue(), activeContexts))); + } + return value; + } else if (node.isDouble()) { + return node.asDouble(); + } else if (node.isFloat()) { + return Float.parseFloat(node.asText()); + } else if (node.isInt()) { + return node.asInt(); + } else if (node.isBigInteger()) { + return new BigInteger(node.asText()); + } else if (node.isBigDecimal()) { + return new BigDecimal(node.asText()); + } else if (node.isLong()) { + return node.asLong(); + } else if (node.isShort()) { + return Short.parseShort(node.asText()); + } else if (node.isBoolean()) { + return node.asBoolean(); + } else if (node.isTextual()) { + return node.textValue(); + } else { + return node; + } + } + + protected static URI createURI(String uriString) { + if (uriString == null || uriString.isEmpty()) { + throw new IllegalArgumentException("URI string is null or empty"); + } + if (HAS_WHITESPACE.matcher(uriString).find()) { + throw new IllegalArgumentException("URI string contains whitespace: '" + uriString + "'"); + } + // Further URI validation can be added here if needed + return URIs.createURI(uriString); + } + + protected enum State { + PARSE_ITEMS, PARSE_PROPERTIES, PARSE_VALUES + } } \ No newline at end of file diff --git a/bundles/io.github.linkedfactory.core/src/test/java/io/github/linkedfactory/core/kvin/util/JsonFormatParserTest.java b/bundles/io.github.linkedfactory.core/src/test/java/io/github/linkedfactory/core/kvin/util/JsonFormatParserTest.java index d0e7f7d..787a600 100644 --- a/bundles/io.github.linkedfactory.core/src/test/java/io/github/linkedfactory/core/kvin/util/JsonFormatParserTest.java +++ b/bundles/io.github.linkedfactory.core/src/test/java/io/github/linkedfactory/core/kvin/util/JsonFormatParserTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Fraunhofer IWU. + * Copyright (c) 2024 Fraunhofer IWU. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,114 +15,129 @@ */ package io.github.linkedfactory.core.kvin.util; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.github.linkedfactory.core.kvin.Kvin; import io.github.linkedfactory.core.kvin.KvinTuple; import io.github.linkedfactory.core.kvin.Record; -import net.enilink.commons.iterator.IExtendedIterator; - +import net.enilink.komma.core.URIs; import org.junit.Test; import java.io.ByteArrayInputStream; import java.io.IOException; +import java.io.InputStream; import java.math.BigInteger; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; public class JsonFormatParserTest { + private final ObjectMapper mapper = new ObjectMapper(); + + @Test + public void shouldParseJsonResource() throws Exception { + List tuples = parse(readResource("/JsonFormatParserTestContent.json")); - @Test - public void shouldParseJson() throws IOException { - JsonFormatParser jsonParser = new JsonFormatParser( - getClass().getClassLoader().getResourceAsStream("JsonFormatParserTestContent.json")); - IExtendedIterator tuples = jsonParser.parse(); - assertNotNull(tuples); - int index = 0; - while (tuples.hasNext()) { - KvinTuple t = tuples.next(); - if (index == 2) { - assertTrue(t.value instanceof Integer); - } else if (index == 3) { - assertTrue(t.value instanceof BigInteger); - } else if (index == 4) { - assertTrue(t.value instanceof Double); - } else if (index == 5) { - assertTrue(t.value instanceof Long); - } else if (index == 6) { - assertTrue(t.value instanceof Boolean); - } else if (index == 7 || index == 10) { - assertTrue(t.value instanceof Record); - } - index++; - } - assertEquals(11, index); - } - - @Test - public void shouldThrowOnMalformedJson() { - String malformedJson = "{ \"item1\": { \"prop1\": [ { \"value\": 123 } ] "; // missing closing braces - try { - JsonFormatParser parser = new JsonFormatParser( - new ByteArrayInputStream(malformedJson.getBytes(StandardCharsets.UTF_8))); - var it = parser.parse(); - while (it.hasNext()) { - it.next(); - } - fail("Expected RuntimeException due to malformed JSON"); - } catch (Exception e) { - assertTrue(e.getCause() instanceof IOException); - } - } - - @Test - public void shouldThrowOnMissingValueField() { - String missingValueJson = "{ \"item1\": { \"prop1\": [ { \"seqNr\": 1 } ] } }"; - try { - JsonFormatParser parser = new JsonFormatParser( - new ByteArrayInputStream(missingValueJson.getBytes(StandardCharsets.UTF_8))); - parser.parse().hasNext(); - fail("Expected RuntimeException due to missing 'value' field"); - } catch (Exception e) { - assertTrue(e.getCause() instanceof IOException); - } - } - - @Test - public void shouldThrowOnEmptyItemName() { - String emptyItemNameJson = "{ \"\": { \"prop1\": [ { \"value\": 1 } ] } }"; - try { - JsonFormatParser parser = new JsonFormatParser( - new ByteArrayInputStream(emptyItemNameJson.getBytes(StandardCharsets.UTF_8))); - parser.parse().hasNext(); - fail("Expected RuntimeException due to empty item name"); - } catch (Exception e) { - assertTrue(e.getCause() instanceof IOException); - } - } - - @Test - public void shouldThrowOnEmptyPropertyName() { - String emptyPropertyNameJson = "{ \"item1\": { \"\": [ { \"value\": 1 } ] } }"; - try { - JsonFormatParser parser = new JsonFormatParser( - new ByteArrayInputStream(emptyPropertyNameJson.getBytes(StandardCharsets.UTF_8))); - parser.parse().hasNext(); - fail("Expected RuntimeException due to empty property name"); - } catch (Exception e) { - assertTrue(e.getCause() instanceof IOException); - } - } + assertEquals(11, tuples.size()); + assertTrue(tuples.get(2).value instanceof java.lang.Integer); + assertTrue(tuples.get(3).value instanceof java.math.BigInteger); + assertTrue(tuples.get(4).value instanceof java.lang.Double); + assertTrue(tuples.get(5).value instanceof java.lang.Long); + assertTrue(tuples.get(6).value instanceof java.lang.Boolean); + assertTrue(tuples.get(7).value instanceof Record); + assertTrue(tuples.get(10).value instanceof Record); + } @Test - public void testUriWithSpaces() { - String jsonWithSpacesInUri = "{ \"http://example.com/item 1\": { \"http://example.com/prop 1\": [ { \"value\": 1 } ] } }"; + public void shouldResolvePrefixesFromContext() throws Exception { + String json = """ + { + "@context": {"pref": "http://test1.example/", "pref2": "http://pref2.example/"}, + "@context": {"pref": "http://test2.example/"}, + "pref": { + "pref:rest": [{"value": "val"}], + "pref2:rest": [{"value": "val2"}] + } + }"""; + + List tuples = parse(json, 1619424246100L); + + assertEquals(2, tuples.size()); + assertEquals("http://test2.example/", tuples.getFirst().item.toString()); + assertEquals("http://test2.example/rest", tuples.get(0).property.toString()); + assertEquals("val", tuples.get(0).value); + assertEquals("http://test2.example/", tuples.get(1).item.toString()); + assertEquals("http://pref2.example/rest", tuples.get(1).property.toString()); + assertEquals("val2", tuples.get(1).value); + } + + @Test + public void shouldParseNestedRecords() throws Exception { + ObjectNode root = mapper.createObjectNode(); + ObjectNode item = root.putObject("http://example.root/item"); + ArrayNode values = item.putArray("http://example.root/nested"); + ObjectNode value = values.addObject().putObject("value"); + value.put("msg", "Error 1"); + value.put("nr", 1); + value.putObject("test_prop").put("msg", "test"); + value.putObject("id_prop").put("@id", "http://example.org/properties/test3"); + + List tuples = parse(root, 1619424246100L); + Record expectedValue = new Record(URIs.createURI("msg"), "Error 1").append(new Record(URIs.createURI("nr"), 1).append(new Record(URIs.createURI("test_prop"), new Record(URIs.createURI("msg"), "test")).append(new Record(URIs.createURI("id_prop"), URIs.createURI("http://example.org/properties/test3"))))); + List expected = List.of(new KvinTuple(URIs.createURI("http://example.root/item"), URIs.createURI("http://example.root/nested"), Kvin.DEFAULT_CONTEXT, 1619424246100L, 0, expectedValue)); + + assertEquals(expected, tuples); + } + + @Test + public void shouldRejectMissingValueField() throws Exception { + String json = "{ \"item1\": { \"prop1\": [ { \"seqNr\": 1 } ] } }"; + try { - JsonFormatParser parser = new JsonFormatParser( - new ByteArrayInputStream(jsonWithSpacesInUri.getBytes(StandardCharsets.UTF_8))); - parser.parse().hasNext(); - fail("Expected RuntimeException due to spaces in URI"); - } catch (Exception e) { + parse(json); + fail("Expected RuntimeException due to missing 'value' field"); + } catch (RuntimeException e) { assertTrue(e.getCause() instanceof IOException); } } -} + + private List parse(JsonNode node) throws Exception { + return parse(node, System.currentTimeMillis()); + } + + private List parse(JsonNode node, long currentTime) throws Exception { + return parseBytes(mapper.writeValueAsBytes(node), currentTime); + } + + private List parse(String json) throws Exception { + return parse(json, System.currentTimeMillis()); + } + + private List parse(String json, long currentTime) throws Exception { + return parseBytes(json.getBytes(StandardCharsets.UTF_8), currentTime); + } + + private List parseBytes(byte[] bytes, long currentTime) throws Exception { + JsonFormatParser parser = new JsonFormatParser(new ByteArrayInputStream(bytes)); + try (var tuples = parser.parse(currentTime)) { + return tuples.toList(); + } + } + + private JsonNode readResource(String path) throws Exception { + InputStream stream = getClass().getResourceAsStream(path); + try (stream) { + assertNotNull(stream); + return mapper.readTree(stream); + } + } +} \ No newline at end of file diff --git a/bundles/io.github.linkedfactory.service/src/main/scala/io/github/linkedfactory/service/KvinService.scala b/bundles/io.github.linkedfactory.service/src/main/scala/io/github/linkedfactory/service/KvinService.scala index 16e83d2..a0b81e5 100644 --- a/bundles/io.github.linkedfactory.service/src/main/scala/io/github/linkedfactory/service/KvinService.scala +++ b/bundles/io.github.linkedfactory.service/src/main/scala/io/github/linkedfactory/service/KvinService.scala @@ -15,27 +15,27 @@ */ package io.github.linkedfactory.service -import io.github.linkedfactory.core.kvin.util.{AsyncExtendedIterator, CsvFormatParser, JsonFormatWriter} +import io.github.linkedfactory.core.kvin.util.{AsyncExtendedIterator, CsvFormatParser, JsonFormatParser, JsonFormatWriter} import io.github.linkedfactory.core.kvin.{Kvin, KvinTuple, Record} import io.github.linkedfactory.core.rdf4j.FederatedServiceComponent -import io.github.linkedfactory.service.util.{JsonFormatParser, LineProtocolParser} +import io.github.linkedfactory.service.util.LineProtocolParser import net.enilink.commons.iterator.{IExtendedIterator, NiceIterator} import net.enilink.komma.core.{URI, URIs} import net.liftweb.common.Box.box2Iterable -import net.liftweb.common._ +import net.liftweb.common.* import net.liftweb.http.rest.RestHelper import net.liftweb.http.{InMemoryResponse, JsonResponse, LiftResponse, OkResponse, OutputStreamResponse, Req, S} -import org.json4s._ +import org.json4s.* import org.json4s.native.JsonMethods.{compact, render as renderJson} -import org.json4s.JsonDSL._ -import net.liftweb.util.Helpers._ +import org.json4s.JsonDSL.* +import net.liftweb.util.Helpers.* import org.apache.commons.csv.{CSVFormat, CSVPrinter} import java.io.{InputStream, OutputStream, OutputStreamWriter} import java.text.SimpleDateFormat import java.util import java.util.Date -import scala.jdk.CollectionConverters._ +import scala.jdk.CollectionConverters.* class KvinService(path: List[String], store: Kvin) extends RestHelper with Loggable { val MAX_LIMIT = 500000 @@ -45,13 +45,13 @@ class KvinService(path: List[String], store: Kvin) extends RestHelper with Logga ("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS") :: // ("Access-Control-Allow-Headers", "*") :: Nil - def responseHeaders: List[(String, String)] = CORS_HEADERS ::: S.getResponseHeaders(Nil) + protected def responseHeaders: List[(String, String)] = CORS_HEADERS ::: S.getResponseHeaders(Nil) - object FailureResponse { + protected object FailureResponse { def apply(msg: String): LiftResponse = createErrorResponse(400, "INVALID_PAYLOAD", msg) } - def createErrorResponse(status: Int, code: String, message: String, details: Box[String] = Empty): LiftResponse = { + protected def createErrorResponse(status: Int, code: String, message: String, details: Box[String] = Empty): LiftResponse = { val body = details.filter(_.nonEmpty).map { d => ("code" -> code) ~ ("message" -> message) ~ ("details" -> d) } openOr { @@ -60,7 +60,7 @@ class KvinService(path: List[String], store: Kvin) extends RestHelper with Logga JsonResponse(body, responseHeaders, S.responseCookies, status) } - def createSuccessResponse(code: String = "OK", message: Box[String] = Empty, data: JObject = JObject(Nil)): LiftResponse = { + protected def createSuccessResponse(code: String = "OK", message: Box[String] = Empty, data: JObject = JObject(Nil)): LiftResponse = { val baseFields = List( JField("success", JBool(true)), JField("status", JInt(200)), @@ -93,7 +93,7 @@ class KvinService(path: List[String], store: Kvin) extends RestHelper with Logga } } - def createJsonResponse(json: JValue): LiftResponse = JsonResponse(json, responseHeaders, S.responseCookies, 200) + protected def createJsonResponse(json: JValue): LiftResponse = JsonResponse(json, responseHeaders, S.responseCookies, 200) serve(path prefix { // support OPTIONS requests @@ -110,8 +110,7 @@ class KvinService(path: List[String], store: Kvin) extends RestHelper with Logga case Full("text/csv") => req.rawInputStream.flatMap(saveCsvValues(_, path ++ list.dropRight(1), System.currentTimeMillis)) case _ => - req.json.flatMap(saveValues(_, path ++ list.dropRight(1), System.currentTimeMillis)) - // req.rawInputStream.flatMap(saveValues(_, path ++ list.dropRight(1), System.currentTimeMillis)) + req.rawInputStream.flatMap(saveJsonValues(_, path ++ list.dropRight(1), System.currentTimeMillis)) } result match { case Failure(msg, _, _) => FailureResponse(msg) @@ -133,7 +132,7 @@ class KvinService(path: List[String], store: Kvin) extends RestHelper with Logga // case list Get _ => // TODO return RDF description }) - def serveValues(path: List[String], contentType: Box[String]): LiftResponse = { + protected def serveValues(path: List[String], contentType: Box[String]): LiftResponse = { val limit = S.param("limit") flatMap (v => tryo(v.toLong)) filter (_ > 0) openOr 10000L if (limit > MAX_LIMIT) { @@ -188,7 +187,7 @@ class KvinService(path: List[String], store: Kvin) extends RestHelper with Logga try { values.forEach(writer.writeTuple(_)) } catch { - case e : Exception => logger.error("Error while writing JSON data", e) + case e: Exception => logger.error("Error while writing JSON data", e) } finally { try { values.close() @@ -266,22 +265,12 @@ class KvinService(path: List[String], store: Kvin) extends RestHelper with Logga } // handle JSON post content - def saveValues(json: JValue, path: List[String], currentTime: Long): Box[?] = { - var parentUri = Data.pathToURI(path) - if (parentUri.lastSegment != "") parentUri = parentUri.appendSegment("") - - JsonFormatParser.parseItem(parentUri, contextModelUri, json, currentTime) map (_.foreach { tuple => - store.put(tuple) - }) - } - - // handle JSON post content - def saveValues(in: InputStream, path: List[String], currentTime: Long): Box[?] = { + protected def saveJsonValues(in: InputStream, path: List[String], currentTime: Long): Box[?] = { var parentUri = Data.pathToURI(path) if (parentUri.lastSegment != "") parentUri = parentUri.appendSegment("") try { - val tuples: IExtendedIterator[KvinTuple] = new io.github.linkedfactory.core.kvin.util.JsonFormatParser(in).parse(currentTime) + val tuples: IExtendedIterator[KvinTuple] = new JsonFormatParser(in).parse(currentTime) store.put(tuples) Empty } catch { @@ -290,7 +279,7 @@ class KvinService(path: List[String], store: Kvin) extends RestHelper with Logga } // handle CSV post content - def saveCsvValues(in: InputStream, path: List[String], currentTime: Long): Box[?] = { + protected def saveCsvValues(in: InputStream, path: List[String], currentTime: Long): Box[?] = { var parentUri = Data.pathToURI(path) if (parentUri.lastSegment != "") parentUri = parentUri.appendSegment("") @@ -307,7 +296,7 @@ class KvinService(path: List[String], store: Kvin) extends RestHelper with Logga } // handle InfluxDB line protocol content - def saveLineValues(is: InputStream, path: List[String], currentTime: Long): Box[?] = { + protected def saveLineValues(is: InputStream, path: List[String], currentTime: Long): Box[?] = { var parentUri = Data.pathToURI(path) if (parentUri.lastSegment != "") parentUri = parentUri.appendSegment("") @@ -316,9 +305,9 @@ class KvinService(path: List[String], store: Kvin) extends RestHelper with Logga }) } - def getSingleItem(path: List[String]): URI = S.param("item") flatMap { s => tryo(URIs.createURI(s)) } openOr Data.pathToURI(path) + protected def getSingleItem(path: List[String]): URI = S.param("item") flatMap { s => tryo(URIs.createURI(s)) } openOr Data.pathToURI(path) - def getValues(path: List[String], limit: Long): IExtendedIterator[KvinTuple] = { + protected def getValues(path: List[String], limit: Long): IExtendedIterator[KvinTuple] = { val items = (S.param("item") or S.param("items")).map { _.split("\\s+").flatMap { i => tryo(URIs.createURI(i)) }.toList } openOr List(Data.pathToURI(path)) @@ -354,7 +343,7 @@ class KvinService(path: List[String], store: Kvin) extends RestHelper with Logga } } - def getValuesMap(path: List[String], limit: Long): Map[String, Map[String, IExtendedIterator[KvinTuple]]] = { + protected def getValuesMap(path: List[String], limit: Long): Map[String, Map[String, IExtendedIterator[KvinTuple]]] = { val items = (S.param("item") or S.param("items")).map { _.split("\\s+").flatMap { i => tryo(URIs.createURI(i)) }.toList } openOr List(Data.pathToURI(path)) @@ -385,7 +374,7 @@ class KvinService(path: List[String], store: Kvin) extends RestHelper with Logga }.toMap } - def deleteValues(path: List[String]): JObject = { + protected def deleteValues(path: List[String]): JObject = { val items = (S.param("item") or S.param("items")).map { _.split("\\s+").flatMap { i => tryo(URIs.createURI(i)) }.toList } openOr List(Data.pathToURI(path)) @@ -409,7 +398,7 @@ class KvinService(path: List[String], store: Kvin) extends RestHelper with Logga JObject(JField("deleted", deletedRows) :: Nil) } - def getDescendants(path: List[String]): JArray = { + protected def getDescendants(path: List[String]): JArray = { val uri = path match { // retrieve all items if path is the root path case p if p == this.path && S.param("item").isEmpty => URIs.createURI("") @@ -425,7 +414,7 @@ class KvinService(path: List[String], store: Kvin) extends RestHelper with Logga JArray(descendants.toList) } - def getProperties(path: List[String]): JArray = { + protected def getProperties(path: List[String]): JArray = { val uri = getSingleItem(path) val properties = store.properties(uri, contextModelUri).iterator.asScala.map { uri => JObject(JField("@id", uri.toString) :: Nil) @@ -433,5 +422,5 @@ class KvinService(path: List[String], store: Kvin) extends RestHelper with Logga JArray(properties.toList) } - def contextModelUri: URI = Data.currentModel.map(_.getURI).openOr(Kvin.DEFAULT_CONTEXT) + protected def contextModelUri: URI = Data.currentModel.map(_.getURI).openOr(Kvin.DEFAULT_CONTEXT) } diff --git a/bundles/io.github.linkedfactory.service/src/main/scala/io/github/linkedfactory/service/mqtt/MqttEventBridge.scala b/bundles/io.github.linkedfactory.service/src/main/scala/io/github/linkedfactory/service/mqtt/MqttEventBridge.scala index 4b342f1..c0d36e2 100644 --- a/bundles/io.github.linkedfactory.service/src/main/scala/io/github/linkedfactory/service/mqtt/MqttEventBridge.scala +++ b/bundles/io.github.linkedfactory.service/src/main/scala/io/github/linkedfactory/service/mqtt/MqttEventBridge.scala @@ -16,28 +16,26 @@ package io.github.linkedfactory.service.mqtt import com.google.common.cache.{Cache, CacheBuilder} -import io.github.linkedfactory.core.kvin.Kvin +import io.github.linkedfactory.core.kvin.util.JsonFormatParser import io.github.linkedfactory.service.ItemDataEvents -import io.github.linkedfactory.service.util.JsonFormatParser import net.enilink.komma.core.{IReference, URIs} import net.enilink.komma.em.concepts.IResource import net.enilink.platform.core.PluginConfigModel -import net.liftweb.common.Full -import org.json4s._ -import org.json4s.native.JsonParser -import org.json4s.native.JsonMethods.{compact, render as renderJson} -import org.json4s.JsonDSL._ import org.eclipse.paho.client.mqttv3.* import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence +import org.json4s.* +import org.json4s.JsonDSL.* +import org.json4s.native.JsonMethods.{compact, render as renderJson} import org.osgi.framework.{FrameworkUtil, ServiceRegistration} import org.osgi.service.component.annotations.{Component, Reference} import org.osgi.service.event.{Event, EventAdmin, EventConstants, EventHandler} +import java.io.ByteArrayInputStream import java.util import java.util.concurrent.TimeUnit import java.util.{HashMap, Hashtable, UUID} -import scala.util.matching.Regex import scala.compiletime.uninitialized +import scala.util.matching.Regex /** * A simple bridge between HTTP and MQTT interfaces for linked factory events. @@ -46,18 +44,18 @@ import scala.compiletime.uninitialized class MqttEventBridge { implicit val formats: DefaultFormats.type = DefaultFormats - val ownAuthorities: Cache[String, Boolean] = CacheBuilder.newBuilder.expireAfterWrite(30, TimeUnit.SECONDS).build.asInstanceOf[Cache[String, Boolean]] + private val ownAuthorities: Cache[String, Boolean] = CacheBuilder.newBuilder.expireAfterWrite(30, TimeUnit.SECONDS).build.asInstanceOf[Cache[String, Boolean]] - val ITEM: Regex = "^LF/[^/]+/([^/]+)/(.*)".r + private val ITEM: Regex = "^LF/[^/]+/([^/]+)/(.*)".r var client: MqttClient = uninitialized - var eventHandlerSvc: ServiceRegistration[?] = uninitialized + private var eventHandlerSvc: ServiceRegistration[EventHandler] = uninitialized var config: PluginConfigModel = uninitialized - var eventAdmin: EventAdmin = uninitialized + private var eventAdmin: EventAdmin = uninitialized - var shuttingDown = false + private var shuttingDown = false def activate(): Unit = { val (broker, filter) = { @@ -91,24 +89,15 @@ class MqttEventBridge { topic match { // ignores own messages case ITEM(authority, path) if !ownAuthorities.getIfPresent(authority) => - JsonParser.parseOpt(new String(msg.getPayload)) map { - json => - val topic = "linkedfactory/itemEvent/external" - val item = "http://" + authority + "/" + path - - JsonFormatParser.parseItem(URIs.createURI(item), Kvin.DEFAULT_CONTEXT, json) match { - case Full(values) => values.map { tuple => - val properties = new util.HashMap[String, Any] - properties.put(ItemDataEvents.ITEM, item) - properties.put(ItemDataEvents.PROPERTY, tuple.property.toString) - properties.put(ItemDataEvents.TIME, tuple.time) - properties.put(ItemDataEvents.VALUE, tuple.value) - - eventAdmin.postEvent(new Event(topic, properties)) - } - case _ => // handle failure - } - } + new JsonFormatParser(new ByteArrayInputStream(msg.getPayload)).parseValues().forEach(tuple => { + val properties = new util.HashMap[String, Any] + properties.put(ItemDataEvents.ITEM, "http://" + authority + "/" + path) + properties.put(ItemDataEvents.PROPERTY, tuple.property.toString) + properties.put(ItemDataEvents.TIME, tuple.time) + properties.put(ItemDataEvents.VALUE, tuple.value) + + eventAdmin.postEvent(new Event("linkedfactory/itemEvent/external", properties)) + }) case _ => // ignore those events } } @@ -150,7 +139,7 @@ class MqttEventBridge { } } - def connect(): Unit = { + private def connect(): Unit = { client.synchronized { if (!client.isConnected) { val options = new MqttConnectOptions @@ -164,7 +153,7 @@ class MqttEventBridge { /** * Publishes item events over MQTT */ - def publish(topic: String, property: String, time: Long, value: Any): Unit = { + private def publish(topic: String, property: String, time: Long, value: Any): Unit = { val qos = 2; // FIXME: avoid deadlock on shutdown diff --git a/bundles/io.github.linkedfactory.service/src/main/scala/io/github/linkedfactory/service/util/JsonFormatParser.scala b/bundles/io.github.linkedfactory.service/src/main/scala/io/github/linkedfactory/service/util/JsonFormatParser.scala deleted file mode 100644 index d9fd0db..0000000 --- a/bundles/io.github.linkedfactory.service/src/main/scala/io/github/linkedfactory/service/util/JsonFormatParser.scala +++ /dev/null @@ -1,183 +0,0 @@ -/* - * Copyright (c) 2022 Fraunhofer IWU. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.github.linkedfactory.service.util - -import io.github.linkedfactory.core.kvin.{KvinTuple, Record} -import net.enilink.komma.core.{URI, URIs} -import net.liftweb.common.* -import net.liftweb.common.Box.box2Iterable -import org.json4s.* -import org.json4s.JsonAST.* - -import javax.xml.datatype.DatatypeFactory - -/** - * Parses JSON objects with linked factory item data. - */ -object JsonFormatParser extends Loggable { - val dtFactoryLocal = new ThreadLocal[DatatypeFactory] - - def datatypeFactory: DatatypeFactory = { - var factory = dtFactoryLocal.get - if (factory == null) { - factory = DatatypeFactory.newInstance - dtFactoryLocal.set(factory) - } - factory - } - - def parseItem(rootItem: URI, context: URI, json: JValue, currentTime: Long = System.currentTimeMillis): Box[List[KvinTuple]] = { - var activeContexts = List[JValue]() - - def collectErrors(a: Box[List[KvinTuple]], b: Box[List[KvinTuple]]): Box[List[KvinTuple]] = { - (a, b) match { - // accumulate errors - case (a: Failure, b: Failure) => Failure(a.msg + "\n" + b.msg) - case (a: Failure, b) => a - case (a, b: Failure) => b - // this is the cause for foldRight, foldLeft would always required to traverse - // all previously folded values when using the ++ operator - case (a, b) => Full(a.openOr(Nil) ++ b.openOr(Nil)) - } - } - - def objectToRecord(o: JObject): Record = o.obj.foldLeft(Record.NULL) { case (e, field) => - val property = resolveUri(field._1, activeContexts) - parseValue(field._2) match { - case Full(value) => e.append(new Record(property, value)) - case _ => e - } - } - - def parseValue(value: JValue): Box[Any] = value match { - case null | JNothing => - Failure("Invalid value") - case JArray(values) => - Full(values.flatMap(parseValue).toArray) - case obj: JObject => - obj \ "@id" match { - case JString(id) => Full(resolveUri(id, activeContexts)) - case _ => Full(objectToRecord(obj)) - } - case value => - val unboxed = value.values - Full(unboxed) - } - - def parseProperty(item: URI, property: URI, values: List[JValue], currentTime: Long): Box[List[KvinTuple]] = { - var generatedSeqNr = -1 - val result = values.map { - // { "time" : 123, "seqNr" : 2, "value" : 1.3 } - case o @ JObject(_) => - var seqNr = o \ "seqNr" match { - case JInt(n) => n.intValue - case _ => 0 - } - - val time = (o \ "time").toOption.getOrElse(o \ "t") match { - case JString(s) => datatypeFactory.newXMLGregorianCalendar(s).toGregorianCalendar.getTimeInMillis - case JInt(n) => n.longValue - case _ => - if (seqNr == 0) { - // generate sequence numbers for multiple values if neither time nor sequence numbers are specified - generatedSeqNr += 1 - seqNr = generatedSeqNr - } - currentTime - } - - parseValue((o \ "value").toOption.getOrElse(o \ "v")) match { - case Full(value) => - Full(new KvinTuple(item, property, context, time, seqNr, value)) - case _ => - Failure("Invalid value for item \"" + item + "\" and property \"" + property + "\".") - } - case other => parseValue(other) match { - case Full(value) => - Full(new KvinTuple(item, property, context, currentTime, value)) - case _ => - Failure("Invalid value for item \"" + item + "\" and property \"" + property + "\".") - } - } - - result.foldRight(Empty: Box[List[KvinTuple]]) { - // accumulate errors - case (a: Failure, b: Failure) => Failure(a.msg + "\n" + b.msg) - case (a: Failure, _) => a - case (_, b: Failure) => b - // this is the cause for foldRight, foldLeft would always required to traverse - // all previously folded values when using the ++ operator - case (a, b) => Full(a.toList ++ b.openOr(Nil)) - } - } - - def resolveUri(uri: String, contexts: List[JValue]): URI = { - uri.split(":") match { - // is a URI with scheme - case Array(_, suf, _*) if suf.startsWith("//") => URIs.createURI(uri) - // may be a CURIE - case Array(pref, _*) => contexts match { - case first :: rest => - first \ pref match { - case JString(s) => - val sufPref = resolveUri(s, contexts).toString - if (pref.length < uri.length) - URIs.createURI(sufPref.concat(uri.substring(pref.length + 1))) - else - URIs.createURI(sufPref.concat(uri.substring(pref.length))) - case _ => resolveUri(uri, rest) - } - // no prefix defined, just use item as URI - case Nil => { - val result = URIs.createURI(uri) - if (result.isRelative) result.resolve(rootItem) else result - } - } - } - } - - json match { - // [ { "time" : 123, "seqNr" : 2, "value" : 1.3 } ] - case JArray(values) => - parseProperty(rootItem, URIs.createURI("value"), values, currentTime) - - // { "item" : { "property1" : [ { "time" : 123, "seqNr" : 2, "value" : 1.3 } ], "property2" : [ { "time" : 123, "seqNr" : 5, "value" : 3.2 } ] } } - case JObject(fields) => fields.flatMap { - case JField(item, itemData) if item.equals("@context") => activeContexts = itemData :: activeContexts; None - // "item" : { ... } - case JField(item, itemData) => - // resolve relative URIs - var itemUri = resolveUri(item, activeContexts) - if (itemUri.lastSegment == "") itemUri = itemUri.trimSegments(1) - itemData match { - // "property1" : [{ ... }] - case JObject(props) => - props.map { - case JField(prop, propData) => - // support single and multiple values - val values = propData match { - case JArray(values) => values - case other => List(other) - } - parseProperty(itemUri, resolveUri(prop, activeContexts), values, currentTime) - } - case _ => Failure("Invalid data: Expected an object with property keys.") - } - }.foldRight(Empty: Box[List[KvinTuple]])(collectErrors) - case _ => Failure("Invalid data") - } - } -} \ No newline at end of file diff --git a/bundles/io.github.linkedfactory.service/src/test/scala/io/github/linkedfactory/service/KvinServiceTest.scala b/bundles/io.github.linkedfactory.service/src/test/scala/io/github/linkedfactory/service/KvinServiceTest.scala index 0af0864..de8838f 100644 --- a/bundles/io.github.linkedfactory.service/src/test/scala/io/github/linkedfactory/service/KvinServiceTest.scala +++ b/bundles/io.github.linkedfactory.service/src/test/scala/io/github/linkedfactory/service/KvinServiceTest.scala @@ -53,14 +53,14 @@ object KvinServiceTest { modelSet.dispose() modelSet = null - store.close + store.close() store = null deleteDirectory(storeDirectory.toPath) } def createStore(): Unit = { storeDirectory = new File("/tmp/leveldb-test-" + System.currentTimeMillis + "-" + Random.nextInt(1000) + "/") - storeDirectory.deleteOnExit + storeDirectory.deleteOnExit() store = new KvinLevelDb(storeDirectory) } @@ -89,7 +89,7 @@ object TestData { * Unit tests for the KVIN service endpoint */ class KvinServiceTest { - val kvinService = new KvinService("linkedfactory" :: Nil, KvinServiceTest.store) { + private val kvinService: KvinService = new KvinService("linkedfactory" :: Nil, KvinServiceTest.store) { override def apply(in: Req): () => Box[LiftResponse] = { try { Globals.contextModelSet.vend.map(_.getUnitOfWork.begin) @@ -258,19 +258,18 @@ class KvinServiceTest { val response = kvinRest(toReq(getReq))().map(_.toResponse).openOr(null) val stringResponse: String = responseBody(response) - val kvinTuples: NiceIterator[KvinTuple] = new JsonFormatParser(new ByteArrayInputStream(stringResponse.getBytes())).parse() + val kvinTuples = new JsonFormatParser(new ByteArrayInputStream(stringResponse.getBytes())).parse() while (kvinTuples.hasNext) { val tuple: KvinTuple = kvinTuples.next() assertEquals(tuple.item.toString, "http://example.org/item1") assertEquals(tuple.property.toString, "http://example.org/properties/p1") - assertEquals(tuple.time, 1619424246120l) + assertEquals(tuple.time, 1619424246120L) assertEquals(tuple.value.toString, "57.934878949512196") } } @Test def queryDataWithLimitTest(): Unit = { - val postReq = new MockHttpServletRequest(baseUrl) { method = "POST" body_=(TestData.itemSet, "application/json") @@ -286,7 +285,7 @@ class KvinServiceTest { val response = kvinRest(toReq(getReq))().map(_.toResponse).openOr(null) val stringResponse: String = responseBody(response) - val kvinTuples: NiceIterator[KvinTuple] = new JsonFormatParser(new ByteArrayInputStream(stringResponse.getBytes())).parse() + val kvinTuples = new JsonFormatParser(new ByteArrayInputStream(stringResponse.getBytes())).parse() assertEquals(kvinTuples.toList.size(), 2) } @@ -308,7 +307,7 @@ class KvinServiceTest { val response = kvinRest(toReq(getReq))().map(_.toResponse).openOr(null) val stringResponse: String = responseBody(response) - val kvinTuples: NiceIterator[KvinTuple] = new JsonFormatParser(new ByteArrayInputStream(stringResponse.getBytes())).parse() + val kvinTuples = new JsonFormatParser(new ByteArrayInputStream(stringResponse.getBytes())).parse() var count = 0 while (kvinTuples.hasNext) { val tuple: KvinTuple = kvinTuples.next() @@ -324,8 +323,7 @@ class KvinServiceTest { } @Test - def getPropertiesTest(): Unit = { - + def retrievePropertiesTest(): Unit = { val postReq = new MockHttpServletRequest(baseUrl) { method = "POST" body_=(TestData.item1, "application/json") diff --git a/bundles/io.github.linkedfactory.service/src/test/scala/io/github/linkedfactory/service/test/JsonFormatParserTest.scala b/bundles/io.github.linkedfactory.service/src/test/scala/io/github/linkedfactory/service/test/JsonFormatParserTest.scala deleted file mode 100644 index e747ce7..0000000 --- a/bundles/io.github.linkedfactory.service/src/test/scala/io/github/linkedfactory/service/test/JsonFormatParserTest.scala +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright (c) 2022 Fraunhofer IWU. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.github.linkedfactory.service.test - -import io.github.linkedfactory.core.kvin.{Kvin, KvinTuple, Record} -import io.github.linkedfactory.service.util.JsonFormatParser -import net.enilink.komma.core.URIs -import net.liftweb.common.Full -import org.json4s._ -import org.json4s.native.JsonMethods._ -import org.json4s.JsonDSL._ -import org.json4s.JsonAST._ -import org.junit.{Assert, Test} - - -class JsonFormatParserTest { - - @Test - def test(): Unit = { - val context = Kvin.DEFAULT_CONTEXT - val time = System.currentTimeMillis - val root = URIs.createURI("http://example.root") - val simpleJson: JValue = - ("@context" -> ("pref" -> "http://test1.example/")) ~ - ("pref" -> ("pref:rest" -> "val") ~ ("pref2:pref3" -> "val2")) - var tuple = JsonFormatParser.parseItem(root, context, simpleJson, time).head.head - Assert.assertEquals("http://test1.example/", tuple.item.toString) - Assert.assertEquals("http://test1.example/rest", tuple.property.toString) - Assert.assertEquals("val", tuple.value) - tuple = JsonFormatParser.parseItem(root, context, simpleJson, time).head.tail.head - Assert.assertEquals("http://test1.example/", tuple.item.toString) - Assert.assertEquals("pref2:pref3", tuple.property.toString) - Assert.assertEquals("val2", tuple.value) - - val withoutContext = ("pref" -> ("pref:rest" -> "val") ~ ("pref2:pref3" -> "val2")) - tuple = JsonFormatParser.parseItem(root, context, withoutContext, time).head.head - Assert.assertEquals("http://example.root/pref", tuple.item.toString) - Assert.assertEquals("pref:rest", tuple.property.toString) - Assert.assertEquals("val", tuple.value) - - val withMultiContext = ("@context" -> ("pref" -> "http://test1.example/") ~ ("pref2" -> "http://pref2.example/")) ~ - ("@context" -> ("pref" -> "http://test2.example/")) ~ - ("pref" -> ("pref:rest" -> "val") ~ ("pref2:rest" -> "val2")) - tuple = JsonFormatParser.parseItem(root, context, withMultiContext, time).head.head - Assert.assertEquals("http://test2.example/", tuple.item.toString) - Assert.assertEquals("http://test2.example/rest", tuple.property.toString) - Assert.assertEquals("val", tuple.value) - tuple = JsonFormatParser.parseItem(root, context, withMultiContext, time).head.tail.head - Assert.assertEquals("http://test2.example/", tuple.item.toString) - Assert.assertEquals("http://pref2.example/rest", tuple.property.toString) - Assert.assertEquals("val2", tuple.value) - - val prefixInContext = ("@context" -> ("pref" -> "http://test1.example/") ~ ("pref2" -> "pref:pref1")) ~ - ("@context" -> ("pref" -> "http://test2.example/")) ~ - ("pref" -> ("pref:rest" -> "val") ~ ("pref2" -> "val2")) - tuple = JsonFormatParser.parseItem(root, context, prefixInContext, time).head.tail.head - Assert.assertEquals(tuple.property.toString, "http://test1.example/pref1") - - val multiPrefixes = ("@context" -> ("pref" -> "http://test1.example/") ~ ("pref2" -> "pref:pref1")) ~ - ("pref:pref1/pref2" -> ("pref:rest" -> "val") ~ ("pref2" -> "val2")) - tuple = JsonFormatParser.parseItem(root, context, multiPrefixes, time).head.head - Assert.assertEquals(tuple.item.toString, "http://test1.example/pref1/pref2") - } - - @Test - def testNested(): Unit = { - val context = Kvin.DEFAULT_CONTEXT - val time = System.currentTimeMillis - val root = URIs.createURI("http://example.root") - val nested = "item" -> ("p1" -> "v1") ~ - ("nested" -> ("value", ("p1" -> "v1") ~ ("p2" -> ("p3", "v3") ~ ("p4", "v4")))) - val parsed = JsonFormatParser.parseItem(root, context, nested, time) - val expected = Full(List( - new KvinTuple(URIs.createURI("http://example.root/item"), - URIs.createURI("http://example.root/p1"), Kvin.DEFAULT_CONTEXT, time, "v1"), - new KvinTuple(URIs.createURI("http://example.root/item"), - URIs.createURI("http://example.root/nested"), Kvin.DEFAULT_CONTEXT, time, - new Record(URIs.createURI("http://example.root/p1"), "v1").append( - new Record(URIs.createURI("http://example.root/p2"), - new Record(URIs.createURI("http://example.root/p3"), "v3").append( - new Record(URIs.createURI("http://example.root/p4"), "v4") - ) - ) - ) - ) - )) - Assert.assertEquals(expected, parsed) - } -} \ No newline at end of file