/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
*
* 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 org.apache.streams.neo4j.bolt;
import org.apache.streams.config.ComponentConfigurator;
import org.apache.streams.config.StreamsConfigurator;
import org.apache.streams.core.DatumStatusCounter;
import org.apache.streams.core.StreamsDatum;
import org.apache.streams.core.StreamsPersistReader;
import org.apache.streams.core.StreamsResultSet;
import org.apache.streams.jackson.StreamsJacksonMapper;
import org.apache.streams.neo4j.Neo4jReaderConfiguration;
import org.apache.streams.util.PropertyUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.collect.Queues;
import org.joda.time.DateTime;
import org.neo4j.driver.internal.value.NodeValue;
import org.neo4j.driver.internal.value.RelationshipValue;
import org.neo4j.driver.internal.value.StringValue;
import org.neo4j.driver.v1.Record;
import org.neo4j.driver.v1.Session;
import org.neo4j.driver.v1.StatementResult;
import org.neo4j.driver.v1.Transaction;
import org.neo4j.driver.v1.Value;
import org.neo4j.driver.v1.types.Node;
import org.neo4j.driver.v1.types.Relationship;
import org.neo4j.driver.v1.util.Function;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.math.BigInteger;
import java.util.Map;
import java.util.Optional;
import java.util.Queue;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import javax.annotation.Nullable;
/**
* Neo4jBoltPersistReader reads documents from neo4j.
*/
public class Neo4jBoltPersistReader implements StreamsPersistReader {
public static final String STREAMS_ID = "CassandraPersistReader";
public static final Logger LOGGER = LoggerFactory.getLogger(Neo4jBoltPersistReader.class);
protected volatile Queue<StreamsDatum> persistQueue;
private ObjectMapper mapper = StreamsJacksonMapper.getInstance();
private ExecutorService executor;
private CompletableFuture<Boolean> readerTaskFuture = new CompletableFuture<>();
private Neo4jReaderConfiguration config;
protected Neo4jBoltClient client;
// protected Cluster cluster;
// protected Session session;
//
// protected String keyspace;
// protected String table;
protected StatementResult statementResult;
protected final ReadWriteLock lock = new ReentrantReadWriteLock();
/**
* Neo4jBoltPersistReader constructor - resolves Neo4jReaderConfiguration from JVM 'neo4j'.
*/
public Neo4jBoltPersistReader() {
this.config = new ComponentConfigurator<>(Neo4jReaderConfiguration.class)
.detectConfiguration(StreamsConfigurator.getConfig().getConfig("neo4j"));
}
/**
* Neo4jBoltPersistReader constructor - uses supplied Neo4jReaderConfiguration.
* @param config config
*/
public Neo4jBoltPersistReader(Neo4jReaderConfiguration config) {
this.config = config;
}
/**
* Neo4jBoltPersistReader constructor - uses supplied persistQueue.
* @param persistQueue persistQueue
*/
public Neo4jBoltPersistReader(Queue<StreamsDatum> persistQueue) {
this();
this.persistQueue = persistQueue;
}
public void setPersistQueue(Queue<StreamsDatum> persistQueue) {
this.persistQueue = persistQueue;
}
public Queue<StreamsDatum> getPersistQueue() {
return persistQueue;
}
public void stop() {
}
@Override
public String getId() {
return STREAMS_ID;
}
@Override
public void prepare(Object configurationObject) {
if( configurationObject instanceof Neo4jReaderConfiguration ) {
this.config = (Neo4jReaderConfiguration) configurationObject;
}
this.client = Neo4jBoltClient.getInstance(this.config);
persistQueue = constructQueue();
executor = Executors.newSingleThreadExecutor();
}
@Override
public void cleanUp() {
stop();
}
protected Optional<StreamsDatum> buildDatum(Record record) {
ObjectNode objectNode;
if( record != null ) {
ObjectNode valueJson = null;
Map<String, ObjectNode> valueJsons = record.asMap(neo4jObjectNodeFunction);
if( valueJsons.size() == 1) {
valueJson = valueJsons.get(valueJsons.keySet().iterator().next());
}
objectNode = PropertyUtil.unflattenObjectNode(valueJson, '.');
return Optional.of(new StreamsDatum(objectNode));
}
return Optional.empty();
}
@Override
public StreamsResultSet readAll() {
Session session = null;
String query = config.getQuery();
Map<String, Object> params = mapper.convertValue(config.getParams(), Map.class);
try {
session = client.client().session();
Transaction transaction = session.beginTransaction();
this.statementResult = client.client().session().beginTransaction().run(query, params);
while( statementResult.hasNext()) {
Record record = statementResult.next();
Optional<StreamsDatum> datum = buildDatum(record);
if( datum.isPresent()) {
write(datum.get());
}
}
} catch(Exception ex) {
LOGGER.warn("Exception", ex);
} finally {
if( session != null ) {
session.close();
}
}
return readCurrent();
}
@Override
public void startStream() {
LOGGER.debug("startStream");
Neo4jBoltPersistReaderTask readerTask = new Neo4jBoltPersistReaderTask(this);
CompletableFuture.runAsync(readerTask, executor);
try {
if (readerTaskFuture.get()) {
executor.shutdown();
}
} catch (InterruptedException ex) {
LOGGER.trace("Interrupt", ex);
} catch (ExecutionException ex) {
LOGGER.trace("Execution exception", ex);
}
}
@Override
public StreamsResultSet readCurrent() {
StreamsResultSet current;
try {
lock.writeLock().lock();
current = new StreamsResultSet(persistQueue);
current.setCounter(new DatumStatusCounter());
persistQueue = constructQueue();
} finally {
lock.writeLock().unlock();
}
return current;
}
protected void write(StreamsDatum entry) {
boolean success;
do {
try {
lock.readLock().lock();
success = persistQueue.offer(entry);
Thread.yield();
} finally {
lock.readLock().unlock();
}
}
while (!success);
}
@Override
public StreamsResultSet readNew(BigInteger sequence) {
return null;
}
@Override
public StreamsResultSet readRange(DateTime start, DateTime end) {
return null;
}
@Override
public boolean isRunning() {
return !executor.isTerminated() || !executor.isShutdown();
}
private Queue<StreamsDatum> constructQueue() {
return Queues.synchronizedQueue(new LinkedBlockingQueue<StreamsDatum>(10000));
}
private static String readAllStatement() {
return "MATCH (v:streams)";
}
public class Neo4jBoltPersistReaderTask implements Runnable {
private Neo4jBoltPersistReader reader;
public Neo4jBoltPersistReaderTask(Neo4jBoltPersistReader reader) {
this.reader = reader;
}
@Override
public void run() {
try {
while (reader.statementResult.hasNext()) {
Record record = statementResult.next();
Optional<StreamsDatum> datum = reader.buildDatum(record);
if( datum.isPresent() ) {
reader.write(datum.get());
}
}
} finally {
readerTaskFuture.complete(true);
}
}
}
Function<Value, ObjectNode> neo4jObjectNodeFunction = new Function<Value, ObjectNode>() {
@Nullable
@Override
public ObjectNode apply(@Nullable Value value) {
ObjectNode resultNode = null;
if (value instanceof StringValue) {
StringValue stringValue = (StringValue) value;
String string = stringValue.asLiteralString();
try {
resultNode = mapper.readValue(string, ObjectNode.class);
} catch (IOException ex) {
LOGGER.error("IOException", ex);
}
} else if ( value instanceof NodeValue) {
NodeValue nodeValue = (NodeValue) value;
Node node = nodeValue.asNode();
Map<String, Object> nodeMap = node.asMap();
resultNode = PropertyUtil.unflattenMap(nodeMap, '.');
} else if (value instanceof RelationshipValue) {
RelationshipValue relationshipValue = (RelationshipValue) value;
Relationship relationship = relationshipValue.asRelationship();
Map<String, Object> relationshipMap = relationship.asMap();
resultNode = PropertyUtil.unflattenMap(relationshipMap, '.');
}
return resultNode;
}
};
}