/** * This file is part of d:swarm graph extension. * * d:swarm graph extension is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * d:swarm graph extension is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with d:swarm graph extension. If not, see <http://www.gnu.org/licenses/>. */ package org.dswarm.graph.resources; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.concurrent.Callable; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicInteger; import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Context; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.StreamingOutput; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import com.sun.jersey.multipart.BodyPart; import com.sun.jersey.multipart.BodyPartEntity; import com.sun.jersey.multipart.MultiPart; import org.neo4j.graphdb.DynamicLabel; import org.neo4j.graphdb.GraphDatabaseService; import org.neo4j.graphdb.Label; import org.neo4j.graphdb.Node; import org.neo4j.graphdb.Relationship; import org.neo4j.graphdb.ResourceIterator; import org.neo4j.test.TestGraphDatabaseFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import rx.Observable; import rx.observables.BlockingObservable; import rx.observables.ConnectableObservable; import org.dswarm.common.DMPStatics; import org.dswarm.common.model.Attribute; import org.dswarm.common.model.AttributePath; import org.dswarm.common.model.ContentSchema; import org.dswarm.common.model.util.AttributePathUtil; import org.dswarm.common.types.Tuple; import org.dswarm.graph.DMPGraphException; import org.dswarm.graph.delta.Changeset; import org.dswarm.graph.delta.DeltaState; import org.dswarm.graph.delta.match.FirstDegreeExactCSEntityMatcher; import org.dswarm.graph.delta.match.FirstDegreeExactCSValueMatcher; import org.dswarm.graph.delta.match.FirstDegreeExactGDMValueMatcher; import org.dswarm.graph.delta.match.FirstDegreeExactSubGraphEntityMatcher; import org.dswarm.graph.delta.match.FirstDegreeExactSubGraphLeafEntityMatcher; import org.dswarm.graph.delta.match.FirstDegreeModificationCSValueMatcher; import org.dswarm.graph.delta.match.FirstDegreeModificationGDMValueMatcher; import org.dswarm.graph.delta.match.FirstDegreeModificationSubGraphLeafEntityMatcher; import org.dswarm.graph.delta.match.ModificationMatcher; import org.dswarm.graph.delta.match.model.CSEntity; import org.dswarm.graph.delta.match.model.SubGraphEntity; import org.dswarm.graph.delta.match.model.SubGraphLeafEntity; import org.dswarm.graph.delta.match.model.ValueEntity; import org.dswarm.graph.delta.match.model.util.CSEntityUtil; import org.dswarm.graph.delta.util.ChangesetUtil; import org.dswarm.graph.delta.util.GraphDBUtil; import org.dswarm.graph.gdm.DataModelGDMNeo4jProcessor; import org.dswarm.graph.gdm.GDMNeo4jProcessor; import org.dswarm.graph.gdm.SimpleGDMNeo4jProcessor; import org.dswarm.graph.gdm.parse.DataModelGDMNeo4jHandler; import org.dswarm.graph.gdm.parse.GDMChangesetParser; import org.dswarm.graph.gdm.parse.GDMHandler; import org.dswarm.graph.gdm.parse.GDMModelParser; import org.dswarm.graph.gdm.parse.GDMNeo4jHandler; import org.dswarm.graph.gdm.parse.GDMParser; import org.dswarm.graph.gdm.parse.GDMResourceParser; import org.dswarm.graph.gdm.parse.GDMUpdateHandler; import org.dswarm.graph.gdm.parse.GDMUpdateParser; import org.dswarm.graph.gdm.parse.Neo4jDeltaGDMHandler; import org.dswarm.graph.gdm.parse.SimpleGDMNeo4jHandler; import org.dswarm.graph.gdm.read.GDMModelReader; import org.dswarm.graph.gdm.read.GDMResourceReader; import org.dswarm.graph.gdm.read.PropertyGraphGDMModelReader; import org.dswarm.graph.gdm.read.PropertyGraphGDMResourceByIDReader; import org.dswarm.graph.gdm.read.PropertyGraphGDMResourceByURIReader; import org.dswarm.graph.gdm.work.GDMWorker; import org.dswarm.graph.gdm.work.PropertyEnrichGDMWorker; import org.dswarm.graph.gdm.work.PropertyGraphDeltaGDMSubGraphWorker; import org.dswarm.graph.hash.HashUtils; import org.dswarm.graph.index.NamespaceIndex; import org.dswarm.graph.index.SchemaIndexUtils; import org.dswarm.graph.json.Resource; import org.dswarm.graph.json.Statement; import org.dswarm.graph.json.stream.ModelBuilder; import org.dswarm.graph.json.stream.ModelParser; import org.dswarm.graph.json.util.Util; import org.dswarm.graph.model.GraphStatics; import org.dswarm.graph.parse.Neo4jUpdateHandler; import org.dswarm.graph.tx.Neo4jTransactionHandler; import org.dswarm.graph.tx.TransactionHandler; import org.dswarm.graph.versioning.VersioningStatics; /** * @author tgaengler */ @Path("/gdm") public class GDMResource extends GraphResource { private static final Logger LOG = LoggerFactory.getLogger(GDMResource.class); public static final int METADATA_BODY_PART = 0; public static final int CONTENT_BODY_PART = 1; /** * The object mapper that can be utilised to de-/serialise JSON nodes. */ private final ObjectMapper objectMapper; private final TestGraphDatabaseFactory impermanentGraphDatabaseFactory; private static final String IMPERMANENT_GRAPH_DATABASE_PATH = "target/test-data/impermanent-db/"; private static final String READ_GDM_MODEL_TYPE = "read GDM record from graph DB request"; private static final String READ_GDM_RECORD_TYPE = "read GDM record from graph DB request"; private static final String SEARCH_GDM_RECORDS_TYPE = "search GDM records"; public GDMResource() { objectMapper = Util.getJSONObjectMapper(); impermanentGraphDatabaseFactory = new TestGraphDatabaseFactory(); } @GET @Path("/ping") public String ping() { GDMResource.LOG.debug("ping was called"); return "pong"; } /** * multipart/mixed payload contains two body parts:<br/> * - first body part is the metadata (i.e. a JSON object with mandatory and obligatory properties for processing the * content):<br/> * - "data_model_URI" (mandatory)<br/> * - "content_schema" (obligatory)<br/> * - "deprecate_missing_records" (obligatory)<br/> * - "record_class_uri" (mandatory for "deprecate_missing_records")<br/> * - second body part is the content (i.e. the real data) * * @param multiPart * @param database * @return * @throws DMPGraphException * @throws IOException */ @POST @Path("/put") @Consumes("multipart/mixed") public Response writeGDM(final MultiPart multiPart, @Context final GraphDatabaseService database, @Context final HttpHeaders requestHeaders) throws DMPGraphException, IOException { LOG.debug("try to process GDM statements and write them into graph db"); final String headers = readHeaders(requestHeaders); GDMResource.LOG.debug("try to process GDM statements and write them into graph db with\n{}", headers); final List<BodyPart> bodyParts = getBodyParts(multiPart); final ObjectNode metadata = getMetadata(bodyParts); final InputStream content = getContent(bodyParts); final Optional<String> optionalDataModelURI = getMetadataPart(DMPStatics.DATA_MODEL_URI_IDENTIFIER, metadata, true); final String dataModelURI = optionalDataModelURI.get(); final Optional<Boolean> optionalEnableVersioning = getEnableVersioningFlag(metadata); final boolean enableVersioning; if (optionalEnableVersioning.isPresent()) { enableVersioning = optionalEnableVersioning.get(); } else { enableVersioning = true; } final AtomicInteger counter = new AtomicInteger(0); final Tuple<Observable<Resource>, BufferedInputStream> modelTuple = getModel(content); final ConnectableObservable<Resource> model = modelTuple.v1() .doOnSubscribe(() -> LOG.debug("subscribed to model observable")) .doOnNext(record -> { if (counter.incrementAndGet() == 1) { LOG.debug("read first records from model observable"); } }) .doOnCompleted(() -> LOG.debug("read '{}' records from model observable", counter.get())) .onBackpressureBuffer(10000) .publish(); final BufferedInputStream bis = modelTuple.v2(); LOG.debug("deserialized GDM statements that were serialised as JSON"); LOG.debug("try to write GDM statements into graph db"); final TransactionHandler tx = new Neo4jTransactionHandler(database); final NamespaceIndex namespaceIndex = new NamespaceIndex(database, tx); final String prefixedDataModelURI = namespaceIndex.createPrefixedURI(dataModelURI); final GDMNeo4jProcessor processor = new DataModelGDMNeo4jProcessor(database, tx, namespaceIndex, prefixedDataModelURI); LOG.info("process GDM statements and write them into graph db for data model '{}' ('{}')", dataModelURI, prefixedDataModelURI); try { final GDMNeo4jHandler handler = new DataModelGDMNeo4jHandler(processor, enableVersioning); final Observable<Resource> newModel; final Observable<Boolean> deprecateRecordsObservable; // note: versioning is enable by default if (enableVersioning) { LOG.info("do versioning with GDM statements for data model '{}' ('{}')", dataModelURI, prefixedDataModelURI); final Optional<ContentSchema> optionalPrefixedContentSchema = getPrefixedContentSchema(metadata, namespaceIndex); // = new resources model, since existing, modified resources were already written to the DB final Tuple<Observable<Resource>, Observable<Long>> result = calculateDeltaForDataModel(model, optionalPrefixedContentSchema, prefixedDataModelURI, database, handler, namespaceIndex); final Observable<Resource> deltaModel = result.v1().onBackpressureBuffer(10000); final Optional<Boolean> optionalDeprecateMissingRecords = getDeprecateMissingRecordsFlag(metadata); if (optionalDeprecateMissingRecords.isPresent() && optionalDeprecateMissingRecords.get()) { final Optional<String> optionalRecordClassURI = getMetadataPart(DMPStatics.RECORD_CLASS_URI_IDENTIFIER, metadata, false); if (!optionalRecordClassURI.isPresent()) { throw new DMPGraphException("could not deprecate missing records, because no record class uri is given"); } // deprecate missing records in DB final Observable<Long> processedResources = result.v2(); deprecateRecordsObservable = deprecateMissingRecords(processedResources, optionalRecordClassURI.get(), dataModelURI, ((Neo4jUpdateHandler) handler.getHandler()) .getVersionHandler().getLatestVersion(), processor); } else { deprecateRecordsObservable = Observable.empty(); } newModel = deltaModel; LOG.info("finished versioning with GDM statements for data model '{}' ('{}')", dataModelURI, prefixedDataModelURI); } else { newModel = model; deprecateRecordsObservable = Observable.empty(); } final AtomicInteger counter2 = new AtomicInteger(0); final ConnectableObservable<Resource> newModelLogged = newModel.doOnSubscribe(() -> LOG.debug("subscribed to new model observable")) .doOnNext(record -> { if (counter2.incrementAndGet() == 1) { LOG.debug("read first records from new model observable"); } }) .doOnCompleted(() -> LOG.debug("read '{}' records from new model observable", counter2.get())) .onBackpressureBuffer(10000) .publish(); //if (deltaModel.size() > 0) { // parse model only, when model contains some resources final AtomicInteger counter3 = new AtomicInteger(0); final GDMParser parser = new GDMModelParser(newModelLogged); parser.setGDMHandler(handler); final Observable<Boolean> newResourcesObservable = parser.parse().doOnSubscribe(() -> LOG.debug("subscribed to new resources observable")) .doOnNext(record -> { if (counter3.incrementAndGet() == 1) { LOG.debug("read first records from new resources observable"); } }) .doOnCompleted(() -> LOG.debug("read '{}' records from new resources observable", counter3.get())); try { final Observable<Boolean> connectedObservable = deprecateRecordsObservable.concatWith(newResourcesObservable); final BlockingObservable<Boolean> blockingObservable = connectedObservable.toBlocking(); final Iterator<Boolean> iterator = blockingObservable.getIterator(); newModelLogged.connect(); if (!enableVersioning) { model.connect(); } if (!iterator.hasNext()) { LOG.debug("model contains no resources, i.e., nothing needs to be written to the DB"); } while (iterator.hasNext()) { iterator.next(); } } catch (final RuntimeException e) { throw new DMPGraphException(e.getMessage(), e.getCause()); } final Long size = handler.getHandler().getCountedStatements(); if (enableVersioning && size > 0) { // update data model version only when some statements are written to the DB ((Neo4jUpdateHandler) handler.getHandler()).getVersionHandler().updateLatestVersion(); } handler.getHandler().closeTransaction(); bis.close(); content.close(); LOG.info( "finished writing {} resources with {} GDM statements (added {} relationships, added {} nodes (resources + bnodes + literals), added {} literals) into graph db for data model URI '{}' ('{}')", parser.parsedResources(), handler.getHandler().getCountedStatements(), handler.getHandler().getRelationshipsAdded(), handler.getHandler().getNodesAdded(), handler.getHandler().getCountedLiterals(), dataModelURI, prefixedDataModelURI); return Response.ok().build(); } catch (final Exception e) { processor.getProcessor().failTx(); bis.close(); content.close(); LOG.error("couldn't write GDM statements into graph db: {}", e.getMessage(), e); throw e; } } @POST @Path("/put") @Consumes(MediaType.APPLICATION_OCTET_STREAM) public Response writeGDM(final InputStream inputStream, @Context final GraphDatabaseService database, @Context final HttpHeaders requestHeaders) throws DMPGraphException, IOException { LOG.debug("try to process GDM statements and write them into graph db"); final String headers = readHeaders(requestHeaders); GDMResource.LOG.debug("try to process GDM statements and write them into graph db with\n{}", headers); if (inputStream == null) { final String message = "input stream for write to graph DB request is null"; GDMResource.LOG.error(message); throw new DMPGraphException(message); } final BufferedInputStream bis = new BufferedInputStream(inputStream, 1024); final ModelParser modelParser = new ModelParser(bis); final Observable<Resource> model = modelParser.parse(); LOG.debug("try to write GDM statements into graph db"); final TransactionHandler tx = new Neo4jTransactionHandler(database); final NamespaceIndex namespaceIndex = new NamespaceIndex(database, tx); final GDMNeo4jProcessor processor = new SimpleGDMNeo4jProcessor(database, tx, namespaceIndex); try { final GDMHandler handler = new SimpleGDMNeo4jHandler(processor, true); final GDMParser parser = new GDMModelParser(model); parser.setGDMHandler(handler); final Observable<Boolean> processedResources = parser.parse(); try { final Iterator<Boolean> iterator = processedResources.toBlocking().getIterator(); if (!iterator.hasNext()) { LOG.debug("model contains no resources, i.e., nothing needs to be written to the DB"); } while (iterator.hasNext()) { iterator.next(); } } catch (final RuntimeException e) { throw new DMPGraphException(e.getMessage(), e.getCause()); } handler.getHandler().closeTransaction(); bis.close(); inputStream.close(); LOG.debug( "finished writing {} resources with {} GDM statements (added {} relationships, added {} nodes (resources + bnodes + literals), added {} literals) into graph db", parser.parsedResources(), handler.getHandler().getCountedStatements(), handler.getHandler().getRelationshipsAdded(), handler.getHandler().getNodesAdded(), handler.getHandler().getCountedLiterals()); } catch (final Exception e) { processor.getProcessor().failTx(); bis.close(); inputStream.close(); LOG.error("couldn't write GDM statements into graph db: {}", e.getMessage(), e); throw e; } return Response.ok().build(); } @POST @Path("/get") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public Response readGDM(final String jsonObjectString, @Context final GraphDatabaseService database, @Context final HttpHeaders requestHeaders) throws DMPGraphException { final String headers = readHeaders(requestHeaders); GDMResource.LOG.debug("try to read GDM statements from graph db with\n{}", headers); final ObjectNode requestJSON = deserializeJSON(jsonObjectString, READ_GDM_MODEL_TYPE); final String recordClassUri = requestJSON.get(DMPStatics.RECORD_CLASS_URI_IDENTIFIER).asText(); final String dataModelUri = requestJSON.get(DMPStatics.DATA_MODEL_URI_IDENTIFIER).asText(); final Optional<Integer> optionalVersion = getIntValue(DMPStatics.VERSION_IDENTIFIER, requestJSON); final Optional<Integer> optionalAtMost = getIntValue(DMPStatics.AT_MOST_IDENTIFIER, requestJSON); final TransactionHandler tx = new Neo4jTransactionHandler(database); final NamespaceIndex namespaceIndex = new NamespaceIndex(database, tx); final String prefixedRecordClassURI = namespaceIndex.createPrefixedURI(recordClassUri); final String prefixedDataModelURI = namespaceIndex.createPrefixedURI(dataModelUri); GDMResource.LOG .info("try to read GDM statements for data model uri = '{}' ('{}') and record class uri = '{}' ('{}') and version = '{}' from graph db", dataModelUri, prefixedDataModelURI, recordClassUri, prefixedRecordClassURI, optionalVersion); final GDMModelReader gdmReader = new PropertyGraphGDMModelReader(prefixedRecordClassURI, prefixedDataModelURI, optionalVersion, optionalAtMost, database, tx, namespaceIndex); final StreamingOutput stream = os -> { try { final BufferedOutputStream bos = new BufferedOutputStream(os, 1024); final Optional<ModelBuilder> optionalModelBuilder = gdmReader.read(bos); if (optionalModelBuilder.isPresent()) { final ModelBuilder modelBuilder = optionalModelBuilder.get(); modelBuilder.build(); bos.flush(); os.flush(); bos.close(); os.close(); GDMResource.LOG .info("finished reading '{}' resources with '{}' GDM statements ('{}' via GDM reader) for data model uri = '{}' ('{}') and record class uri = '{}' ('{}') and version = '{}' from graph db", gdmReader.readResources(), gdmReader.countStatements(), gdmReader.countStatements(), dataModelUri, prefixedDataModelURI, recordClassUri, prefixedRecordClassURI, optionalVersion); } else { bos.close(); os.close(); GDMResource.LOG .info("couldn't find any GDM statements for data model uri = '{}' ('{}') and record class uri = '{}' ('{}') and version = '{}' from graph db", dataModelUri, prefixedDataModelURI, recordClassUri, prefixedRecordClassURI, optionalVersion); } } catch (final DMPGraphException e) { throw new WebApplicationException(e); } }; return Response.ok(stream, MediaType.APPLICATION_JSON_TYPE).build(); } @POST @Path("/getrecord") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public Response readGDMRecord(final String jsonObjectString, @Context final GraphDatabaseService database) throws DMPGraphException { GDMResource.LOG.debug("try to read GDM record from graph db"); final ObjectNode requestJSON = deserializeJSON(jsonObjectString, READ_GDM_RECORD_TYPE); final Optional<String> optionalRecordUri = getStringValue(DMPStatics.RECORD_URI_IDENTIFIER, requestJSON); final String dataModelUri = requestJSON.get(DMPStatics.DATA_MODEL_URI_IDENTIFIER).asText(); final Optional<Integer> optionalVersion = getIntValue(DMPStatics.VERSION_IDENTIFIER, requestJSON); final Optional<String> optionalLegacyRecordIdentifierAP = getStringValue(DMPStatics.LEGACY_RECORD_IDENTIFIER_ATTRIBUTE_PATH, requestJSON); final Optional<String> optionalRecordId = getStringValue(DMPStatics.RECORD_ID_IDENTIFIER, requestJSON); final TransactionHandler tx = new Neo4jTransactionHandler(database); final NamespaceIndex namespaceIndex = new NamespaceIndex(database, tx); final String prefixedDataModelURI = namespaceIndex.createPrefixedURI(dataModelUri); final GDMResourceReader gdmReader; final String requestParameter; if (optionalRecordUri.isPresent()) { final String recordURI = optionalRecordUri.get(); final String prefixedRecordURI = namespaceIndex.createPrefixedURI(recordURI); requestParameter = String.format("and record uri = '%s' ('%s')", recordURI, prefixedRecordURI); GDMResource.LOG .debug("try to read GDM record for data model uri = '{}' ('{}') {} and version = '{}' from graph db", dataModelUri, prefixedDataModelURI, requestParameter, optionalVersion); gdmReader = new PropertyGraphGDMResourceByURIReader(prefixedRecordURI, prefixedDataModelURI, optionalVersion, database, tx, namespaceIndex); } else if (optionalLegacyRecordIdentifierAP.isPresent() && optionalRecordId.isPresent()) { final AttributePath legacyRecordIdentifierAP = AttributePathUtil.parseAttributePathString(optionalLegacyRecordIdentifierAP.get()); final AttributePath prefixedLegacyRecordIdentifierAP = prefixAttributePath(legacyRecordIdentifierAP, namespaceIndex); final String recordId = optionalRecordId.get(); requestParameter = String .format("and legacy record identifier attribute path = '%s' ('%s') and record identifier = '%s'", legacyRecordIdentifierAP, prefixedLegacyRecordIdentifierAP, recordId); GDMResource.LOG .info("try to read GDM record for data model uri = '{}' ('{}') {} and version = '{}' from graph db", dataModelUri, prefixedDataModelURI, requestParameter, optionalVersion); gdmReader = new PropertyGraphGDMResourceByIDReader(recordId, prefixedLegacyRecordIdentifierAP, prefixedDataModelURI, optionalVersion, database, tx, namespaceIndex); } else { throw new DMPGraphException( "no identifiers given to retrieve a GDM record from the graph db. Please specify a record URI or legacy record identifier attribute path + record identifier"); } final Resource resource = gdmReader.read(); if (resource == null) { GDMResource.LOG.info( "no record found for data mode uri = '{}' ('{}') {} and version = '{}' from graph db", dataModelUri, prefixedDataModelURI, requestParameter, optionalVersion); return Response.status(404).build(); } final String result = serializeJSON(resource, READ_GDM_RECORD_TYPE); GDMResource.LOG .info("finished reading '{}' GDM statements ('{}' via GDM reader) for data model uri = '{}' ('{}') {} and version = '{}' from graph db", resource.size(), gdmReader.countStatements(), dataModelUri, prefixedDataModelURI, requestParameter, optionalVersion); return Response.ok().entity(result).build(); } @POST @Path("/searchrecords") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public Response searchGDMRecords(final String jsonObjectString, @Context final GraphDatabaseService database) throws DMPGraphException { GDMResource.LOG.debug("try to {} in graph db", SEARCH_GDM_RECORDS_TYPE); final ObjectNode requestJSON = deserializeJSON(jsonObjectString, READ_GDM_RECORD_TYPE); final String keyAPString = requestJSON.get(DMPStatics.KEY_ATTRIBUTE_PATH_IDENTIFIER).asText(); final String searchValue = requestJSON.get(DMPStatics.SEARCH_VALUE_IDENTIFIER).asText(); final String dataModelUri = requestJSON.get(DMPStatics.DATA_MODEL_URI_IDENTIFIER).asText(); final Optional<Integer> optionalVersion = getIntValue(DMPStatics.VERSION_IDENTIFIER, requestJSON); final TransactionHandler tx = new Neo4jTransactionHandler(database); final NamespaceIndex namespaceIndex = new NamespaceIndex(database, tx); final String prefixedDataModelURI = namespaceIndex.createPrefixedURI(dataModelUri); final AttributePath keyAP = AttributePathUtil.parseAttributePathString(keyAPString); final AttributePath prefixedKeyAP = prefixAttributePath(keyAP, namespaceIndex); GDMResource.LOG .info("try to search GDM records for key attribute path = '{}' ('{}') and search value = '{}' in data model '{}' ('{}') with version = '{}' from graph db", keyAP, prefixedKeyAP, searchValue, dataModelUri, prefixedDataModelURI, optionalVersion); final Collection<String> recordURIs = GraphDBUtil.determineRecordUris(searchValue, prefixedKeyAP, prefixedDataModelURI, database); if (recordURIs == null || recordURIs.isEmpty()) { GDMResource.LOG .info("couldn't find any record for key attribute path = '{}' ('{}') and search value = '{}' in data model '{}' ('{}') with version = '{}' from graph db", keyAP, prefixedKeyAP, searchValue, dataModelUri, prefixedDataModelURI, optionalVersion); final StreamingOutput stream = os -> { final BufferedOutputStream bos = new BufferedOutputStream(os, 1024); final ModelBuilder modelBuilder = new ModelBuilder(bos); modelBuilder.build(); bos.flush(); os.flush(); bos.close(); os.close(); }; return Response.ok(stream, MediaType.APPLICATION_JSON_TYPE).build(); } final StreamingOutput stream = os -> { try { final BufferedOutputStream bos = new BufferedOutputStream(os, 1024); final ModelBuilder modelBuilder = new ModelBuilder(bos); int resourcesSize = 0; long statementsSize = 0; for (final String recordUri : recordURIs) { final GDMResourceReader gdmReader = new PropertyGraphGDMResourceByURIReader(recordUri, prefixedDataModelURI, optionalVersion, database, tx, namespaceIndex); try { final Resource resource = gdmReader.read(); modelBuilder.addResource(resource); resourcesSize++; statementsSize += resource.size(); } catch (final DMPGraphException e) { GDMResource.LOG.debug("couldn't retrieve record for record URI '{}'", recordUri); } } modelBuilder.build(); bos.flush(); os.flush(); bos.close(); os.close(); if (resourcesSize > 0) { GDMResource.LOG .info("finished reading '{} records with '{}' GDM statements for key attribute path = '{}' ('{}') and search value = '{}' in data model '{}' ('{}') with version = '{}' from graph db", resourcesSize, statementsSize, keyAP, prefixedKeyAP, searchValue, dataModelUri, prefixedDataModelURI, optionalVersion); } else { GDMResource.LOG .info("couldn't retrieve any record for key attribute path = '{}' ('{}') and search value = '{}' in data model '{}' ('{}') with version = '{}' from graph db", keyAP, prefixedKeyAP, searchValue, dataModelUri, prefixedDataModelURI, optionalVersion); } } catch (final DMPGraphException e) { throw new WebApplicationException(e); } }; return Response.ok(stream, MediaType.APPLICATION_JSON_TYPE).build(); } private Tuple<Observable<Resource>, Observable<Long>> calculateDeltaForDataModel( final ConnectableObservable<Resource> model, final Optional<ContentSchema> optionalPrefixedContentSchema, final String prefixedDataModelURI, final GraphDatabaseService permanentDatabase, final GDMUpdateHandler handler, final NamespaceIndex namespaceIndex) throws DMPGraphException { GDMResource.LOG.debug("start calculating delta for model"); final Set<Long> processedResources = new HashSet<>(); // calculate delta resource-wise final Observable<Resource> newResources = model.flatMap(newResource -> { try { final String resourceURI = newResource.getUri(); final String prefixedResourceURI = namespaceIndex.createPrefixedURI(resourceURI); final String hash = UUID.randomUUID().toString(); final GraphDatabaseService newResourceDB = loadResource(newResource, IMPERMANENT_GRAPH_DATABASE_PATH + hash + "-2", namespaceIndex); final Resource existingResource; final GDMResourceReader gdmReader; final TransactionHandler tx = new Neo4jTransactionHandler(permanentDatabase); if (optionalPrefixedContentSchema.isPresent() && optionalPrefixedContentSchema.get().getRecordIdentifierAttributePath() != null) { // determine legacy resource identifier via content schema final String recordIdentifier = GraphDBUtil.determineRecordIdentifier(newResourceDB, optionalPrefixedContentSchema.get() .getRecordIdentifierAttributePath(), prefixedResourceURI); // try to retrieve existing model via legacy record identifier // note: version is absent -> should make use of latest version gdmReader = new PropertyGraphGDMResourceByIDReader(recordIdentifier, optionalPrefixedContentSchema.get().getRecordIdentifierAttributePath(), prefixedDataModelURI, Optional.empty(), permanentDatabase, tx, namespaceIndex); } else { // try to retrieve existing model via resource uri // note: version is absent -> should make use of latest version gdmReader = new PropertyGraphGDMResourceByURIReader(prefixedResourceURI, prefixedDataModelURI, Optional.empty(), permanentDatabase, tx, namespaceIndex); } existingResource = gdmReader.read(); if (existingResource == null) { // we don't need to calculate the delta, since everything is new shutDownDeltaDB(newResourceDB); // take new resource model, since there was no match in the data model graph for this resource identifier return Observable.just(newResource); } final String existingResourceURI = existingResource.getUri(); final String prefixedExistingResourceURI = handler.getHandler().getProcessor().createPrefixedURI(existingResourceURI); final long existingResourceHash = handler.getHandler().getProcessor().generateResourceHash(prefixedExistingResourceURI, Optional.empty()); processedResources.add(existingResourceHash); // final Model newResourceModel = new Model(); // newResourceModel.addResource(resource); final GraphDatabaseService existingResourceDB = loadResource(existingResource, IMPERMANENT_GRAPH_DATABASE_PATH + hash + "-1", namespaceIndex); final Changeset changeset = calculateDeltaForResource(existingResource, existingResourceDB, newResource, newResourceDB, optionalPrefixedContentSchema, namespaceIndex); if (!changeset.hasChanges()) { // process changeset only, if it provides changes GDMResource.LOG.debug("no changes detected for this resource"); shutDownDeltaDBs(existingResourceDB, newResourceDB); return Observable.empty(); } // write modified resources resource-wise - instead of the whole model at once. final GDMUpdateParser parser = new GDMChangesetParser(changeset, existingResourceHash, existingResourceDB, newResourceDB); parser.setGDMHandler(handler); parser.parse(); shutDownDeltaDBs(existingResourceDB, newResourceDB); return Observable.empty(); } catch (final DMPGraphException e) { throw new RuntimeException(e); } }); try { final Observable<Resource> completedNewResources = newResources.doOnCompleted(() -> GDMResource.LOG.info("finished calculating delta for model and writing changes to graph DB")).cache(); // needed to make connectable observable to work? completedNewResources.ignoreElements().subscribe(); final BlockingObservable<Resource> blockingObservable = completedNewResources.toBlocking(); model.connect(); @SuppressWarnings("unused") final Resource runNewResources = blockingObservable.lastOrDefault(null); final Observable<Long> processedResourcesObservable = Observable.from(processedResources); // return only model with new, non-existing resources return Tuple.tuple(completedNewResources, processedResourcesObservable); } catch (final RuntimeException e) { throw new DMPGraphException(e.getMessage(), e.getCause()); } } private Changeset calculateDeltaForResource(final Resource existingResource, final GraphDatabaseService existingResourceDB, final Resource newResource, final GraphDatabaseService newResourceDB, final Optional<ContentSchema> optionalPrefixedContentSchema, final NamespaceIndex namespaceIndex) throws DMPGraphException { final String existingResourceURI = existingResource.getUri(); final String newResourceURI = newResource.getUri(); final String prefixedExistingResourceURI = namespaceIndex.createPrefixedURI(existingResourceURI); final String prefixedNewResourceURI = namespaceIndex.createPrefixedURI(newResourceURI); final long existingResourceHash = HashUtils.generateHash(prefixedExistingResourceURI); final long newResourceHash = HashUtils.generateHash(prefixedNewResourceURI); enrichModel(existingResourceDB, namespaceIndex, prefixedExistingResourceURI, existingResourceHash); enrichModel(newResourceDB, namespaceIndex, prefixedNewResourceURI, newResourceHash); // GraphDBUtil.printNodes(existingResourceDB); // GraphDBUtil.printRelationships(existingResourceDB); // GraphDBUtil.printPaths(existingResourceDB, existingResource.getUri()); // GraphDBPrintUtil.printDeltaRelationships(existingResourceDB); // final URL resURL = Resources.getResource("versioning/lic_dmp_v2.csv"); // final String resURLString = resURL.toString(); // try { // final URL existingResURL = new URL(newResource.getUri()); // final String path = existingResURL.getPath(); // final String uuid = path.substring(path.lastIndexOf("/") + 1, path.length()); // final String newResURLString = resURLString + "." + uuid + ".txt"; // final URL newResURL = new URL(newResURLString); // GraphDBPrintUtil.writeDeltaRelationships(newResourceDB, newResURL); // } catch (MalformedURLException e) { // e.printStackTrace(); // } // GraphDBUtil.printNodes(newResourceDB); // GraphDBUtil.printRelationships(newResourceDB); // GraphDBUtil.printPaths(newResourceDB, newResource.getUri()); // GraphDBPrintUtil.printDeltaRelationships(newResourceDB); final Map<Long, Long> changesetModifications = new HashMap<>(); final Optional<AttributePath> optionalPrefixedCommonAttributePath; if (optionalPrefixedContentSchema.isPresent()) { optionalPrefixedCommonAttributePath = AttributePathUtil.determineCommonAttributePath(optionalPrefixedContentSchema.get()); } else { optionalPrefixedCommonAttributePath = Optional.empty(); } if (optionalPrefixedCommonAttributePath.isPresent()) { // do specific processing with content schema knowledge final AttributePath commonPrefixedAttributePath = optionalPrefixedCommonAttributePath.get(); final ContentSchema prefixedContentSchema = optionalPrefixedContentSchema.get(); final Collection<CSEntity> newCSEntities = GraphDBUtil.getCSEntities(newResourceDB, prefixedNewResourceURI, commonPrefixedAttributePath, prefixedContentSchema); final Collection<CSEntity> existingCSEntities = GraphDBUtil.getCSEntities(existingResourceDB, prefixedExistingResourceURI, commonPrefixedAttributePath, prefixedContentSchema); // do delta calculation on enriched GDM models in graph // note: we can also follow a different strategy, i.e., all most exact steps first and the reduce this level, i.e., do // for // each exact level all steps first and continue afterwards (?) // 1. identify exact matches for cs entities // 1.1 hash with key, value(s) + entity order + value(s) order => matches complete cs entities // keep attention to sub entities of CS entities -> note: this needs to be done as part of the the exact cs entity => // see step 7 // matching as well, i.e., we need to be able to calc a hash from sub entities of the cs entities final FirstDegreeExactCSEntityMatcher exactCSMatcher = new FirstDegreeExactCSEntityMatcher(Optional.ofNullable(existingCSEntities), Optional.ofNullable(newCSEntities), existingResourceDB, newResourceDB, prefixedExistingResourceURI, prefixedNewResourceURI); exactCSMatcher.match(); final Optional<? extends Collection<CSEntity>> newExactCSNonMatches = exactCSMatcher.getNewEntitiesNonMatches(); final Optional<? extends Collection<CSEntity>> existingExactCSNonMatches = exactCSMatcher.getExistingEntitiesNonMatches(); final Optional<? extends Collection<ValueEntity>> newFirstDegreeExactCSValueNonMatches = CSEntityUtil .getValueEntities(newExactCSNonMatches); final Optional<? extends Collection<ValueEntity>> existingFirstDegreeExactCSValueNonMatches = CSEntityUtil .getValueEntities(existingExactCSNonMatches); // 1.2 hash with key, value + entity order + value order => matches value entities final FirstDegreeExactCSValueMatcher firstDegreeExactCSValueMatcher = new FirstDegreeExactCSValueMatcher( existingFirstDegreeExactCSValueNonMatches, newFirstDegreeExactCSValueNonMatches, existingResourceDB, newResourceDB, prefixedExistingResourceURI, prefixedNewResourceURI); firstDegreeExactCSValueMatcher.match(); final Optional<? extends Collection<ValueEntity>> newExactCSValueNonMatches = firstDegreeExactCSValueMatcher.getNewEntitiesNonMatches(); final Optional<? extends Collection<ValueEntity>> existingExactCSValueNonMatches = firstDegreeExactCSValueMatcher .getExistingEntitiesNonMatches(); // 1.3 hash with key, value + entity order => matches value entities // 1.4 hash with key, value => matches value entities // 2. identify modifications for cs entities // 2.1 hash with key + entity order + value order => matches value entities final ModificationMatcher<ValueEntity> modificationCSMatcher = new FirstDegreeModificationCSValueMatcher(existingExactCSValueNonMatches, newExactCSValueNonMatches, existingResourceDB, newResourceDB, prefixedExistingResourceURI, prefixedNewResourceURI); modificationCSMatcher.match(); // 2.2 hash with key + entity order => matches value entities // 2.3 hash with key => matches value entities // 7. identify non-matched CS entity sub graphs // TODO: remove this later GDMResource.LOG.debug("determine non-matched cs entity sub graphs for new cs entities"); final Collection<SubGraphEntity> newSubGraphEntities = GraphDBUtil.determineNonMatchedCSEntitySubGraphs(newCSEntities, newResourceDB); // TODO: remove this later GDMResource.LOG.debug("determine non-matched cs entity sub graphs for existing entities"); final Collection<SubGraphEntity> existingSubGraphEntities = GraphDBUtil.determineNonMatchedCSEntitySubGraphs(existingCSEntities, existingResourceDB); // 7.1 identify exact matches of (non-hierarchical) CS entity sub graphs // 7.1.1 key + predicate + sub graph hash + order final FirstDegreeExactSubGraphEntityMatcher firstDegreeExactSubGraphEntityMatcher = new FirstDegreeExactSubGraphEntityMatcher( Optional.ofNullable(existingSubGraphEntities), Optional.ofNullable(newSubGraphEntities), existingResourceDB, newResourceDB, prefixedExistingResourceURI, prefixedNewResourceURI); firstDegreeExactSubGraphEntityMatcher.match(); final Optional<? extends Collection<SubGraphEntity>> newFirstDegreeExactSubGraphEntityNonMatches = firstDegreeExactSubGraphEntityMatcher .getNewEntitiesNonMatches(); final Optional<? extends Collection<SubGraphEntity>> existingFirstDegreeExactSubGraphEntityNonMatches = firstDegreeExactSubGraphEntityMatcher .getExistingEntitiesNonMatches(); // 7.2 identify of partial matches (paths) of (non-hierarchical) CS entity sub graphs final Optional<? extends Collection<SubGraphLeafEntity>> newSubGraphLeafEntities = GraphDBUtil.getSubGraphLeafEntities( newFirstDegreeExactSubGraphEntityNonMatches, newResourceDB); final Optional<? extends Collection<SubGraphLeafEntity>> existingSubGraphLeafEntities = GraphDBUtil.getSubGraphLeafEntities( existingFirstDegreeExactSubGraphEntityNonMatches, existingResourceDB); // 7.2.1 key + predicate + sub graph leaf path hash + order final FirstDegreeExactSubGraphLeafEntityMatcher firstDegreeExactSubGraphLeafEntityMatcher = new FirstDegreeExactSubGraphLeafEntityMatcher( existingSubGraphLeafEntities, newSubGraphLeafEntities, existingResourceDB, newResourceDB, prefixedExistingResourceURI, prefixedNewResourceURI); firstDegreeExactSubGraphLeafEntityMatcher.match(); final Optional<? extends Collection<SubGraphLeafEntity>> newFirstDegreeExactSubGraphLeafEntityNonMatches = firstDegreeExactSubGraphLeafEntityMatcher .getNewEntitiesNonMatches(); final Optional<? extends Collection<SubGraphLeafEntity>> existingFirstDegreeExactSubGraphLeafEntityNonMatches = firstDegreeExactSubGraphLeafEntityMatcher .getExistingEntitiesNonMatches(); // 7.3 identify modifications of (non-hierarchical) sub graphs final FirstDegreeModificationSubGraphLeafEntityMatcher firstDegreeModificationSubGraphLeafEntityMatcher = new FirstDegreeModificationSubGraphLeafEntityMatcher( existingFirstDegreeExactSubGraphLeafEntityNonMatches, newFirstDegreeExactSubGraphLeafEntityNonMatches, existingResourceDB, newResourceDB, prefixedExistingResourceURI, prefixedNewResourceURI); firstDegreeModificationSubGraphLeafEntityMatcher.match(); for (final Map.Entry<ValueEntity, ValueEntity> modificationEntry : modificationCSMatcher.getModifications().entrySet()) { changesetModifications.put(modificationEntry.getKey().getNodeId(), modificationEntry.getValue().getNodeId()); } for (final Map.Entry<SubGraphLeafEntity, SubGraphLeafEntity> firstDegreeModificationSubGraphLeafEntityModificationEntry : firstDegreeModificationSubGraphLeafEntityMatcher .getModifications().entrySet()) { changesetModifications.put(firstDegreeModificationSubGraphLeafEntityModificationEntry.getKey().getNodeId(), firstDegreeModificationSubGraphLeafEntityModificationEntry.getValue().getNodeId()); } } // 3. identify exact matches of resource node-based statements final Collection<ValueEntity> newFlatResourceNodeValueEntities = GraphDBUtil.getFlatResourceNodeValues(prefixedNewResourceURI, newResourceDB); final Collection<ValueEntity> existingFlatResourceNodeValueEntities = GraphDBUtil.getFlatResourceNodeValues(prefixedExistingResourceURI, existingResourceDB); // 3.1 with key (predicate), value + value order => matches value entities final FirstDegreeExactGDMValueMatcher firstDegreeExactGDMValueMatcher = new FirstDegreeExactGDMValueMatcher( Optional.ofNullable(existingFlatResourceNodeValueEntities), Optional.ofNullable(newFlatResourceNodeValueEntities), existingResourceDB, newResourceDB, prefixedExistingResourceURI, prefixedNewResourceURI); firstDegreeExactGDMValueMatcher.match(); final Optional<? extends Collection<ValueEntity>> newFirstDegreeExactGDMValueNonMatches = firstDegreeExactGDMValueMatcher .getNewEntitiesNonMatches(); final Optional<? extends Collection<ValueEntity>> existingFirstDegreeExactGDMValueNonMatches = firstDegreeExactGDMValueMatcher .getExistingEntitiesNonMatches(); // 4. identify modifications of resource node-based statements // 4.1 with key (predicate), value + value order => matches value entities final FirstDegreeModificationGDMValueMatcher firstDegreeModificationGDMValueMatcher = new FirstDegreeModificationGDMValueMatcher( existingFirstDegreeExactGDMValueNonMatches, newFirstDegreeExactGDMValueNonMatches, existingResourceDB, newResourceDB, prefixedExistingResourceURI, prefixedNewResourceURI); firstDegreeModificationGDMValueMatcher.match(); // 5. identify additions in new model graph // => see above // 6. identify removals in existing model graph // => see above // TODO: do sub graph matching for node-based statements (?) // // note: mark matches or modifications after every step // maybe utilise confidence value for different matching approaches // check graph matching completeness final boolean isExistingResourceMatchedCompletely = GraphDBUtil.checkGraphMatchingCompleteness(existingResourceDB, "existing resource"); final boolean isNewResourceMatchedCompletely = GraphDBUtil.checkGraphMatchingCompleteness(newResourceDB, "new resource"); if (!isExistingResourceMatchedCompletely && !isNewResourceMatchedCompletely) { throw new DMPGraphException("existing and new resource weren't matched completely by the delta algo"); } else { if (!isExistingResourceMatchedCompletely) { throw new DMPGraphException("existing resource wasn't matched completely by the delta algo"); } if (!isNewResourceMatchedCompletely) { throw new DMPGraphException("new resource wasn't matched completely by the delta algo"); } } // traverse resource graphs to extract changeset final PropertyGraphDeltaGDMSubGraphWorker addedStatementsPGDGDMSGWorker = new PropertyGraphDeltaGDMSubGraphWorker(prefixedNewResourceURI, DeltaState.ADDITION, newResourceDB, namespaceIndex); final Map<Long, Statement> addedStatements = addedStatementsPGDGDMSGWorker.work(); final PropertyGraphDeltaGDMSubGraphWorker removedStatementsPGDGDMSGWorker = new PropertyGraphDeltaGDMSubGraphWorker( prefixedExistingResourceURI, DeltaState.DELETION, existingResourceDB, namespaceIndex); final Map<Long, Statement> removedStatements = removedStatementsPGDGDMSGWorker.work(); final PropertyGraphDeltaGDMSubGraphWorker newModifiedStatementsPGDGDMSGWorker = new PropertyGraphDeltaGDMSubGraphWorker( prefixedNewResourceURI, DeltaState.MODIFICATION, newResourceDB, namespaceIndex); final Map<Long, Statement> newModifiedStatements = newModifiedStatementsPGDGDMSGWorker.work(); final PropertyGraphDeltaGDMSubGraphWorker existingModifiedStatementsPGDGDMSGWorker = new PropertyGraphDeltaGDMSubGraphWorker( prefixedExistingResourceURI, DeltaState.MODIFICATION, existingResourceDB, namespaceIndex); final Map<Long, Statement> existingModifiedStatements = existingModifiedStatementsPGDGDMSGWorker.work(); for (final Map.Entry<ValueEntity, ValueEntity> firstDegreeModificationGDMValueModificationEntry : firstDegreeModificationGDMValueMatcher .getModifications().entrySet()) { changesetModifications.put(firstDegreeModificationGDMValueModificationEntry.getKey().getNodeId(), firstDegreeModificationGDMValueModificationEntry.getValue().getNodeId()); } final Map<Long, Statement> preparedExistingModifiedStatements = ChangesetUtil.providedModifiedStatements(existingModifiedStatements); final Map<Long, Statement> preparedNewModifiedStatements = ChangesetUtil.providedModifiedStatements(newModifiedStatements); // return a changeset model (i.e. with information for add, delete, update per triple) return new Changeset(addedStatements, removedStatements, changesetModifications, preparedExistingModifiedStatements, preparedNewModifiedStatements); } private GraphDatabaseService loadResource(final Resource resource, final String impermanentGraphDatabaseDir, final NamespaceIndex namespaceIndex) throws DMPGraphException { // TODO: find proper graph database settings to hold everything in-memory only final GraphDatabaseService impermanentDB = impermanentGraphDatabaseFactory.newImpermanentDatabaseBuilder(new File(impermanentGraphDatabaseDir)).newGraphDatabase(); SchemaIndexUtils.createSchemaIndices(impermanentDB, impermanentGraphDatabaseDir); // TODO: implement handler that enriches the GDM resource with useful information for changeset detection final GDMHandler handler = new Neo4jDeltaGDMHandler(impermanentDB, namespaceIndex); final GDMParser parser = new GDMResourceParser(resource); parser.setGDMHandler(handler); parser.parse(); LOG.debug("added '{}' statements ('{}' relationships; '{}' nodes; '{}' literals) to impermanent delta graph DB '{}'", handler.getCountedStatements(), handler.getRelationshipsAdded(), handler.getNodesAdded(), handler.getCountedLiterals(), impermanentGraphDatabaseDir); return impermanentDB; } private void enrichModel(final GraphDatabaseService graphDB, final NamespaceIndex namespaceIndex, final String prefixedResourceURI, final long resourceHash) throws DMPGraphException { final GDMWorker worker = new PropertyEnrichGDMWorker(prefixedResourceURI, resourceHash, graphDB, namespaceIndex); worker.work(); } private void shutDownDeltaDBs(final GraphDatabaseService existingResourceDB, final GraphDatabaseService newResourceDB) { GDMResource.LOG.debug("start shutting down working graph data model DBs for resources"); // should probably be delegated to a background worker thread, since it looks like that shutting down the working graph // DBs take some time (for whatever reason) final ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10)); service.submit((Callable<Void>) () -> { newResourceDB.shutdown(); existingResourceDB.shutdown(); return null; }); GDMResource.LOG.debug("finished shutting down working graph data model DBs for resources"); } private void shutDownDeltaDB(final GraphDatabaseService resourceDB) { GDMResource.LOG.debug("start shutting down working graph data model DB for resource"); // should probably be delegated to a background worker thread, since it looks like that shutting down the working graph // DBs take some time (for whatever reason) final ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10)); service.submit((Callable<Void>) () -> { resourceDB.shutdown(); return null; }); GDMResource.LOG.debug("finished shutting down working graph data model DB for resource"); } private Observable<Boolean> deprecateMissingRecords(final Observable<Long> processedResources, final String recordClassUri, final String dataModelUri, final int latestVersion, final GDMNeo4jProcessor processor) throws DMPGraphException { return processedResources.toList().map(processedResourcesSet -> { // determine all record URIs of the data model // how? - via record class? try { processor.getProcessor().ensureRunningTx(); final Label recordClassLabel = DynamicLabel.label(recordClassUri); final ResourceIterator<Node> recordNodes = processor.getProcessor().getDatabase() .findNodes(recordClassLabel, GraphStatics.DATA_MODEL_PROPERTY, dataModelUri); if (recordNodes == null) { GDMResource.LOG.debug("finished read data model record nodes TX successfully"); return Boolean.FALSE; } final Set<Node> notProcessedResources = new HashSet<>(); while (recordNodes.hasNext()) { final Node recordNode = recordNodes.next(); final Long resourceHash = (Long) recordNode.getProperty(GraphStatics.HASH, null); if (resourceHash == null) { LOG.debug("there is no resource hash at record node '{}'", recordNode.getId()); continue; } if (!processedResourcesSet.contains(resourceHash)) { notProcessedResources.add(recordNode); // TODO: do we also need to deprecate the record nodes themselves? } } for (final Node notProcessedResource : notProcessedResources) { final Iterable<org.neo4j.graphdb.Path> notProcessedResourcePaths = GraphDBUtil.getResourcePaths(processor.getProcessor() .getDatabase(), notProcessedResource); if (notProcessedResourcePaths == null) { continue; } for (final org.neo4j.graphdb.Path notProcessedResourcePath : notProcessedResourcePaths) { final Iterable<Relationship> rels = notProcessedResourcePath.relationships(); if (rels == null) { continue; } for (final Relationship rel : rels) { rel.setProperty(VersioningStatics.VALID_TO_PROPERTY, latestVersion); } } } recordNodes.close(); return Boolean.TRUE; } catch (final Exception e) { final String message = "couldn't determine record URIs of the data model successfully"; processor.getProcessor().failTx(); GDMResource.LOG.error(message, e); throw new RuntimeException(message, e); } }); } private List<BodyPart> getBodyParts(final MultiPart multiPart) throws DMPGraphException { if (multiPart == null) { final String message = "couldn't write GDM, no multipart payload available"; GDMResource.LOG.error(message); throw new DMPGraphException(message); } final List<BodyPart> bodyParts = multiPart.getBodyParts(); if (bodyParts == null || bodyParts.isEmpty()) { final String message = "couldn't write GDM, no body parts available"; GDMResource.LOG.error(message); throw new DMPGraphException(message); } if (bodyParts.size() < 2) { final String message = "couldn't write GDM, there must be a content and a metadata body part"; GDMResource.LOG.error(message); throw new DMPGraphException(message); } return bodyParts; } private InputStream getContent(final List<BodyPart> bodyParts) throws DMPGraphException { final BodyPart contentBodyPart = bodyParts.get(CONTENT_BODY_PART); if (contentBodyPart == null) { final String message = "couldn't write GDM, no content part available"; GDMResource.LOG.error(message); throw new DMPGraphException(message); } final BodyPartEntity bpe = (BodyPartEntity) contentBodyPart.getEntity(); if (bpe == null) { final String message = "couldn't write GDM, no content part entity available"; GDMResource.LOG.error(message); throw new DMPGraphException(message); } final InputStream gdmInputStream = bpe.getInputStream(); if (gdmInputStream == null) { final String message = "input stream for write to graph DB request is null"; GDMResource.LOG.error(message); throw new DMPGraphException(message); } return gdmInputStream; } private ObjectNode getMetadata(final List<BodyPart> bodyParts) throws DMPGraphException { final BodyPart metadataBodyPart = bodyParts.get(METADATA_BODY_PART); if (metadataBodyPart == null) { final String message = "couldn't write GDM, no metadata part available"; GDMResource.LOG.error(message); throw new DMPGraphException(message); } final String metadataString = metadataBodyPart.getEntityAs(String.class); if (metadataString == null) { final String message = "couldn't write GDM, no metadata entity part available"; GDMResource.LOG.error(message); throw new DMPGraphException(message); } if (LOG.isDebugEnabled()) { LOG.debug("metadata of request '{}'", metadataString); } try { return objectMapper.readValue(metadataString, ObjectNode.class); } catch (final IOException e) { final String message = String.format("couldn't write GDM, couldn't deserialize metadata part '%s'", metadataString); GDMResource.LOG.error(message); throw new DMPGraphException(message, e); } } private Tuple<Observable<Resource>, BufferedInputStream> getModel(final InputStream content) { final BufferedInputStream bis = new BufferedInputStream(content, 1024); final ModelParser modelParser = new ModelParser(bis); return Tuple.tuple(modelParser.parse(), bis); } private Optional<JsonNode> getMetadataPartNode(final String property, final ObjectNode metadata, final boolean mandatory) throws DMPGraphException { final JsonNode metadataPartNode = metadata.get(property); if (metadataPartNode == null) { final String message; if (mandatory) { message = "couldn't write GDM, mandatory property '" + property + "' is not available in request metadata"; GDMResource.LOG.error(message); throw new DMPGraphException(message); } else { message = "couldn't find obligatory property '" + property + "' in request metadata"; GDMResource.LOG.debug(message); return Optional.empty(); } } return Optional.of(metadataPartNode); } private Optional<String> getMetadataPart(final String property, final ObjectNode metadata, final boolean mandatory) throws DMPGraphException { final Optional<JsonNode> optionalMetadataPartNode = getMetadataPartNode(property, metadata, mandatory); if (!optionalMetadataPartNode.isPresent()) { return Optional.empty(); } final JsonNode metadataPartNode = optionalMetadataPartNode.get(); final String metadataPartValue = metadataPartNode.asText(); if (metadataPartValue == null) { final String message; if (mandatory) { message = "couldn't write GDM, mandatory value for property '" + property + "' is not available in request metadata"; GDMResource.LOG.error(message); throw new DMPGraphException(message); } else { message = "couldn't find obligatory value for property '" + property + "' in request metadata"; GDMResource.LOG.debug(message); return Optional.empty(); } } return Optional.of(metadataPartValue); } private Optional<ContentSchema> getPrefixedContentSchema(final ObjectNode metadata, final NamespaceIndex namespaceIndex) throws DMPGraphException { final Optional<JsonNode> optionalContentSchemaJSON = getMetadataPartNode(DMPStatics.CONTENT_SCHEMA_IDENTIFIER, metadata, false); if (!optionalContentSchemaJSON.isPresent()) { return Optional.empty(); } try { final String contentSchemaJSONString = objectMapper.writeValueAsString(optionalContentSchemaJSON.get()); if (LOG.isDebugEnabled()) { LOG.debug("content schema JSON string '{}'", contentSchemaJSONString); } final ContentSchema contentSchema = objectMapper.readValue(contentSchemaJSONString, ContentSchema.class); if (LOG.isDebugEnabled()) { LOG.debug("try to prefix URIs of content schema '{}'", objectMapper.writeValueAsString(contentSchema)); } final Optional<ContentSchema> contentSchemaOptional = Optional.ofNullable(contentSchema); if (contentSchemaOptional.isPresent()) { return prefixContentSchema(contentSchemaOptional.get(), namespaceIndex); } return contentSchemaOptional; } catch (final IOException e) { final String message = "could not deserialise content schema JSON for write from graph DB request"; GDMResource.LOG.error(message); return Optional.empty(); } } private Optional<ContentSchema> prefixContentSchema(final ContentSchema contentSchema, final NamespaceIndex namespaceIndex) throws DMPGraphException { final Map<AttributePath, AttributePath> prefixedAttributePathMap = new HashMap<>(); final Map<Attribute, Attribute> prefixedAttributeMap = new HashMap<>(); final LinkedList<AttributePath> keyAttributePaths = contentSchema.getKeyAttributePaths(); final LinkedList<AttributePath> prefixedKeyAttributePaths; if (keyAttributePaths != null) { prefixedKeyAttributePaths = new LinkedList<>(); for (final AttributePath keyAttributePath : keyAttributePaths) { final AttributePath prefixedKeyAttributePath = prefixAttributePath(keyAttributePath, prefixedAttributePathMap, prefixedAttributeMap, namespaceIndex); prefixedKeyAttributePaths.add(prefixedKeyAttributePath); } } else { prefixedKeyAttributePaths = null; } final AttributePath recordIdentifierAttributePath = contentSchema.getRecordIdentifierAttributePath(); final AttributePath prefixedRecordIdentifierAttributePath = prefixAttributePath(recordIdentifierAttributePath, prefixedAttributePathMap, prefixedAttributeMap, namespaceIndex); final AttributePath valueAttributePath = contentSchema.getValueAttributePath(); final AttributePath prefixedValueAttributePath; if (valueAttributePath != null) { prefixedValueAttributePath = prefixAttributePath(valueAttributePath, prefixedAttributePathMap, prefixedAttributeMap, namespaceIndex); } else { prefixedValueAttributePath = null; } return Optional.of(new ContentSchema(prefixedRecordIdentifierAttributePath, prefixedKeyAttributePaths, prefixedValueAttributePath)); } private AttributePath prefixAttributePath(final AttributePath attributePath, final NamespaceIndex namespaceIndex) throws DMPGraphException { final Map<AttributePath, AttributePath> prefixedAttributePathMap = new HashMap<>(); final Map<Attribute, Attribute> prefixedAttributeMap = new HashMap<>(); return prefixAttributePath(attributePath, prefixedAttributePathMap, prefixedAttributeMap, namespaceIndex); } private AttributePath prefixAttributePath(final AttributePath attributePath, final Map<AttributePath, AttributePath> prefixedAttributePathMap, final Map<Attribute, Attribute> prefixedAttributeMap, final NamespaceIndex namespaceIndex) throws DMPGraphException { if (!prefixedAttributeMap.containsKey(attributePath)) { final LinkedList<Attribute> attributes = attributePath.getAttributes(); final LinkedList<Attribute> prefixedAttributes = new LinkedList<>(); for (final Attribute attribute : attributes) { final Attribute prefixedAttribute = prefixAttribute(attribute, prefixedAttributeMap, namespaceIndex); prefixedAttributes.add(prefixedAttribute); } final AttributePath prefixedAttributePath = new AttributePath(prefixedAttributes); prefixedAttributePathMap.put(attributePath, prefixedAttributePath); } return prefixedAttributePathMap.get(attributePath); } private Attribute prefixAttribute(final Attribute attribute, final Map<Attribute, Attribute> prefixedAttributeMap, final NamespaceIndex namespaceIndex) throws DMPGraphException { if (!prefixedAttributeMap.containsKey(attribute)) { final String attributeUri = attribute.getUri(); if (attributeUri == null || attributeUri.trim().isEmpty()) { final String message = "attribute URI shouldn't be null or empty"; LOG.error(message); throw new DMPGraphException(message); } final String prefixedAttributeURI = namespaceIndex.createPrefixedURI(attributeUri); final Attribute prefixedAttribute = new Attribute(prefixedAttributeURI); prefixedAttributeMap.put(attribute, prefixedAttribute); } return prefixedAttributeMap.get(attribute); } /** * default = false * * @param metadata * @return * @throws DMPGraphException */ private Optional<Boolean> getDeprecateMissingRecordsFlag(final ObjectNode metadata) throws DMPGraphException { final Optional<String> optionalDeprecateMissingRecords = getMetadataPart(DMPStatics.DEPRECATE_MISSING_RECORDS_IDENTIFIER, metadata, false); final Optional<Boolean> result; if (optionalDeprecateMissingRecords.isPresent()) { result = Optional.ofNullable(Boolean.valueOf(optionalDeprecateMissingRecords.get())); } else { result = Optional.of(Boolean.FALSE); } return result; } /** * default = true * * @param metadata * @return * @throws DMPGraphException */ private Optional<Boolean> getEnableVersioningFlag(final ObjectNode metadata) throws DMPGraphException { final Optional<String> optionalEnableVersioning = getMetadataPart(DMPStatics.ENABLE_VERSIONING_IDENTIFIER, metadata, false); final Optional<Boolean> result; if (optionalEnableVersioning.isPresent()) { result = Optional.ofNullable(Boolean.valueOf(optionalEnableVersioning.get())); } else { result = Optional.of(Boolean.TRUE); } return result; } }