diff --git a/stack-clients/docker-compose.yml b/stack-clients/docker-compose.yml index 67af642a..08aaf438 100644 --- a/stack-clients/docker-compose.yml +++ b/stack-clients/docker-compose.yml @@ -1,6 +1,6 @@ services: stack-client: - image: ghcr.io/theworldavatar/stack-client${IMAGE_SUFFIX}:1.54.1 + image: ghcr.io/theworldavatar/stack-client${IMAGE_SUFFIX}:1.55.0 secrets: - blazegraph_password - postgis_password diff --git a/stack-clients/pom.xml b/stack-clients/pom.xml index 1f122256..f04eaa29 100644 --- a/stack-clients/pom.xml +++ b/stack-clients/pom.xml @@ -7,7 +7,7 @@ com.cmclinnovations stack-clients - 1.54.1 + 1.55.0 Stack Clients https://theworldavatar.io @@ -116,7 +116,7 @@ uk.ac.cam.cares.jps jps-base-lib - 1.48.0 + 1.49.0 diff --git a/stack-clients/src/main/java/com/cmclinnovations/stack/clients/timeseries/TimeSeriesRDBClient.java b/stack-clients/src/main/java/com/cmclinnovations/stack/clients/timeseries/TimeSeriesRDBClient.java new file mode 100644 index 00000000..f8b862f2 --- /dev/null +++ b/stack-clients/src/main/java/com/cmclinnovations/stack/clients/timeseries/TimeSeriesRDBClient.java @@ -0,0 +1,131 @@ +package com.cmclinnovations.stack.clients.timeseries; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.sql.Connection; +import java.time.Instant; +import java.util.List; + +import org.apache.commons.io.IOUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import com.cmclinnovations.stack.clients.core.EndpointNames; +import com.cmclinnovations.stack.clients.ontop.OntopClient; +import com.cmclinnovations.stack.clients.utils.LocalTempDir; +import com.cmclinnovations.stack.services.OntopService; +import com.cmclinnovations.stack.services.ServiceManager; +import com.cmclinnovations.stack.services.config.ServiceConfig; + +import uk.ac.cam.cares.jps.base.exception.JPSRuntimeException; +import uk.ac.cam.cares.jps.base.timeseries.TimeSeriesRDBClientOntop; + +public class TimeSeriesRDBClient extends TimeSeriesRDBClientOntop { + + private static final String UNIX_TRS = "http://dbpedia.org/resource/Unix_time"; + private static final String GENERIC_TRS = "http://example.org/TRS_placeholder"; + + /** + * Logger for error output. + */ + private static final Logger LOGGER = LogManager.getLogger(TimeSeriesRDBClient.class); + + // IRI of temporal reference system, used in ontop mapping to indicate reference + // of time, e.g. Unix + private final String trsIri; + + // name of ontop container + private String ontopName = "ontop-timeseries"; + + public TimeSeriesRDBClient(Class timeClass) { + this(timeClass, null); + } + + public TimeSeriesRDBClient(Class timeClass, String trsIri) { + super(timeClass); + if (null != trsIri) { + this.trsIri = trsIri; + LOGGER.info("TRS is set to {}", trsIri); + } else { + if (Instant.class == timeClass) { + this.trsIri = UNIX_TRS; + LOGGER.info("Time class is Instant, TRS is set to {}", UNIX_TRS); + } else { + this.trsIri = GENERIC_TRS; + LOGGER.info("Time class is not Instant, TRS is set to {}", GENERIC_TRS); + } + } + } + + public void setOntopName(String ontopName) { + this.ontopName = ontopName; + } + + @Override + public List bulkInitTimeSeriesTable(List> dataIRIs, List>> dataClasses, + List tsIRIs, Integer srid, Connection conn) { + List result = super.bulkInitTimeSeriesTable(dataIRIs, dataClasses, tsIRIs, srid, conn); + + // spin up a new ontop container if it does not exist + configureOntop(); + + return result; + } + + public void configureOntop() { + String stackName = System.getenv("STACK_NAME"); + if (stackName == null) { + LOGGER.warn("STACK_NAME not detected, skipping Ontop intialisation"); + return; + } + + ServiceManager serviceManager = new ServiceManager(false); + + ServiceConfig newOntopServiceConfig = serviceManager.duplicateServiceConfig(EndpointNames.ONTOP, ontopName); + newOntopServiceConfig.setEnvironmentVariable(OntopService.ONTOP_DB_NAME, "postgres"); + + newOntopServiceConfig.getEndpoints() + .replaceAll((endpointName, connection) -> new com.cmclinnovations.stack.services.config.Connection( + connection.getUrl(), + connection.getUri(), + URI.create(connection.getExternalPath().toString() + .replace(EndpointNames.ONTOP, ontopName)))); + serviceManager.initialiseService(stackName, ontopName); + + OntopClient ontopClient = OntopClient.getInstance(ontopName); + + // create temporary file for ontop mapping + try (LocalTempDir tempDir = new LocalTempDir()) { + + String obda = prepareMapping(); + + Path filePath = tempDir.getPath().resolve("ontop.obda"); + Files.write(filePath, obda.getBytes()); + + // sends obda to container + ontopClient.updateOBDA(filePath); + } catch (IOException e) { + throw new JPSRuntimeException("Failed to write ontop mapping into temporary folder", e); + } + } + + /** + * set TRS in Ontop mapping + * + * @return + */ + private String prepareMapping() { + // read template from resources folder + try (InputStream is = TimeSeriesRDBClient.class.getResourceAsStream("timeseries_ontop_template.obda")) { + return IOUtils.toString(is, StandardCharsets.UTF_8) + .replace("[TRS_REPLACE]", trsIri).replace("[SCHEMA]", getSchema()); + } catch (IOException e) { + throw new JPSRuntimeException("Error while reading timeseries_ontop_template.obda", e); + } + } + +} diff --git a/stack-clients/src/main/resources/com/cmclinnovations/stack/clients/timeseries/timeseries_ontop_template.obda b/stack-clients/src/main/resources/com/cmclinnovations/stack/clients/timeseries/timeseries_ontop_template.obda new file mode 100644 index 00000000..06506690 --- /dev/null +++ b/stack-clients/src/main/resources/com/cmclinnovations/stack/clients/timeseries/timeseries_ontop_template.obda @@ -0,0 +1,29 @@ +[PrefixDeclaration] +obda: https://w3id.org/obda/vocabulary# +geo: http://www.opengis.net/ont/geosparql# +timeprefix: http://www.w3.org/2006/time# +timeseries: https://www.theworldavatar.com/kg/ontotimeseries/ + +[MappingDeclaration] @collection [[ +mappingId time_series_data_[SCHEMA] +target timeseries:data/[SCHEMA]/{data_iri_index} timeseries:hasObservation timeseries:observation/[SCHEMA]/{id} . + timeseries:observation/[SCHEMA]/{id} a timeseries:Observation ; + timeseries:observationOf timeseries:data/[SCHEMA]/{data_iri_index} ; + timeseries:hasResult timeseries:result/[SCHEMA]/{id} ; + timeprefix:hasTime timeseries:time/[SCHEMA]/{id} . + timeseries:time/[SCHEMA]/{id} a timeprefix:Instant ; + timeprefix:inXSDDateTime {time_as_timestamp} ; + timeprefix:inTimePosition timeseries:timePosition/[SCHEMA]/{id}. + timeseries:timePosition/[SCHEMA]/{id} a timeprefix:TimePosition ; + timeprefix:numericPosition {time_as_number} ; + timeprefix:unitType timeprefix:unitSecond ; + timeprefix:hasTRS <[TRS_REPLACE]> . + timeseries:result/[SCHEMA]/{id} a timeseries:Result ; + timeseries:hasValue {"double precision"}, {wkt}^^geo:wktLiteral, {int}; + timeseries:hasUnit {unit} . +source SELECT id, time_as_number, time_as_timestamp, data_iri_index, "double precision", ST_AsText("geometry(Point,4326)") AS wkt, int, unit FROM "[SCHEMA]".time_series_data + +mappingId equivalent_iri_[SCHEMA] +target <{data_iri}> obda:isCanonicalIRIOf timeseries:data/[SCHEMA]/{data_iri_index} . +source SELECT data_iri, data_iri_index from "[SCHEMA]".time_series_data_iri +]] \ No newline at end of file diff --git a/stack-data-uploader/docker-compose.yml b/stack-data-uploader/docker-compose.yml index e07d9ef3..62d8a49a 100644 --- a/stack-data-uploader/docker-compose.yml +++ b/stack-data-uploader/docker-compose.yml @@ -1,6 +1,6 @@ services: stack-data-uploader: - image: ghcr.io/theworldavatar/stack-data-uploader${IMAGE_SUFFIX}:1.54.1 + image: ghcr.io/theworldavatar/stack-data-uploader${IMAGE_SUFFIX}:1.55.0 secrets: - blazegraph_password - postgis_password diff --git a/stack-data-uploader/pom.xml b/stack-data-uploader/pom.xml index 85c6c570..f5fb4bb0 100644 --- a/stack-data-uploader/pom.xml +++ b/stack-data-uploader/pom.xml @@ -7,7 +7,7 @@ com.cmclinnovations stack-data-uploader - 1.54.1 + 1.55.0 Stack Data Uploader https://theworldavatar.io @@ -38,7 +38,7 @@ com.cmclinnovations stack-clients - 1.54.1 + 1.55.0 diff --git a/stack-manager/docker-compose.yml b/stack-manager/docker-compose.yml index 6f095bd3..fae293a3 100644 --- a/stack-manager/docker-compose.yml +++ b/stack-manager/docker-compose.yml @@ -1,6 +1,6 @@ services: stack-manager: - image: ghcr.io/theworldavatar/stack-manager${IMAGE_SUFFIX}:1.54.1 + image: ghcr.io/theworldavatar/stack-manager${IMAGE_SUFFIX}:1.55.0 environment: EXTERNAL_PORT: "${EXTERNAL_PORT-3838}" STACK_BASE_DIR: "${STACK_BASE_DIR}" diff --git a/stack-manager/pom.xml b/stack-manager/pom.xml index 74ae3bd8..8e89c0ee 100644 --- a/stack-manager/pom.xml +++ b/stack-manager/pom.xml @@ -7,7 +7,7 @@ com.cmclinnovations stack-manager - 1.54.1 + 1.55.0 Stack Manager https://theworldavatar.io @@ -38,7 +38,7 @@ com.cmclinnovations stack-clients - 1.54.1 + 1.55.0