public class BioNetDBConfiguration {

    private String logLevel;
    private String logFile;

    private DatabaseConfiguration database;
    private DownloadProperties download;

    protected static Logger logger = LoggerFactory.getLogger(BioNetDBConfiguration.class);

    public BioNetDBConfiguration() { }

    public BioNetDBConfiguration(DatabaseConfiguration databaseConfiguration) {
        database = databaseConfiguration;

    }

    /*
     * This method attempts to find and load the configuration from installation directory,
     * if not exists then loads JAR storage-configuration.yml.
     */
    @Deprecated
    public static BioNetDBConfiguration load() throws IOException {
        String appHome = System.getProperty("app.home", System.getenv("BIONETDB_HOME"));
        Path path = Paths.get(appHome + "/configuration.yml");
        if (appHome != null && Files.exists(path)) {
            logger.debug("Loading configuration from '{}'", appHome + "/configuration.yml");
            return BioNetDBConfiguration
                    .load(new FileInputStream(new File(appHome + "/configuration.yml")));
        } else {
            logger.debug("Loading configuration from '{}'",
                    BioNetDBConfiguration.class.getClassLoader()
                            .getResourceAsStream("configuration.yml")
                            .toString());
            return BioNetDBConfiguration
                    .load(BioNetDBConfiguration.class.getClassLoader().getResourceAsStream("configuration.yml"));
        }
    }

    public static BioNetDBConfiguration load(InputStream configurationInputStream) throws IOException {
        return load(configurationInputStream, "yaml");
    }

    public static BioNetDBConfiguration load(InputStream configurationInputStream, String format) throws IOException {
        BioNetDBConfiguration bioNetDBConfiguration;
        ObjectMapper objectMapper;
        switch (format) {
            case "json":
                objectMapper = new ObjectMapper();
                bioNetDBConfiguration = objectMapper.readValue(configurationInputStream, BioNetDBConfiguration.class);
                break;
            case "yml":
            case "yaml":
            default:
                objectMapper = new ObjectMapper(new YAMLFactory());
                bioNetDBConfiguration = objectMapper.readValue(configurationInputStream, BioNetDBConfiguration.class);
                break;
        }

        return bioNetDBConfiguration;
    }

    public void serialize(OutputStream configurationOututStream) throws IOException {
        ObjectMapper jsonMapper = new ObjectMapper(new YAMLFactory());
        jsonMapper.writerWithDefaultPrettyPrinter().writeValue(configurationOututStream, this);
    }

//    public DatabaseConfiguration findDatabase(String database) {
//        DatabaseConfiguration databaseConfiguration = null;
//        if (StringUtils.isNotEmpty(database)) {
//            for (DatabaseConfiguration databaseConf : databases) {
//                if (databaseConf.getId().equals(database)) {
//                    databaseConfiguration = databaseConf;
//                    break;
//                }
//            }
//        } else {
//            databaseConfiguration = this.databases.get(0);
//        }
//
//        return databaseConfiguration;
//    }
//
//    public void addDatabase(InputStream configurationInputStream) throws IOException {
//        addDatabase(configurationInputStream, "yaml");
//    }
//
//    public void addDatabase(InputStream configurationInputStream, String format) throws IOException {
//        DatabaseConfiguration databaseConfiguration;
//        ObjectMapper objectMapper;
//        switch (format) {
//            case "json":
//                objectMapper = new ObjectMapper();
//                databaseConfiguration = objectMapper.readValue(configurationInputStream, DatabaseConfiguration.class);
//                break;
//            case "yml":
//            case "yaml":
//            default:
//                objectMapper = new ObjectMapper(new YAMLFactory());
//                databaseConfiguration = objectMapper.readValue(configurationInputStream, DatabaseConfiguration.class);
//                break;
//        }
//
//        if (databaseConfiguration != null) {
//            this.databases.add(databaseConfiguration);
//        }
//    }

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder("BioNetDBConfiguration{");
        sb.append("logLevel='").append(logLevel).append('\'');
        sb.append(", logFile='").append(logFile).append('\'');
        sb.append(", database=").append(database);
        sb.append(", download=").append(download);
        sb.append('}');
        return sb.toString();
    }

    public String getLogLevel() {
        return logLevel;
    }

    public BioNetDBConfiguration setLogLevel(String logLevel) {
        this.logLevel = logLevel;
        return this;
    }

    public String getLogFile() {
        return logFile;
    }

    public BioNetDBConfiguration setLogFile(String logFile) {
        this.logFile = logFile;
        return this;
    }

    public DatabaseConfiguration getDatabase() {
        return database;
    }

    public BioNetDBConfiguration setDatabase(DatabaseConfiguration database) {
        this.database = database;
        return this;
    }

    public DownloadProperties getDownload() {
        return download;
    }

    public BioNetDBConfiguration setDownload(DownloadProperties download) {
        this.download = download;
        return this;
    }

    public static Logger getLogger() {
        return logger;
    }

    public static void setLogger(Logger logger) {
        BioNetDBConfiguration.logger = logger;
    }
} public class DatabaseConfiguration {

//    private String id;
//    private String species;

    private String host;
    private int port;
    private String user;
    private String password;

    /**
     * options parameter defines database-specific parameters.
     */
//    private ObjectMap options;


    public DatabaseConfiguration() {
    }

//    @Deprecated
//    public DatabaseConfiguration(String id, ObjectMap options) {
//        this.id = id;
//        this.options = options;
//    }


    public DatabaseConfiguration(String host, int port, String user, String password) {
        this.host = host;
        this.port = port;
        this.user = user;
        this.password = password;
    }

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder("DatabaseConfiguration{");
        sb.append("host='").append(host).append('\'');
        sb.append(", port=").append(port);
        sb.append(", user='").append(user).append('\'');
        sb.append(", password='").append(password).append('\'');
        sb.append('}');
        return sb.toString();
    }

    public String getHost() {
        return host;
    }

    public DatabaseConfiguration setHost(String host) {
        this.host = host;
        return this;
    }

    public int getPort() {
        return port;
    }

    public DatabaseConfiguration setPort(int port) {
        this.port = port;
        return this;
    }

    public String getUser() {
        return user;
    }

    public DatabaseConfiguration setUser(String user) {
        this.user = user;
        return this;
    }

    public String getPassword() {
        return password;
    }

    public DatabaseConfiguration setPassword(String password) {
        this.password = password;
        return this;
    }
} public class BioNetDBException extends Exception {

    public BioNetDBException(String message) {
        super(message);
    }

    public BioNetDBException(String message, Throwable cause) {
        super(message, cause);
    }

    public BioNetDBException(Throwable cause) {
        super(cause);
    }

} public class BioNetDBResult<T> extends DataResult<T> {

    public BioNetDBResult() {
    }

    public BioNetDBResult(int time, List<Event> events, int numResults, List<T> results, long numMatches) {
        super(time, events, numResults, results, numMatches);
    }

    public BioNetDBResult(int time, List<Event> events, int numResults, List<T> results, long numMatches, ObjectMap attributes) {
        super(time, events, numResults, results, numMatches, attributes);
    }

    public BioNetDBResult(int time, List<Event> events, long numMatches, long numInserted, long numUpdated, long numDeleted) {
        super(time, events, numMatches, numInserted, numUpdated, numDeleted);
    }

    public BioNetDBResult(int time, List<Event> events, long numMatches, long numInserted, long numUpdated, long numDeleted,
                          ObjectMap attributes) {
        super(time, events, numMatches, numInserted, numUpdated, numDeleted, attributes);
    }

    public BioNetDBResult(int time, List<Event> events, int numResults, List<T> results, long numMatches, long numInserted, long numUpdated,
                          long numDeleted, ObjectMap attributes) {
        super(time, events, numResults, results, numMatches, numInserted, numUpdated, numDeleted, attributes);
    }


    public BioNetDBResult(DataResult<T> result) {
        this(result.getTime(), result.getEvents(), result.getNumResults(), result.getResults(), result.getNumMatches(),
                result.getNumInserted(), result.getNumUpdated(), result.getNumDeleted(), result.getAttributes());
    }

    @Deprecated
    public static BioNetDBResult empty() {
        return new BioNetDBResult<>(0, new ArrayList<>(), 0, new ArrayList<>(), 0, 0, 0, 0, new ObjectMap());
    }

    public static <T> BioNetDBResult<T> empty(Class<T> c) {
        return new BioNetDBResult<T>(0, new ArrayList<>(), 0, new ArrayList<>(), 0, 0, 0, 0, new ObjectMap())
                .setResultType(c.getCanonicalName());
    }

    public static <T> BioNetDBResult<T> merge(List<BioNetDBResult<T>> results) {
        BioNetDBResult<T> result = new BioNetDBResult<T>(
                results.stream().map(BioNetDBResult::getTime).reduce(0, Integer::sum),
                results.stream().map(BioNetDBResult::getEvents).flatMap(Collection::stream).collect(Collectors.toList()),
                results.stream().map(BioNetDBResult::getNumResults).reduce(0, Integer::sum),
                results.stream().map(BioNetDBResult::getResults).flatMap(Collection::stream).collect(Collectors.toList()),
                results.stream().map(BioNetDBResult::getNumMatches).reduce(0L, Long::sum),
                results.stream().map(BioNetDBResult::getNumInserted).reduce(0L, Long::sum),
                results.stream().map(BioNetDBResult::getNumUpdated).reduce(0L, Long::sum),
                results.stream().map(BioNetDBResult::getNumDeleted).reduce(0L, Long::sum),
                new ObjectMap());

        return result;
    }

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder("BioNetDBResult{");
        sb.append(", time=").append(time);
        sb.append(", events=").append(events);
        sb.append(", numResults=").append(numResults);
        sb.append(", results=").append(results);
        sb.append(", resultType='").append(resultType).append('\'');
        sb.append(", numMatches=").append(numMatches);
        sb.append(", numInserted=").append(numInserted);
        sb.append(", numUpdated=").append(numUpdated);
        sb.append(", numDeleted=").append(numDeleted);
        sb.append(", attributes=").append(attributes);
        sb.append('}');
        return sb.toString();
    }

    public int getTime() {
        return time;
    }

    public BioNetDBResult<T> setTime(int time) {
        this.time = time;
        return this;
    }

    public List<Event> getEvents() {
        return events;
    }

    public BioNetDBResult<T> setEvents(List<Event> events) {
        this.events = events;
        return this;
    }

    public BioNetDBResult<T> addEvent(Event event) {
        if (events == null) {
            events = new ArrayList<>();
        } else {
            try {
                events.add(event);
            } catch (UnsupportedOperationException e) {
                events = new ArrayList<>(events);
                events.add(event);
            }
        }
        return this;
    }

    public int getNumResults() {
        return numResults;
    }

    public BioNetDBResult<T> setNumResults(int numResults) {
        this.numResults = numResults;
        return this;
    }

    public List<T> getResults() {
        return results;
    }

    public BioNetDBResult<T> setResults(List<T> results) {
        this.results = results;
        this.setNumResults(results.size());
        return this;
    }

    @Deprecated
    public long getNumTotalResults() {
        return numMatches;
    }

    @Deprecated
    public BioNetDBResult<T> setNumTotalResults(long numTotalResults) {
        this.numMatches = numTotalResults;
        return this;
    }

    public long getNumMatches() {
        return numMatches;
    }

    public BioNetDBResult<T> setNumMatches(long numMatches) {
        this.numMatches = numMatches;
        return this;
    }

    public long getNumInserted() {
        return numInserted;
    }

    public BioNetDBResult<T> setNumInserted(long numInserted) {
        this.numInserted = numInserted;
        return this;
    }

    public long getNumUpdated() {
        return numUpdated;
    }

    public BioNetDBResult<T> setNumUpdated(long numUpdated) {
        this.numUpdated = numUpdated;
        return this;
    }

    public long getNumDeleted() {
        return numDeleted;
    }

    public BioNetDBResult<T> setNumDeleted(long numDeleted) {
        this.numDeleted = numDeleted;
        return this;
    }

    public String getResultType() {
        return resultType;
    }

    public BioNetDBResult<T> setResultType(String resultType) {
        this.resultType = resultType;
        return this;
    }

    public ObjectMap getAttributes() {
        return attributes;
    }

    public BioNetDBResult<T> setAttributes(ObjectMap attributes) {
        this.attributes = attributes;
        return this;
    }
} public interface NetworkDBAdaptor extends AutoCloseable {

    enum NetworkQueryParams implements QueryParam {
        NODE_TYPE("node.type", TEXT_ARRAY, ""),   // This is PHYSICAL_ENTITY, INTERACTION, XREF, ...
        NODE_UID("node.uid", STRING, ""),
        NODE_ID("node.id", TEXT_ARRAY, ""),

        SRC_NODE("src-node", TEXT_ARRAY, ""),
        DEST_NODE("dest-node", TEXT_ARRAY, ""),
        INTERM_NODE("interm-node", TEXT_ARRAY, ""),
        MAX_JUMPS("max-jumps", INTEGER, ""),
        OUTPUT("output", STRING, ""),

        PE_ID ("pe.id", TEXT_ARRAY, ""),
        PE_DESCRIPTION ("pe.description", TEXT_ARRAY, ""),
        REL_TYPE("rel.type", TEXT_ARRAY, ""),
        PE_ATTR_EXPR ("pe.attr.expr", TEXT_ARRAY, ""),  // example: "brain:t2>0.3;brain:t4<=0.3"
        PE_ONTOLOGY ("pe.ontology", TEXT_ARRAY, ""),  // example: "go:001234, go:002345"
        PE_CELLOCATION ("pe.cellularLocation", TEXT_ARRAY, ""), // example: "nucleoplasm,..."
        JUMPS("jumps", INTEGER, ""),
        SCRIPT ("script",  STRING, ""),

        ID("id", TEXT_ARRAY, ""),
        LABEL("label", TEXT_ARRAY, "");

        NetworkQueryParams(String key, Type type, String description) {
            this.key = key;
            this.type = type;
            this.description = description;
        }

        private final String key;
        private Type type;
        private String description;

        @Override public String key() {
            return key;
        }

        @Override public String description() {
            return description;
        }

        @Override public Type type() {
            return type;
        }
    }

    //-------------------------------------------------------------------------
    // I N D E X
    //-------------------------------------------------------------------------

    void index();
    void close();

    //-------------------------------------------------------------------------
    // I N S E R T     N E T W O R K S
    //-------------------------------------------------------------------------

    void insert(Network network, QueryOptions queryOptions) throws BioNetDBException;

    //-------------------------------------------------------------------------
    // A N N O T A T I O N     M E T H O D s
    //-------------------------------------------------------------------------

//    void annotateVariants(NodeQuery query, QueryOptions options, VariantClient variantClient) throws BioNetDBException, IOException;
//    void annotateVariants(List<String> variantIds, VariantClient variantClient) throws BioNetDBException, IOException;
//
//    void annotateGenes(NodeQuery query, QueryOptions options, GeneClient geneClient) throws BioNetDBException, IOException;
//    void annotateGenes(List<String> geneIds, GeneClient geneClient) throws BioNetDBException, IOException;
//
//    void annotateProteins(NodeQuery query, QueryOptions options, ProteinClient proteinClient) throws BioNetDBException, IOException;
//    void annotateProteins(List<String> proteinIds, ProteinClient proteinClient) throws BioNetDBException, IOException;

    //========================================================================
    // N E T W O R K
    //========================================================================

    //-------------------------------------------------------------------------
    // N O D E S
    //-------------------------------------------------------------------------

    NodeIterator nodeIterator(Query query, QueryOptions queryOptions);
    NodeIterator nodeIterator(String cypher);

    BioNetDBResult<Node> nodeQuery(Query query, QueryOptions queryOptions);
    BioNetDBResult<Node> nodeQuery(String cypher);

    BioNetDBResult<NodeStats> nodeStats(Query query);

    //-------------------------------------------------------------------------

    long addNode(Node node) throws BioNetDBException;
    void updateNode(Node node) throws BioNetDBException;
    void deleteNode(Node node) throws BioNetDBException;
    boolean existNode(Node node) throws BioNetDBException;

    void addRelation(Node origNode, Node destNode, Relation relation) throws BioNetDBException;
    void updateRelation(Node origNode, Node destNode, Relation relation) throws BioNetDBException;
    void deleteRelation(Node origNode, Node destNode, Relation relation) throws BioNetDBException;
    boolean existRelation(Node origNode, Node destNode, Relation relation) throws BioNetDBException;

    //-------------------------------------------------------------------------
    // T A B L E S / R O W S
    //   - a table is modeled as a list of rows, and
    //   - a row is modeled as a list of Object
    //-------------------------------------------------------------------------

//    RowIterator rowIterator(Query query, QueryOptions queryOptions) throws BioNetDBException;
//    RowIterator rowIterator(String cypher) throws BioNetDBException;
//
//    DataResult<List<Object>> rowQuery(Query query, QueryOptions queryOptions) throws BioNetDBException;
//    DataResult<List<Object>> rowQuery(String cypher) throws BioNetDBException;


    //-------------------------------------------------------------------------
    // N E T W O R K     P A T H S
    //   - a network path is modeled as a 'lineal' network, it contains
    //     the origin node and the destionation node
    //-------------------------------------------------------------------------

    NetworkPathIterator networkPathIterator(Query networkPathQuery, QueryOptions queryOptions) throws BioNetDBException;
    NetworkPathIterator networkPathIterator(String cypher);

    BioNetDBResult<NetworkPath> networkPathQuery(Query query, QueryOptions queryOptions) throws BioNetDBException;
    BioNetDBResult<NetworkPath> networkPathQuery(String cypher);

    //-------------------------------------------------------------------------
    // N E T W O R K S
    //-------------------------------------------------------------------------

    BioNetDBResult<NetworkStats> networkStats();

//    DataResult<Network> networkQuery(List<NodeQuery> nodeQueries, QueryOptions queryOptions) throws BioNetDBException;
//    DataResult<Network> networkQueryByPaths(List<NetworkPathQuery> pathQueries, QueryOptions queryOptions) throws BioNetDBException;
//    DataResult<Network> networkQuery(String cypher) throws BioNetDBException;

    //========================================================================
    // V A R I A N T
    //========================================================================

//    VariantIterator variantIterator(Query query, QueryOptions queryOptions) throws BioNetDBException;
//    VariantIterator variantIterator(String cypher) throws BioNetDBException;
//
//    DataResult<Variant> variantQuery(Query query, QueryOptions queryOptions) throws BioNetDBException;
//    DataResult<Variant> variantQuery(String cypher) throws BioNetDBException;

    //========================================================================
    // I N T E R P R E T A T I O N     A N A L Y S I S
    //========================================================================

//    DataResult<Variant> proteinNetworkInterpretationAnalysis(boolean complexOrReaction, Query query) throws BioNetDBException;
} public interface NetworkPathIterator extends Iterator<NetworkPath> {

//    @Override
//    public boolean hasNext() {
//        return hasNext();
//    }
//
//    @Override
//    public T next() {
//        return next();
//    }
} public interface NodeIterator extends Iterator<Node> {

//    @Override
//    public boolean hasNext() {
//        return hasNext();
//    }
//
//    @Override
//    public T next() {
//        return next();
//    }
} public class Neo4JNetworkPathIterator implements NetworkPathIterator {
    private Result result;
    private List<NetworkPath> buffer;

    public Neo4JNetworkPathIterator(Result result) {
        this.result = result;
        this.buffer = new ArrayList<>();
    }

    @Override
    public boolean hasNext() {
        if (!buffer.isEmpty()) {
            return true;
        } else {
            if (result.hasNext()) {
                buffer = Neo4jConverter.toPathList(result.next());
            }
            return !buffer.isEmpty();
        }
    }

    @Override
    public NetworkPath next() {
        return buffer.remove(0);
    }
} public class Neo4JNodeIterator implements NodeIterator {
    private Result result;
    private List<Node> buffer;

    public Neo4JNodeIterator(Result result) {
        this.result = result;
        this.buffer = new ArrayList<>();
    }

    @Override
    public boolean hasNext() {
        if (!buffer.isEmpty()) {
            return true;
        } else {
            if (result.hasNext()) {
                buffer = Neo4jConverter.toNodeList(result.next());
            }
            return !buffer.isEmpty();
        }
    }

    @Override
    public Node next() {
        return buffer.remove(0);
    }

} public class Neo4JQueryParser {

    //public static final Pattern operationPattern = Pattern.compile("^()(<=?|>=?|!=|!?=?~|==?)([^=<>~!]+.*)$");

    private static final Set<String> GENE_PSEUDO_ATTRS = new HashSet<>(Arrays.asList("drug", "hpo", "go"));
    private static final Set<String> VARIANT_PSEUDO_ATTRS = new HashSet<>(Arrays.asList("popFreq", "so"));

    public static class CypherStatement {
        private String match;
        private String where;
        private String with;

        public CypherStatement() {
        }

        public CypherStatement(String match, String where, String with) {
            this.match = match;
            this.where = where;
            this.with = with;
        }

        public String getMatch() {
            return match;
        }

        public void setMatch(String match) {
            this.match = match;
        }

        public String getWhere() {
            return where;
        }

        public void setWhere(String where) {
            this.where = where;
        }

        public String getWith() {
            return with;
        }

        public void setWith(String with) {
            this.with = with;
        }
    }

    public static String parseNodeQuery(Query query, QueryOptions options) {
        String nameNode = "n";

        // Match clause
        StringBuilder match = new StringBuilder();
        match.append("MATCH (").append(nameNode);
        if (StringUtils.isNotEmpty(query.getString("type"))) {
            match.append(":").append(query.getString("type"));
        }
        match.append(")");

        // Where clauses, parse attributes and relationships
        StringBuilder where = new StringBuilder();
        List<String> filters = getFilters(nameNode, query);
        if (filters.size() > 0) {
            where.append(" WHERE ").append(StringUtils.join(filters, " AND "));
        }

//        // Parse pseudo-attributes
//        if (query.getLabels() == Node.Label.GENE) {
//            where.append(parseGeneNode(query, options));
//        } else if (query.getLabels() == Node.Label.VARIANT) {
//            where.append(parseVariantNode(query, options));
//        }

        // Return clauses
        StringBuilder ret = new StringBuilder();
        if (options.containsKey(QueryOptions.INCLUDE)) {
            List<String> includes = options.getAsStringList(QueryOptions.INCLUDE);
            ret.append(" RETURN ").append(nameNode).append(".").append(includes.get(0));
            for (int i = 1; i < includes.size(); i++) {
                ret.append(", ").append(nameNode).append(".").append(includes.get(i));
            }
        } else {
            ret.append(" RETURN ").append(nameNode);
        }

        if (options.containsKey(QueryOptions.LIMIT)) {
            ret.append(" LIMIT ").append(options.getString(QueryOptions.LIMIT));
        }

        // Build the complete Cypher statement
        StringBuilder cypher = new StringBuilder();
        cypher.append(match).append(where).append(ret);

        return cypher.toString();
    }

    public static List<String> getFilters(String nodeName, Query query) {
        List<String> filters = new ArrayList<>();

        addFilters("uid", nodeName, query, filters);
        addFilters("id", nodeName, query, filters);
        addFilters("name", nodeName, query, filters);
        addFilters("label", nodeName, query, filters);
        addFilters("source", nodeName, query, filters);
        addFilters("attribute", nodeName, query, filters);

        return filters;
    }

    public static String parseNetworkPathQuery(Query query, QueryOptions options) throws BioNetDBException {
        String origNodeName = "n1";
        String destNodeName = "n2";
        String origLabel = query.containsKey("origin_label") ? query.getString("origin_label") : "";
        String destLabel = query.containsKey("destination_label") ? query.getString("destination_label") : "";

        // Max. num. hops
        int maxHops = query.getInt("maxNumHops", 3);

        // Match clause
        StringBuilder match = new StringBuilder();
        match.append("MATCH path=");
        match.append("(").append(origNodeName).append(":").append(origLabel).append(")");
        match.append("-[*..").append(maxHops).append("]-");
        match.append("(").append(destNodeName).append(":").append(destLabel).append(")");

        // Where clauses, parse attributes
        List<String> filters = new ArrayList<>();
        if (query.containsKey("origin_filters")) {
            filters.addAll(getFilters(origNodeName, buildQuery(query.getList("origin_filters"))));
        }

        if (query.containsKey("destination_filters")) {
            filters.addAll(getFilters(destNodeName, buildQuery(query.getList("destination_filters"))));
        }

        StringBuilder where = new StringBuilder();
        if (CollectionUtils.isNotEmpty(filters)) {
            where.append(" WHERE ").append(StringUtils.join(filters, " AND "));
        }

        // Build the complete Cypher statement
        StringBuilder cypher = new StringBuilder();
        cypher.append(match).append(where).append(" RETURN path");

        return cypher.toString();
    }
//
//    public static String parseNodesForNetwork(List<NodeQuery> nodeQueries, QueryOptions options) throws BioNetDBException {
//        List<NetworkPathQuery> pathQueries = new ArrayList<>();
//        int size = nodeQueries.size();
//        for (int i = 0; i < size; i++) {
//            for (int j = i + 1; j < size; j++) {
//                NetworkPathQuery networkPathQuery = new NetworkPathQuery(nodeQueries.get(i), nodeQueries.get(j));
//                pathQueries.add(networkPathQuery);
//            }
//        }
//
//        return parsePathsForNetwork(pathQueries, options);
//    }
//
//    public static String parsePathsForNetwork(List<NetworkPathQuery> pathQueries, QueryOptions options) throws BioNetDBException {
//        StringBuilder cypher = new StringBuilder();
//        if (ListUtils.isNotEmpty(pathQueries)) {
//            cypher.append(parsePath(pathQueries.get(0), options));
//            for (int i = 1; i < pathQueries.size(); i++) {
//                cypher.append(" UNION ").append(parsePath(pathQueries.get(i), options));
//            }
//        }
//        return cypher.toString();
//    }

    //-------------------------------------------------------------------------
    // P R I V A T E     M E T H O D S
    //-------------------------------------------------------------------------

    private static void addFilters(String key, String nodeName, Query query, List<String> filters) {
        if (query.containsKey(key)) {
            String[] values = query.getString(key).split(",");

            StringBuilder filter = new StringBuilder("(");
            switch (key) {
                case "uid":
                    filter.append(nodeName).append(".").append(key).append("=").append(values[0]);
                    break;
                case "label":
                    filter.append(nodeName).append(":").append(values[0]);
                    break;
                case "attribute":
                    String[] fields = values[0].split("=");
                    filter.append(nodeName).append(".").append("attr_").append(fields[0]).append("=\"").append(fields[1]).append("\"");
                    break;
                default:
                    filter.append(nodeName).append(".").append(key).append("=\"").append(values[0]).append("\"");
                    break;
            }
            for (int i = 1; i < values.length; i++) {
                filter.append(" OR ");
                switch (key) {
                    case "uid":
                        filter.append(nodeName).append(".").append(key).append("=").append(values[i]);
                        break;
                    case "label":
                        filter.append(nodeName).append(":").append(values[0]);
                        break;
                    case "attribute":
                        String[] fields = values[i].split("=");
                        filter.append(nodeName).append(".").append("attr_").append(fields[0]).append("=\"").append(fields[1]).append("\"");
                        break;
                    default:
                        filter.append(nodeName).append(".").append(key).append("=\"").append(values[i]).append("\"");
                        break;
                }
            }
            filter.append(")");

            filters.add(filter.toString());
        }
    }

    private static Query buildQuery(List<Object> filters) {
        Query query = new Query();
        for (Object filter: filters) {
            String[] fields = ((String) filter).split("=");
            switch (fields[0]) {
                case "uid":
                    query.put("uid", fields[1]);
                    break;
                case "id":
                    query.put("id", fields[1]);
                    break;
                case "name":
                    query.put("name", fields[1]);
                    break;
                case "label":
                    query.put("label", fields[1]);
                    break;
                case "source":
                    query.put("source", fields[1]);
                    break;
                default:
                    query.put("attr_" + fields[0], fields[1]);
                    break;
            }
        }
        return query;
    }

//    private static String parseGeneNode(NodeQuery query, QueryOptions options) {
//        StringBuilder cypher = new StringBuilder();
//        return cypher.toString();
//    }
//
//    private static String parseVariantNode(NodeQuery query, QueryOptions options) {
//        StringBuilder cypher = new StringBuilder();
//        return cypher.toString();
//    }

    //-------------------------------------------------------------------------
    //-------------------------------------------------------------------------
    //-------------------------------------------------------------------------

    public static String parse(Query query, QueryOptions options) {
        int counter = 1;

        StringBuilder match = new StringBuilder();
        String srcNode = null, destNode = null;

        // Source node
        if (StringUtils.isNotEmpty(query.getString(NetworkDBAdaptor.NetworkQueryParams.SRC_NODE.key()))) {
            srcNode = query.getString(NetworkDBAdaptor.NetworkQueryParams.SRC_NODE.key());
        }

        // Destination node
        if (StringUtils.isNotEmpty(query.getString(NetworkDBAdaptor.NetworkQueryParams.DEST_NODE.key()))) {
            destNode = query.getString(NetworkDBAdaptor.NetworkQueryParams.DEST_NODE.key());
        }

        // Max jumps
        int maxJumps = query.getInt(NetworkDBAdaptor.NetworkQueryParams.MAX_JUMPS.key(), 2);

        // Output
        String output = "network";
        if (StringUtils.isNotEmpty(query.getString(NetworkDBAdaptor.NetworkQueryParams.OUTPUT.key()))) {
            output = query.getString(NetworkDBAdaptor.NetworkQueryParams.OUTPUT.key());
        }

        if (StringUtils.isEmpty(srcNode) || StringUtils.isEmpty(destNode)) {
            String inputNode = (StringUtils.isNotEmpty(srcNode) ? srcNode : destNode);
            if (StringUtils.isNotEmpty(inputNode)) {
                Map<String, String> aliasMap = new HashMap<>();
                List<String> where = new ArrayList<>();

                // Source node
                String[] split = inputNode.split(":");
                String alias = "n" + (counter++);
                String inputLabel = alias + ":" + split[0];
                aliasMap.put(split[0], alias);
                if (split.length > 1) {
                    where.add(alias + "." + split[1]);
                }
                match.append("MATCH path=(").append(inputLabel).append(")");
                if (ListUtils.isNotEmpty(where)) {
                    match.append(" WHERE ").append(StringUtils.join(where, " AND "));
                }
                match.append(getReturnStatment(output, aliasMap));
            }
        } else {
            Map<String, String> aliasMap = new HashMap<>();
            List<String> where = new ArrayList<>();

            // Source node
            String[] split = srcNode.split(":");
            String alias = "n" + (counter++);
            String srcLabel = alias + ":" + split[0];
            aliasMap.put(split[0], alias);
            if (split.length > 1) {
                where.add(alias + "." + split[1]);
            }

            // Dest node
            split = destNode.split(":");
            alias = "n" + (counter++);
            String destLabel = alias + ":" + split[0];
            aliasMap.put(split[0], alias);
            if (split.length > 1) {
                where.add(alias + "." + split[1]);
            }

            match.append("MATCH path=(").append(srcLabel).append(")-[*..").append(maxJumps).append("]-(").append(destLabel).append(")");
            if (ListUtils.isNotEmpty(where)) {
                match.append(" WHERE ").append(StringUtils.join(where, " AND "));
            }
            match.append(getReturnStatment(output, aliasMap));
        }
        return match.toString();
    }

    private static String getReturnStatment(String output, Map<String, String> aliasMap) {
        StringBuilder retStatement = new StringBuilder();
        retStatement.append(" RETURN ");

        String[] split;
        if ("network".equals(output)) {
            // Output as network
            retStatement.append("path");
        } else if (output.contains(".")) {
            // Output as table
            List<String> ret = new ArrayList<>();
            String[] nodes = output.split(",");
            for (int i = 0; i < nodes.length; i++) {
                if (nodes[i].contains(".")) {
                    split = nodes[i].split("\\.");
                    if (StringUtils.isNotEmpty(aliasMap.get(split[0]))) {
                        ret.add(aliasMap.get(split[0]) + "." + split[1]);
                    }
                }
            }
            retStatement.append(StringUtils.join(ret, ", "));
        } else {
            // Output as node
            List<String> ret = new ArrayList<>();
            String[] nodes = output.split(",");
            for (int i = 0; i < nodes.length; i++) {
                if (StringUtils.isNotEmpty(aliasMap.get(nodes[i]))) {
                    ret.add(aliasMap.get(nodes[i]));
                }
            }
            retStatement.append(StringUtils.join(ret, ","));
        }

        return retStatement.toString();
    }

    public static String parse(String nodeName, Query query, QueryOptions options) throws BioNetDBException {
        if (query == null) {
            query = new Query();
        }

        List<String> myMatchList = new ArrayList<>();
        List<String> myWhereClauses = new ArrayList<>();
        StringBuilder cypherQuery = new StringBuilder();

        if (StringUtils.isNotEmpty(query.getString(NetworkDBAdaptor.NetworkQueryParams.NODE_TYPE.key()))) {
            myMatchList.add("(" + nodeName + ":" + query.getString(NetworkDBAdaptor.NetworkQueryParams.NODE_TYPE.key()
                    .replace(',', '|')).toUpperCase() + ")");
        }
//        else {
//            myMatchList.add("(" + nodeName + ")");
//        }

        if (StringUtils.isNotEmpty(query.getString(NetworkDBAdaptor.NetworkQueryParams.NODE_UID.key()))) {
            myWhereClauses.add(nodeName + ".uid=" + query.getString(NetworkDBAdaptor.NetworkQueryParams.NODE_UID.key()));
        }

        if (StringUtils.isNotEmpty(query.getString(NetworkDBAdaptor.NetworkQueryParams.NODE_ID.key()))) {
            myWhereClauses.add(nodeName + ".id=\"" + query.getString(NetworkDBAdaptor.NetworkQueryParams.NODE_ID.key())
                    + "\"");
        }

        if (StringUtils.isNotEmpty(query.getString(NetworkDBAdaptor.NetworkQueryParams.PE_ID.key()))) {
            myMatchList.add("(" + nodeName + ")-[:XREF]->(" + nodeName + "x:XREF)");
            myWhereClauses.add(nodeName + "x.id IN [\"" + query.getString(NetworkDBAdaptor.NetworkQueryParams.PE_ID.key())
                    .replace(",", "\", \"") + "\"]");
        }

        if (StringUtils.isNotEmpty(query.getString(NetworkDBAdaptor.NetworkQueryParams.PE_ONTOLOGY.key()))) {
            myMatchList.add("(" + nodeName + ")-[:ONTOLOGY]->(" + nodeName + "o:ONTOLOGY)");
            myWhereClauses.add(nodeName + "o.id IN [\"" + query.getString(NetworkDBAdaptor.NetworkQueryParams.PE_ONTOLOGY.key())
                    .replace(",", "\", \"") + "\"]");
        }

        if (StringUtils.isNotEmpty(query.getString(NetworkDBAdaptor.NetworkQueryParams.PE_CELLOCATION.key()))) {
            myMatchList.add("(" + nodeName + ")-[:CELLULAR_LOCATION]->(" + nodeName + "c:CELLULAR_LOCATION)");
            myWhereClauses.add(nodeName + "c.name IN [\"" + query.getString(NetworkDBAdaptor.NetworkQueryParams.PE_CELLOCATION.key())
                    .replace(",", "\", \"") + "\"]");
        }

        cypherQuery.append(StringUtils.join(myMatchList, ','));

        if (myWhereClauses.size() > 0) {
            cypherQuery.append(" WHERE ");
            cypherQuery.append(StringUtils.join(myWhereClauses, " AND "));
        }
        return cypherQuery.toString();
    }
} public enum Label {
    INTERNAL_CONNFIG,

    UNDEFINED,

    PHYSICAL_ENTITY,
    TRANSCRIPT,
    ENSEMBL_TRANSCRIPT,
    REFSEQ_TRANSCRIPT,
    EXON,
    ENSEMBL_EXON,
    REFSEQ_EXON,
    PROTEIN,
    PHYSICAL_ENTITY_COMPLEX,
    RNA,
    SMALL_MOLECULE,

    MIRNA,
    MIRNA_MATURE,
    MIRNA_TARGET,

    DNA,
    GENE,
    ENSEMBL_GENE,
    REFSEQ_GENE,
    VARIANT,
    REGULATION_REGION,
    TFBS,

    HGV,

    PANEL_GENE,
    PANEL_VARIANT,
    PANEL_STR,
    PANEL_REGION,

    PROTEIN_ANNOTATION,
    PROTEIN_FEATURE,

    STRUCTURAL_VARIATION,
    BREAKEND,
    BREAKEND_MATE,
    VARIANT_ANNOTATION,
    EVIDENCE_SUBMISSION,
    HERITABLE_TRAIT,
    PROPERTY,
    REPEAT,
    CYTOBAND,
    VARIANT_CONSEQUENCE_TYPE,
    SO_TERM,
    VARIANT_DRUG_INTERACTION,
    VARIANT_POPULATION_FREQUENCY,
    VARIANT_CONSERVATION_SCORE,
    VARIANT_FUNCTIONAL_SCORE,
    PROTEIN_VARIANT_ANNOTATION,
    PROTEIN_SUBSTITUTION_SCORE,
    VARIANT_CLASSIFICATION,

    GENE_ANNOTATION,
    CLINICAL_EVIDENCE,
    GENE_TRAIT_ASSOCIATION,
    GENE_DRUG_INTERACTION,
    DRUG,
    GENE_EXPRESSION,
    ONTOLOGY,
    FEATURE_ONTOLOGY_TERM_ANNOTATION,
    TRANSCRIPT_ANNOTATION_EVIDENCE,

    TRANSCRIPT_CONSTRAINT_SCORE,

    PATHWAY,

    CELLULAR_LOCATION,
    REGULATION,
    CATALYSIS,
    REACTION,
    COMPLEX_ASSEMBLY,
    TRANSPORT,
    INTERACTION,

    VARIANT_FILE,
    VARIANT_FILE_DATA,
    SAMPLE,
    VARIANT_SAMPLE_DATA,
    INDIVIDUAL,
    FAMILY,

    TRANSCRIPT_ANNOTATION_FLAG,
    EXON_OVERLAP,
    PROTEIN_KEYWORD,

    DISEASE_PANEL,
    GENOMIC_FEATURE,

    DISORDER,
    PHENOTYPE,
    ONTOLOGY_TERM,

    ASSEMBLY,

    CUSTOM,

    XREF
} public static final String PREFIX_ATTRIBUTES = "attr_";
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.StopWatch;
import org.neo4j.driver.*;
import org.opencb.bionetdb.core.config.BioNetDBConfiguration;
import org.opencb.bionetdb.core.config.DatabaseConfiguration;
import org.opencb.bionetdb.core.exceptions.BioNetDBException;
import org.opencb.bionetdb.core.models.network.*;
import org.opencb.bionetdb.core.response.BioNetDBResult;
import org.opencb.bionetdb.lib.api.NetworkDBAdaptor;
import org.opencb.bionetdb.lib.api.iterators.NetworkPathIterator;
import org.opencb.bionetdb.lib.api.iterators.NodeIterator;
import org.opencb.bionetdb.lib.db.iterators.Neo4JNetworkPathIterator;
import org.opencb.bionetdb.lib.db.iterators.Neo4JNodeIterator;
import org.opencb.bionetdb.lib.db.query.Neo4JQueryParser;
import org.opencb.commons.datastore.core.ObjectMap;
import org.opencb.commons.datastore.core.Query;
import org.opencb.commons.datastore.core.QueryOptions;
import java.util.*;
import java.util.concurrent.TimeUnit;
import static org.opencb.bionetdb.core.models.network.Node.Label.*;
import static org.opencb.bionetdb.lib.utils.Utils.PREFIX_ATTRIBUTES;
package org.opencb.bionetdb.lib.db;




/**
 * Created by imedina on 05/08/15.
 */
public class Neo4JNetworkDBAdaptor implements NetworkDBAdaptor {

    private Driver driver;
    private BioNetDBConfiguration configuration;

    public Neo4JNetworkDBAdaptor(BioNetDBConfiguration configuration) {
        this.configuration = configuration;

        // configuration class checks if database is null or empty
        DatabaseConfiguration databaseConfiguration = configuration.getDatabase();

        String databaseURI = databaseConfiguration.getHost() + ":" + databaseConfiguration.getPort();
        String user = databaseConfiguration.getUser();
        String password = databaseConfiguration.getPassword();

        driver = GraphDatabase.driver("bolt://" + databaseURI, AuthTokens.basic(user, password));
        Runtime.getRuntime().addShutdownHook(new Thread(() -> driver.close()));

//        // Add configuration node
//        if (!existConfigNode()) {
//            createConfigNode();
//        }
    }

    @Override
    public void close() {
        driver.close();
    }

    @Override
    public void index() {
        Session session = this.driver.session();
        if (session != null) {
            // Create indexes
            session.writeTransaction(tx -> {

                List<String> nodeTypes = new ArrayList<>(Arrays.asList(GENE.name(), ENSEMBL_GENE.name(), REFSEQ_GENE.name(),
                        TRANSCRIPT.name(), ENSEMBL_TRANSCRIPT.name(), REFSEQ_TRANSCRIPT.name(), PROTEIN.name(), VARIANT.name(),
                        SAMPLE.name(), INDIVIDUAL.name(), XREF.name(), DISEASE_PANEL.name()));

                for (String nodeType: nodeTypes) {
                    tx.run("CREATE INDEX IF NOT EXISTS FOR (n:" + nodeType + ") ON (n.uid)");
                    tx.run("CREATE INDEX IF NOT EXISTS FOR (n:" + nodeType + ") ON (n.id)");
                    tx.run("CREATE INDEX IF NOT EXISTS FOR (n:" + nodeType + ") ON (n.name)");
                }
                tx.run("CREATE INDEX IF NOT EXISTS FOR (n:" + Node.Label.CUSTOM.name() + ") ON (n.attr_type)");
                tx.commit();

                return 1;
            });

            // Wait for indexes been created
            session.writeTransaction(tx -> {
                tx.run("CALL db.awaitIndexes(3000000)");
                tx.commit();
                return 1;
            });
            session.close();
        }
    }

    public boolean isReady() {
        Transaction tx = null;
        try {
            tx = driver.session().beginTransaction();
            tx.run("MATCH (n:" + Node.Label.INTERNAL_CONNFIG + "{uid:0}) return n");
            tx.commit();
            return true;
        } catch (Exception e) {
            return false;
        } finally {
            if (tx != null) {
                tx.close();
            }
        }
    }

    //-------------------------------------------------------------------------
    // I N S E R T     N E T W O R K S
    //-------------------------------------------------------------------------

    /**
     * Insert an entire network into the Neo4J database.
     *
     * @param network      Object containing all the nodes and interactions
     * @param queryOptions Optional params
     */
    @Override
    public void insert(Network network, QueryOptions queryOptions) throws BioNetDBException {
        Session session = this.driver.session();

        // First, insert Neo4J nodes
        for (Node node : network.getNodes()) {
            session.writeTransaction(tx -> {
                addNode(node, tx);
                return 1;
            });
        }

        // Second, insert Neo4J relationships
        for (Relation relation : network.getRelations()) {
            session.writeTransaction(tx -> {
                addRelation(relation, tx);
                return 1;
            });
        }

        session.close();
    }

    //-------------------------------------------------------------------------
    // A N N O T A T I O N     M E T H O D s
    //-------------------------------------------------------------------------

//    public void annotateVariants(NodeQuery query, QueryOptions options, VariantClient variantClient) throws BioNetDBException,
//            IOException {
//        NodeIterator nodeIterator = nodeIterator(query, options);
//        List<String> variantIds = new ArrayList<>(1000);
//        while (nodeIterator.hasNext()) {
//            Node variantNode = nodeIterator.next();
//            variantIds.add(variantNode.getId());
//            if (variantIds.size() >= 1000) {
//                annotateVariants(variantIds, variantClient);
//                variantIds.clear();
//            }
//        }
//        if (CollectionUtils.isNotEmpty(variantIds)) {
//            annotateVariants(variantIds, variantClient);
//        }
//    }
//
//    public void annotateVariants(List<String> variantIds, VariantClient variantClient) throws BioNetDBException, IOException {
//        Neo4JVariantLoader variantLoader = new Neo4JVariantLoader(this);
//        CellBaseDataResponse<Variant> entryQueryResponse = variantClient.get(variantIds, QueryOptions.empty());
//        for (CellBaseDataResult<Variant> queryResult : entryQueryResponse.getResponses()) {
//            if (CollectionUtils.isNotEmpty(queryResult.getResults())) {
//                variantLoader.loadVariants(queryResult.getResults());
//            }
//        }
//    }

    //-------------------------------------------------------------------------

//    public void annotateGenes(NodeQuery query, QueryOptions options, GeneClient geneClient) throws BioNetDBException,
//            IOException {
//        NodeIterator nodeIterator = nodeIterator(query, options);
//        List<String> geneIds = new ArrayList<>(1000);
//        while (nodeIterator.hasNext()) {
//            Node geneNode = nodeIterator.next();
//            geneIds.add(geneNode.getId());
//            if (geneIds.size() >= 1000) {
//                annotateGenes(geneIds, geneClient);
//                geneIds.clear();
//            }
//        }
//        if (CollectionUtils.isNotEmpty(geneIds)) {
//            annotateGenes(geneIds, geneClient);
//        }
//    }

//    public void annotateGenes(List<String> geneIds, GeneClient geneClient) throws BioNetDBException, IOException {
//        Neo4JVariantLoader variantLoader = new Neo4JVariantLoader(this);
//        QueryOptions options = new QueryOptions("EXCLUDE", "transcripts.exons,transcripts.cDnaSequence");
//        CellBaseDataResponse<Gene> entryQueryResponse = geneClient.get(geneIds, options);
//        for (CellBaseDataResult<Gene> queryResult : entryQueryResponse.getResponses()) {
//            if (CollectionUtils.isNotEmpty(queryResult.getResults())) {
//                variantLoader.loadGenes(queryResult.getResults());
//            }
//        }
//    }

    //-------------------------------------------------------------------------

//    public void annotateProteins(NodeQuery query, QueryOptions options, ProteinClient proteinClient) throws BioNetDBException,
//            IOException {
//        NodeIterator nodeIterator = nodeIterator(query, options);
//        List<String> proteinIds = new ArrayList<>(1000);
//        while (nodeIterator.hasNext()) {
//            Node geneNode = nodeIterator.next();
//            proteinIds.add(geneNode.getId());
//            if (proteinIds.size() >= 1000) {
//                annotateProteins(proteinIds, proteinClient);
//                proteinIds.clear();
//            }
//        }
//        if (CollectionUtils.isNotEmpty(proteinIds)) {
//            annotateProteins(proteinIds, proteinClient);
//        }
//    }

//    public void annotateProteins(List<String> proteinIds, ProteinClient proteinClient) throws BioNetDBException, IOException {
//        Neo4JVariantLoader variantLoader = new Neo4JVariantLoader(this);
//        QueryOptions options = new QueryOptions("EXCLUDE", "transcripts.exons,transcripts.cDnaSequence");
//        CellBaseDataResponse<Entry> entryQueryResponse = proteinClient.get(proteinIds, options);
//        for (CellBaseDataResult<Entry> responses : entryQueryResponse.getResponses()) {
//            if (CollectionUtils.isNotEmpty(responses.getResults())) {
//                variantLoader.loadProteins(responses.getResults());
//            }
//        }
//    }

//    public void annotateProtein(ProteinClient proteinClient) throws BioNetDBException, IOException {
//        // First, get all proteins from the network
//        Query query = new Query();
//        String cypher = "MATCH path=(p:PROTEIN)-[xr:XREF]->(x:XREF) WHERE toLower(x.attr_source) = \"uniprot\" return path";
//        QueryResult<Network> networkResult = networkQuery(cypher);
//
//        if (ListUtils.isEmpty(networkResult.getResult())) {
//            System.out.println("Network not found!!");
//            return;
//        }
//        Network network = networkResult.getResult().get(0);
//
//        // Get proteins annotations from Cellbase...
//        // ... prepare list of protein id/names from xref/protein nodes
//        List<String> proteinIds = new ArrayList<>();
//        for (Node node: network.getNodes()) {
//            if (node.getLabels() == Node.Label.XREF) {
//                proteinIds.add(node.getId());
//            }
//        }
//
//        // ... finally, call Cellbase service
//        Map<String, Entry> proteinMap = new HashMap<>();
//        QueryResponse<Entry> entryQueryResponse = proteinClient.get(proteinIds, new QueryOptions(QueryOptions.EXCLUDE,
//                "reference,organism,comment,evidence,sequence"));
//        for (QueryResult<Entry> queryResult: entryQueryResponse.getResponse()) {
//            proteinMap.put(queryResult.getId(), queryResult.getResult().get(0));
//        }
//
//        for (String key: proteinMap.keySet()) {
//            System.out.println(key + " -> " + proteinMap.get(key));
//        }
//    }

    //-------------------------------------------------------------------------
    // N O D E     Q U E R I E S
    //-------------------------------------------------------------------------

    @Override
    public NodeIterator nodeIterator(Query query, QueryOptions queryOptions) {
        String cypher = Neo4JQueryParser.parseNodeQuery(query, queryOptions);
        return nodeIterator(cypher);
    }

    @Override
    public NodeIterator nodeIterator(String cypher) {
        Session session = this.driver.session();
        System.out.println("Cypher query: " + cypher);
        return new Neo4JNodeIterator(session.run(cypher));
    }

    @Override
    public BioNetDBResult<Node> nodeQuery(Query query, QueryOptions queryOptions) {