com.chap.memo.memoNodes.MemoNode.java Source code

Java tutorial

Introduction

Here is the source code for com.chap.memo.memoNodes.MemoNode.java

Source

package com.chap.memo.memoNodes;

import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.regex.Pattern;

import com.chap.memo.memoNodes.bus.MemoReadBus;
import com.chap.memo.memoNodes.bus.MemoWriteBus;
import com.chap.memo.memoNodes.model.ArcList;
import com.chap.memo.memoNodes.model.NodeValue;
import com.chap.memo.memoNodes.servlet.JSONTuple;
import com.eaio.uuid.UUID;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableList.Builder;

/**
 * MemoNode is a graph database, designed to run effectively on the Google App Engine datastore. 
 * It features arbitrary length byte values, full history and a self-referencing pattern-matching
 * search facility. 
 * 
 * Copyright: Ludo Stellingwerff, Almende B.V.
 * License:   Apache License Version 2.0
 * @see <a href="http://chap.almende.com/">Part of CHAP</a>
 * 
 * @author Ludo Stellingwerff
 * @author Almende B.V.
 * @version 1.0
 * 
 */
public class MemoNode implements Comparable<MemoNode> {
    public static UUID ROOT = UUID.nilUUID();

    //   private static int existingNodes = 0;
    static final ObjectMapper om = new ObjectMapper();

    private MemoReadBus readBus = MemoReadBus.getBus();
    private MemoWriteBus writeBus = MemoWriteBus.getBus();
    private long lastUpdate = System.currentTimeMillis();

    private UUID uuid;
    private NodeValue value = null;
    private final ArcList parents;
    private final ArcList children;

    /**
     * Makes sure all graph changes are written to the datastore. It is advisable to run this
     * at least at the end of each Servlet call.
     */
    public static void flushDB() {
        MemoWriteBus.getBus().flush();
    }

    /**
     * Dangerous! Empties the entire graph database, irreversible! You will loose data...
     */
    public static void emptyDB() {
        MemoWriteBus.emptyDB();
    }

    /**
     * Recluster the database for faster access. (Run every couple of minutes)
     */
    public static void compactDB() {
        MemoReadBus.getBus().compactDB();
    }

    /**
     * Export the entire node database as a byte array
     */
    public static void exportDB(OutputStream out) {
        MemoReadBus.getBus().exportDB(out, false);
    }

    /**
     * Export and remove historical nodes, leaving only current values 
     */
    public static void purgeHistory(OutputStream out) {
        MemoReadBus.getBus().exportDB(out, true);
    }

    /**
     * Remove historical data, leaving only current values 
     */
    public static void dropHistory() {
        MemoReadBus.getBus().dropHistory();
    }

    /**
     * import the given byte array as subgraph, HOW?
     */
    public static void importDB(InputStream in) {
        MemoWriteBus.getBus().importDB(in);
    }

    /**
     * Get the node that can serve as a tree root, providing at least one anchor for the database.
     * Use sparsely as this node will otherwise get a lot of children, better to use one or more 
     * intermediate nodes.
     * 
     * Root node has UUID: 00000000-0000-0000-0000-000000000000 
     */
    public static MemoNode getRootNode() {
        MemoReadBus readBus = MemoReadBus.getBus();
        MemoNode result = readBus.find(ROOT);
        if (result == null) {
            result = new MemoNode(ROOT, "root");
        }
        //      System.out.println("Root node("+ROOT.toString()+") has time:"+result.getId().time);
        return result;
    }

    @Override
    public int compareTo(MemoNode o) {
        if (this.getId().equals(o.getId()))
            return 0;
        return (int) ((this.getTimestamp() - o.getTimestamp()) % 1);
    }

    @Override
    public int hashCode() {
        return this.getId().hashCode();
    }

    @Override
    public boolean equals(Object o) {
        if (o instanceof MemoNode) {
            return this.getId().equals(((MemoNode) o).getId());
        } else {
            return false;
        }
    }

    /*   protected void finalize(){
       if (existingNodes > 100000) System.out.println("Quite many nodes found!"+existingNodes);
       existingNodes--;
    }*/
    /**
     * Find or create node with specified UUID. This is the recommended way to obtain 
     * existing nodes of which you know the UUID. If node can't be found, this node will have an
     * empty value;
     * 
     * @see <a href="http://johannburkard.de/software/uuid/">http://johannburkard.de/software/uuid/</a>
     */
    public MemoNode(UUID uuid) {
        //      existingNodes++;
        this.uuid = uuid;
        this.parents = new ArcList(uuid, 0);
        this.children = new ArcList(uuid, 1);
    }

    /**
     * Create new node with specified value.
     * 
     */
    public MemoNode(byte[] value) {
        //      existingNodes++;
        this.uuid = new UUID();
        //      if (this.uuid.time < 0) System.out.println("created UUID with time:"+this.uuid.time);
        this.value = writeBus.store(this.uuid, value);
        this.parents = new ArcList(this.uuid, 0);
        this.children = new ArcList(this.uuid, 1);
    }

    /**
     * Create new node with specified string value.
     * 
     */
    public MemoNode(String value) {
        //      existingNodes++;
        this.uuid = new UUID();
        //      if (this.uuid.time < 0) System.out.println("created UUID with time:"+this.uuid.time);
        this.value = writeBus.store(this.uuid, value.getBytes());
        this.parents = new ArcList(this.uuid, 0);
        this.children = new ArcList(this.uuid, 1);
    }

    /**
     * Find or create node with specified UUID and value. If node existed it will be updated to the
     * provided value.
     * 
     * @see <a href="http://johannburkard.de/software/uuid/">http://johannburkard.de/software/uuid/</a>
     */
    public MemoNode(UUID uuid, byte[] value) {
        //      existingNodes++;
        this.uuid = uuid;
        this.parents = new ArcList(this.uuid, 0);
        this.children = new ArcList(this.uuid, 1);
        this.value = writeBus.store(uuid, value);
    }

    /**
     * Find or create node with specified UUID and value. If node existed it will be updated to the
     * provided string value.
     * 
     * @see <a href="http://johannburkard.de/software/uuid/">http://johannburkard.de/software/uuid/</a>
     */
    public MemoNode(UUID uuid, String value) {
        //      existingNodes++;
        this.uuid = uuid;
        this.parents = new ArcList(this.uuid, 0);
        this.children = new ArcList(this.uuid, 1);
        this.value = writeBus.store(uuid, value.getBytes());
    }

    public MemoNode(NodeValue value) {
        //      existingNodes++;
        if (value != null) {
            this.uuid = value.getId();
            this.value = value;
        } else {
            System.out.println("Null value given, generating new UUID!");
            this.uuid = new UUID();
            this.value = null;
        }
        this.parents = new ArcList(this.uuid, 0);
        this.children = new ArcList(this.uuid, 1);
    }

    /**
     * Update node's value to specified value.
     * 
     * @return this node
     */
    public MemoNode update(byte[] value) {
        if (value == null)
            value = new byte[0];
        this.value = writeBus.store(this.getId(), value);
        return this;
    }

    /**
     * Update node's value to specified string value.
     * 
     * @return this node
     */
    public MemoNode update(String value) {
        this.value = writeBus.store(this.getId(), value.getBytes());
        return this;
    }

    /**
     * Add a new parent arc between a node with specified parent UUID and this node, effectively
     * making this node a child of the provided node.
     * 
     * @see <a href="http://johannburkard.de/software/uuid/">http://johannburkard.de/software/uuid/</a>
     */
    public void addParent(UUID parent) {
        parents.addNode(parent);
    }

    /**
     * Add a new child arc between a node with specified child UUID and this node, effectively making
     * the provided node a child of this node.
     * 
     * @see <a href="http://johannburkard.de/software/uuid/">http://johannburkard.de/software/uuid/</a>
     */
    public void addChild(UUID child) {
        children.addNode(child);
    }

    /**
     * Remove the parent arc between the node with specified parent UUID and this node, effectively
     * making this node no longer a child of the provided node.
     * 
     * @see <a href="http://johannburkard.de/software/uuid/">http://johannburkard.de/software/uuid/</a>
     */
    public void delParent(UUID parent) {
        parents.delNode(parent);
    }

    /**
     * Remove the child arc between this node and the node with the provided child UUID, effectively making
     * this node no longer a parent of the provided node.
     * 
     * @see <a href="http://johannburkard.de/software/uuid/">http://johannburkard.de/software/uuid/</a>
     */
    public void delChild(UUID child) {
        children.delNode(child);
    }

    /**
     * Add a new parent arc between the specified parent node and this node, effectively
     * making this node a child of the provided node.
     * 
     * @return parent
     */
    public MemoNode addParent(MemoNode parent) {
        addParent(parent.getId());
        return parent;
    }

    /**
     * Add a new child arc between the specified child node and this node, effectively making
     * the provided node a child of this node.
     * 
     * @return child
     */
    public MemoNode addChild(MemoNode child) {
        addChild(child.getId());
        return child;
    }

    /**
     * Add a new parent arc between the specified parent node and this node, effectively
     * making this node a child of the provided node.
     * 
     * @return this node
     */
    public MemoNode setParent(MemoNode parent) {
        addParent(parent.getId());
        return this;
    }

    /**
     * Add a new child arc between the specified child node and this node, effectively making
     * the provided node a child of this node.
     * 
     * @return this node
     */
    public MemoNode setChild(MemoNode child) {
        addChild(child.getId());
        return this;
    }

    /**
     * Remove the parent arc between the specified parent node and this node, effectively
     * making this node no longer a child of the provided node.
     * 
     * @return this node
     */
    public MemoNode delParent(MemoNode parent) {
        delParent(parent.getId());
        return this;
    }

    /**
     * Remove the child arc between this node and the provided child node, effectively making
     * this node no longer a parent of the provided node.
     * 
     * @return this node
     */
    public MemoNode delChild(MemoNode child) {
        delChild(child.getId());
        return this;
    }

    /**
     * Checks if given node is a direct parent of this node.
     * 
     * @return boolean
     */
    public boolean isChildOf(MemoNode node) {
        return this.getParentIds().contains(node.uuid);
    }

    /**
     * Checks if given node is a direct parent of this node.
     * 
     * @return boolean
     */
    public boolean isParentOf(MemoNode node) {
        //TODO: can be done more efficient?
        return this.getChildIds().contains(node.uuid);
    }

    /**
     * Get current value of node. If node has been deleted, can't be found or has been 
     * updated to a null value, this call returns a zero-size byte[].
     * 
     * @return byte[], zero-size if node not found/empty/null
     */
    public byte[] getValue() {
        if (this.value == null || readBus.valueChanged(lastUpdate)) {
            this.value = readBus.getValue(this.uuid);
            lastUpdate = System.currentTimeMillis();
        }
        return this.value == null ? null : this.value.getValue();
    }

    /**
     * Get current value of node as a string. The returned string is assuming an ASCII charset, for
     * special chars use "new String(getValue())" instead.
     * If node has been deleted, can't be found or has been updated to a null value,
     * this call returns an empty string.
     * 
     * @return String
     */
    public String getStringValue() {
        byte[] bytes = getValue();
        if (bytes == null)
            return "";
        char[] buffer = new char[bytes.length];
        for (int i = 0; i < buffer.length; i++) {
            buffer[i] = (char) bytes[i];
        }
        return new String(buffer);
    }

    public byte[] valueAt(long timestamp) {
        NodeValue oldValue;
        oldValue = readBus.getValue(getId(), timestamp);
        return oldValue.getValue();
    }

    public ArrayList<MemoNode> history() {
        ArrayList<MemoNode> result = readBus.findAll(getId());
        if (!result.get(result.size() - 1).equals(this)) {
            result.add(this);
        }
        return result;
    }

    /**
     * Returns the UUID of this node. 
     * 
     * @see <a href="http://johannburkard.de/software/uuid/">http://johannburkard.de/software/uuid/</a>
     */
    public UUID getId() {
        return this.uuid;
    }

    /**
     * Returns the timestamp of the latest update (or creation) to this node as the amount of microseconds since midnight 1 Jan 1970. 
     * 
     * @return long, amount of microseconds since 1-1-1970 00:00:00.00;
     */
    public long getTimestamp() {
        return Math.max(this.value.getTimestamp_long(),
                Math.max(this.children.getTimestamp_long(), this.parents.getTimestamp_long()));
    }

    /**
     * Returns the list of the uuids of direct parent nodes. 
     * Use this to count the amount of parents, i.s.o. the getParents.
     * (which is way more expensive.) 
     * 
     * @return ImmutableList<UUID> parentIds
     */
    public ImmutableList<UUID> getParentIds() {
        return ImmutableList.copyOf(this.parents.getNodesIds());
    }

    /**
     * Returns the list of direct parent nodes. 
     * 
     * @return ImmutableList<MemoNode> parents
     */
    public ImmutableList<MemoNode> getParents() {
        return this.parents.getNodes();
    }

    /**
     * Returns the list of the uuids of direct child nodes. 
     * Use this to count the amount of children, i.s.o. the getChildren.
     * (which is way more expensive.) 
     * 
     * @return ImmutableList<UUID> childIds
     */
    public ImmutableList<UUID> getChildIds() {
        return ImmutableList.copyOf(this.children.getNodesIds());
    }

    /**
     * Returns the list of direct child nodes. 
     * 
     * @return ImmutableList<MemoNode> children
     */
    public ImmutableList<MemoNode> getChildren() {
        return this.children.getNodes();
    }

    /**
     * Returns all children whose string value equal the given string.
     * 
     * @param value the string value to compare with
     * @param topx return a maximum of topx children (Set to 0 for all matching children)
     * @return ArrayList<MemoNode> children
     */
    public ArrayList<MemoNode> getChildrenByStringValue(String value, int topx) {
        UUID[] childids = this.children.getNodesIds();
        ArrayList<MemoNode> result = new ArrayList<MemoNode>(topx > 0 ? topx : 10);
        for (UUID childid : childids) {
            MemoNode child = new MemoNode(childid);
            if (child.getStringValue().equals(value)) {
                result.add(child);
                if (topx > 0 && result.size() >= topx)
                    return result;
            }
        }
        return result;
    }

    /**
     * Get a single child whose string value equals the given string. If multiple 
     * children are found, return the (arbitrary) first one.
     * 
     * @param value the string value to compare with
     * @return MemoNode child
     */
    public MemoNode getChildByStringValue(String value) {
        ArrayList<MemoNode> children = getChildrenByStringValue(value, 1);
        if (children != null && children.size() > 0) {
            return children.get(0);
        }
        return null;
    }

    /**
     * Returns all children whose string value match the given regular expression.
     * 
     * @see  java.util.regex.Pattern
     * @param regex the regular expression to match against
     * @param topx return a maximum of topx children (Set to 0 for all matching children)
     * @return ArrayList<MemoNode> children
     */
    public ArrayList<MemoNode> getChildrenByRegEx(Pattern regex, int topx) {
        UUID[] childids = this.children.getNodesIds();
        ArrayList<MemoNode> result = new ArrayList<MemoNode>(topx > 0 ? topx : 10);
        for (UUID uuid : childids) {
            MemoNode child = new MemoNode(uuid);
            if (regex.matcher(child.getStringValue()).matches()) {
                result.add(child);
                if (topx > 0 && result.size() >= topx)
                    return result;
            }
        }
        return result;
    }

    /**
     * Returns all children whose integer value falls in the give range.
     * (Currently interprets the string value as an integer, will probably still change in the future?)
     * 
     * @param lower the lower bound of the range, value is included in the range.
     * @param upper the upper bound of the range, value is included in the range.
     * @param topx return a maximum of topx children (Set to 0 for all matching children)
     * @return ArrayList<MemoNode> children
     */
    public ArrayList<MemoNode> getChildrenByRange(int lower, int upper, int topx) {
        //TODO: store integers differently? Not as String...
        UUID[] childids = this.children.getNodesIds();
        ArrayList<MemoNode> result = new ArrayList<MemoNode>(topx > 0 ? topx : 10);
        for (UUID uuid : childids) {
            MemoNode child = new MemoNode(uuid);
            try {
                int value = Integer.parseInt(child.getStringValue());
                if (value >= lower && value <= upper) {
                    result.add(child);
                    if (topx > 0 && result.size() >= topx)
                        return result;
                }
            } catch (NumberFormatException e) {
            }
        }
        return result;
    }

    /**
     * Remove this node.(=setting its value to null and removing all arcs) This method will delete
     * entire subgraphs. Removing large subgraphs can be
     * an expensive operation.
     * 
     */
    public void delete() {
        MemoNode current = this;
        ArrayList<UUID> todo = new ArrayList<UUID>(20);
        while (current != null) {
            UUID[] children = current.children.getNodesIds();
            UUID[] parents = current.parents.getNodesIds();
            current.update((byte[]) null);
            if (children.length > 0) {
                todo.ensureCapacity(todo.size() + children.length);
                todo.addAll(Arrays.asList(children));
                current.children.clear();
            }
            if (parents.length > 0) {
                current.parents.clear();
            }
            if (todo.size() > 0) {
                current = new MemoNode(todo.remove(0));
            } else {
                break;
            }
        }
    }

    /**
     * Convenience method to store a property pattern for this node. This method
     * will store the given propValue as a child of an intermediate propName node, which 
     * will be stored as a child to the current node. (current->propName->propValue)
     * Existing propNames for this node will lead to an update of the value.
     * 
     * @see #getPropertyValue(String propName)
     * @param propName the name of this property, must be unique for this node.
     * @param propValue the (new) value of this property.
     * 
     */
    public MemoNode setPropertyValue(String propName, String propValue) {
        if (propName == null)
            return this;
        if (propValue == null)
            propValue = "";

        ArrayList<MemoNode> properties = getChildrenByStringValue(propName, 1);
        switch (properties.size()) {
        case 0:
            MemoNode value = new MemoNode(propValue.getBytes());
            MemoNode property = new MemoNode(propName.getBytes());
            property.addChild(value.getId());
            this.addChild(property.getId());
            break;
        case 1:
            List<UUID> values = properties.get(0).getChildIds();
            if (values.size() == 1) {
                MemoNode val = new MemoNode(values.get(0));
                if (val.getStringValue().equals(propValue))
                    break;
                val.update(propValue.getBytes());
                break;
            }
            if (values.size() == 0) {
                properties.get(0).addChild(new MemoNode(propValue.getBytes()));
                break;
            }
            //explicit no-break
        default:
            System.out.println("Error, incorrect properties found, repairing! setPropertyValue(" + propName + ","
                    + propValue + ")!");
            for (MemoNode node : properties) {
                node.delete();
            }
            return setPropertyValue(propName, propValue);
        }
        return this;
    }

    /**
     * Convenience method to get a property pattern for this node. 
     * 
     * @see #setPropertyValue(String propName,String propValue)
     * @param propName the name of the property to retrieve
     * 
     */
    public String getPropertyValue(String propName) {
        ArrayList<MemoNode> properties = getChildrenByStringValue(propName, 1);
        if (properties.size() == 1) {
            List<UUID> values = properties.get(0).getChildIds();
            if (values.size() > 1)
                System.out.println("Warning, property with multiple values: " + this.getId() + ":" + propName);
            if (values.size() >= 1)
                return new MemoNode(values.get(0)).getStringValue();
        }
        return "";
    }

    private class StepState {

        private boolean matched = false;

        public StepState(boolean matched, String reason, MemoQuery query, MemoNode toCompare) {
            //System.out.println(toCompare.getValue()+"/"+query.value+" -> returning: "+matched+" reason:"+reason);
            this.setMatched(matched);
        }

        public boolean isMatched() {
            return matched;
        }

        public void setMatched(boolean matched) {
            this.matched = matched;
        }
    }

    private StepState doStep(boolean preamble, MemoQuery query, MemoNode toCompare, ArrayList<UUID> results,
            HashSet<UUID> seenNodes, ArrayList<MemoNode> patterns, int topX, HashMap<String, String> arguments) {

        MemoNode step = query.node;
        //System.out.println("checking node:" + toCompare.getStringValue() + "/" + query.value + "("+preamble+")");

        if (!query.match(toCompare))
            return new StepState(false, "Node doesn't match.", query, toCompare);
        if (seenNodes.contains(toCompare.getId()))
            return new StepState(true, "Loop/Multipath detected", query, toCompare);
        if (preamble) {
            for (MemoNode pattern : patterns) {
                StepState res = doStep(false, MemoQuery.parseQuery(pattern.getChildren().get(0), arguments),
                        toCompare, null, new HashSet<UUID>(), null, 0, arguments);
                if (res.matched) {
                    results.add(toCompare.getId());
                    return new StepState(true, "Node matches pattern! Added to result, no need to search deeper.",
                            query, toCompare);
                }
            }
        }
        seenNodes.add(toCompare.getId());

        List<MemoNode> nextPats = step.getChildren();
        int toMatchNo = nextPats.size();
        if (toMatchNo == 0)
            return new StepState(true, "End of pattern", query, toCompare);

        List<UUID> children = toCompare.getChildIds();
        if (!preamble && children.size() < toMatchNo)
            return new StepState(false, "Too little children for pattern", query, toCompare);

        ArrayList<MemoQuery> queries = new ArrayList<MemoQuery>(toMatchNo);
        HashSet<MemoQuery> foundQueries = new HashSet<MemoQuery>(toMatchNo);
        for (MemoNode nextPat : nextPats) {
            queries.add(MemoQuery.parseQuery(nextPat, arguments));
        }
        MemoQuery[] queryArray = { new MemoQuery() };
        queryArray = queries.toArray(queryArray);
        Arrays.sort(queryArray);

        for (UUID uuid : children) {
            MemoNode child = new MemoNode(uuid);
            for (MemoQuery iQuery : queryArray) {
                if (foundQueries.contains(iQuery))
                    continue;
                StepState res = doStep(preamble, iQuery, child, results, seenNodes, patterns, topX, arguments);

                if (topX > 0 && results.size() >= topX)
                    return new StepState(true, "TopX results reached, returning!", query, toCompare);
                if (preamble || !res.isMatched())
                    continue;
                //Return on fully matched pattern
                foundQueries.add(iQuery);
                if (foundQueries.size() == queryArray.length)
                    return new StepState(true, "Pattern fully matched", query, toCompare);
            }
        }
        if (preamble)
            return new StepState(true, "preamble return.", query, toCompare);
        return new StepState(false, "Pattern didn't match entirely.", query, toCompare);
    }

    /**
     * Search for nodes according to the give Preamble(s) and Pattern(s). 
     * (Full documentation is still on the todo list:) )
     * 
     * @see "MemoTestServlet.java for an example of searching."
     */
    public List<MemoNode> search(ArrayList<MemoNode> preambles, ArrayList<MemoNode> patterns, int topx,
            HashMap<String, String> arguments) {

        ArrayList<UUID> result = new ArrayList<UUID>(topx > 0 ? Math.min(200, topx) : 200);
        HashSet<UUID> seenNodes = new HashSet<UUID>(200);

        if (patterns.size() <= 0) {
            System.out.println("Warning, empty algorithm used (no patterns).");
            return new ArrayList<MemoNode>(0);
        }
        if (preambles.size() <= 0) {
            System.out.println("Warning, empty algorithm used (no preambles).");
            return new ArrayList<MemoNode>(0);
        }

        for (MemoNode preamble : preambles) {
            doStep(true, MemoQuery.parseQuery(preamble.getChildren().get(0), arguments), (MemoNode) this, result,
                    seenNodes, patterns, topx, arguments);
        }
        Builder<MemoNode> resBuilder = ImmutableList.builder();
        for (UUID uuid : result) {
            resBuilder.add(new MemoNode(uuid));
        }
        return resBuilder.build();
    }

    /**
     * Search for nodes according to the give Preamble(s) and Pattern(s). 
     * (Full documentation is still on the todo list:) )
     * 
     * @see "MemoTestServlet.java for an example of searching."
     */
    public List<MemoNode> search(MemoNode algorithm, int topx, HashMap<String, String> arguments) {
        ArrayList<MemoNode> preambles = algorithm.getChildrenByStringValue("PreAmble", -1);
        ArrayList<MemoNode> patterns = algorithm.getChildrenByStringValue("Pattern", -1);
        return this.search(preambles, patterns, topx, arguments);
    }

    /**
     * Search for nodes according to the give Preamble(s) and Pattern(s). 
     * (Full documentation is still on the todo list:) )
     * 
     * @see "MemoTestServlet.java for an example of searching."
     */
    public List<MemoNode> search(MemoNode preamble, MemoNode pattern, int topx, HashMap<String, String> arguments) {
        ArrayList<MemoNode> preambles = new ArrayList<MemoNode>(1);
        preambles.add(preamble);
        ArrayList<MemoNode> patterns = new ArrayList<MemoNode>(1);
        patterns.add(pattern);
        return this.search(preambles, patterns, topx, arguments);
    }

    /**
     * Return the subgraph below this node as a JSON object suitable for the chap network
     * viewer.
     * 
     * @see <a href="http://almende.github.com/chap-links-library/network.html">CHAP network viewer</a>
     * @param depth maximum depth of the subgraph to retrieve. (set to 0 for unlimited =dangerous)
     */
    public String toJSONString(int depth) {
        //TODO: prevent loops
        JSONTuple tuple = new JSONTuple();
        this.toJSON(depth, tuple);
        ObjectNode node = om.createObjectNode();
        node.put("nodes", tuple.getNodes());
        node.put("links", tuple.getLinks());
        return node.toString();
    }

    private void toJSON(int depth, JSONTuple result) {
        if (result.getSeenNodes().contains(this.uuid))
            return;
        result.getSeenNodes().add(this.uuid);
        ObjectNode node = om.createObjectNode();
        node.put("id", this.getId().toString());
        node.put("title", this.getStringValue());

        List<UUID> children = this.getChildIds();
        if (depth-- > 0) {
            for (UUID uuid : children) {
                MemoNode child = new MemoNode(uuid);
                child.toJSON(depth, result);
                result.getLinks().add(om.createObjectNode().put("from", this.getId().toString()).put("to",
                        child.getId().toString()));
            }
        }
        if (depth < 0 && children.size() > 0) {
            node.put("group", "more");
        }
        result.getNodes().add(node);
    }
}

class MemoQuery implements Comparable<MemoQuery> {
    public enum Type {
        Equal, Regex, Range, Any
    };

    static HashMap<MemoNode, MemoQuery> queryCache = new HashMap<MemoNode, MemoQuery>();

    MemoNode node = null;
    Type type = Type.Equal;
    String value = "";
    boolean hasArg = false;
    java.util.regex.Pattern regex = null;
    int lower = 0;
    int upper = 0;

    @Override
    public int compareTo(MemoQuery arg0) {
        return this.type.compareTo(arg0.type);
    }

    public static MemoQuery parseQuery(MemoNode step, HashMap<String, String> arguments) {
        if (queryCache.containsKey(step)) {
            MemoQuery cached = queryCache.get(step);
            if (!cached.hasArg) {
                return cached;
            }
            ;
        }
        MemoQuery result = new MemoQuery();
        result.node = step;
        String query = step.getStringValue();
        if (query.equals("any")) {
            result.type = MemoQuery.Type.Any;
        } else if (query.startsWith("equal;")) {
            result.type = MemoQuery.Type.Equal;
            result.value = query.substring(6);
            if (result.value.startsWith("arg(")) {
                result.value = arguments.get(result.value.substring(4, result.value.length() - 1));
                //System.out.println("Setting arg value to:"+result.value);
                result.hasArg = true;
            }
        } else if (query.startsWith("regex;")) {
            result.type = MemoQuery.Type.Regex;
            result.regex = Pattern.compile(query.substring(6));
        } else if (query.startsWith("range:")) {
            result.type = MemoQuery.Type.Range;
            String[] parts = query.substring(6).split(";");
            result.lower = Integer.parseInt(parts[0]);
            result.upper = Integer.parseInt(parts[0]);
        } else {
            result.type = MemoQuery.Type.Equal;
            result.value = query;
            if (result.value.startsWith("arg(")) {
                result.value = arguments.get(result.value.substring(4, result.value.length() - 1));
                result.hasArg = true;
            }
        }
        if (!result.hasArg)
            queryCache.put(step, result);
        return result;
    }

    public boolean match(MemoNode node) {
        switch (this.type) {
        case Equal:
            //System.out.println("Checking "+node.getId()+":"+node.getStringValue()+" against "+this.value);
            return node.getStringValue().equals(this.value);
        case Regex:
            return this.regex.matcher(node.getStringValue()).matches();
        case Range:
            try {
                int value = Integer.parseInt(node.getStringValue());
                return value >= this.lower && value <= this.upper;
            } catch (Exception e) {
                return false;
            }
        case Any:
            return true;
        default:
            System.out.println("MemoQuery: Unknown enum value???");
            return false;
        }
    }
}