/*
* Copyright (c) 2002-2009 "Neo Technology,"
* Network Engine for Objects in Lund AB [http://neotechnology.com]
*
* This file is part of Neo4j.
*
* Neo4j is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.neo4j.index.impl;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import org.neo4j.graphdb.Direction;
import org.neo4j.graphdb.GraphDatabaseService;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.Relationship;
import org.neo4j.graphdb.RelationshipType;
import org.neo4j.graphdb.Transaction;
import org.neo4j.index.Index;
import org.neo4j.index.IndexHits;
import org.neo4j.index.impl.btree.BTree;
import org.neo4j.index.impl.btree.KeyEntry;
import org.neo4j.kernel.AbstractGraphDatabase;
/**
* A "multi" index implementation using {@link org.neo4j.util.btree.BTree BTree}
* that can index multiple nodes per key. They key is checked for equality
* using both <CODE>hashCode</CODE> and <CODE>equal</CODE> methods.
* <p>
* Note: this implementation is not thread safe (yet).
*
* This class isn't ready for general usage yet and use of it is discouraged.
*
* @deprecated
*/
abstract class AbstractIndex implements Index
{
public static enum RelTypes implements RelationshipType
{
INDEX_ENTRY,
}
private static final Object GOTO_NODE = Long.MIN_VALUE;
private static final String INDEX_NAME = "index_name";
private static final String INDEX_KEY = "index_key";
private static final String INDEX_TYPE = "index_type";
protected static final String INDEX_VALUES = "index_values";
private final Node underlyingNode;
private BTree bTree;
private String name;
private GraphDatabaseService graphDb;
protected abstract String getIndexType();
protected abstract void addOrReplace( KeyEntry entry, long value );
protected abstract void addOrReplace( Node entryNode, long value );
protected abstract boolean removeAllOrOne( KeyEntry entry, long value );
protected abstract boolean removeAllOrOne( Node entry, long value );
protected abstract long[] getValues( KeyEntry entry );
protected abstract long[] getValues( Node entry );
protected abstract long getSingleValue( KeyEntry entry );
protected abstract long getSingleValue( Node entry );
/**
* Creates/loads a index. The <CODE>underlyingNode</CODE> can either
* be a new (just created) node or a node that already represents a
* previously created index.
*
* @param name The unique name of the index
* @param underlyingNode The underlying node representing the index
* @param graphDb The embedded graph database instance
* @throws IllegalArgumentException if the underlying node is a index with
* a different name set or wrong index type.
*/
public AbstractIndex( String name, Node underlyingNode,
GraphDatabaseService graphDb )
{
if ( underlyingNode == null || graphDb == null )
{
throw new IllegalArgumentException(
"Null parameter underlyingNode=" + underlyingNode +
" graphDb=" + graphDb );
}
this.underlyingNode = underlyingNode;
this.graphDb = graphDb;
Transaction tx = graphDb.beginTx();
try
{
if ( underlyingNode.hasProperty( INDEX_NAME ) )
{
String storedName = (String) underlyingNode.getProperty(
INDEX_NAME );
if ( !storedName.equals( name ) )
{
throw new IllegalArgumentException( "Name of index " +
"for node=" + underlyingNode.getId() + "," +
storedName + " is not same as passed in name=" +
name );
}
if ( !getIndexType().equals( underlyingNode.getProperty(
INDEX_TYPE ) ) )
{
throw new IllegalArgumentException( "This index is " +
"not a " + getIndexType() + " value index" );
}
this.name = name;
}
else
{
underlyingNode.setProperty( INDEX_NAME, name );
underlyingNode.setProperty( INDEX_TYPE, getIndexType() );
this.name = name;
}
Relationship bTreeRel = underlyingNode.getSingleRelationship(
org.neo4j.index.impl.btree.BTree.RelTypes.TREE_ROOT,
Direction.OUTGOING );
if ( bTreeRel != null )
{
bTree = new BTree( graphDb, bTreeRel.getEndNode() );
}
else
{
Node bTreeNode = graphDb.createNode();
underlyingNode.createRelationshipTo( bTreeNode,
org.neo4j.index.impl.btree.BTree.RelTypes.TREE_ROOT );
bTree = new BTree( graphDb, bTreeNode );
}
tx.success();
}
finally
{
tx.finish();
}
}
public String getName()
{
return this.name;
}
protected Node getUnderlyingNode()
{
return underlyingNode;
}
/**
* Creates a index mapping for <CODE>indexKey</CODE> to
* <CODE>nodeToIndex</CODE>. This multi index implementation does
* handle multiple nodes for the same key. It also compares keys using the
* <CODE>equals</CODE> method.
*
* @param nodeToIndex the node to index with the specified key
* @param indexKey the key
*/
public void index( Node nodeToIndex, Object indexKey )
{
if ( indexKey == null )
{
throw new IllegalArgumentException( "Null index key" );
}
if ( nodeToIndex == null )
{
throw new IllegalArgumentException( "Null node" );
}
Transaction tx = graphDb.beginTx();
try
{
int hashCode = indexKey.hashCode();
long value = nodeToIndex.getId();
KeyEntry entry = bTree.addIfAbsent( hashCode, value );
if ( entry != null )
{
entry.setKeyValue( indexKey );
}
else
{
entry = bTree.getAsKeyEntry( hashCode );
Object goOtherNode = entry.getKeyValue();
Node bucketNode = null;
if ( !goOtherNode.equals( GOTO_NODE ) )
{
Object prevValue = entry.getValue();
Object prevKey = entry.getKeyValue();
if ( prevKey.equals( indexKey ) )
{
addOrReplace( entry, value );
tx.success();
return;
}
entry.setKeyValue( GOTO_NODE );
bucketNode = graphDb.createNode();
entry.setValue( bucketNode.getId() );
Node prevEntry = graphDb.createNode();
bucketNode.createRelationshipTo( prevEntry,
RelTypes.INDEX_ENTRY );
prevEntry.setProperty( INDEX_KEY, prevKey );
prevEntry.setProperty( INDEX_VALUES, prevValue );
Node newEntry = graphDb.createNode();
bucketNode.createRelationshipTo( newEntry,
RelTypes.INDEX_ENTRY );
newEntry.setProperty( INDEX_KEY, indexKey );
newEntry.setProperty( INDEX_VALUES, value );
}
else
{
bucketNode = graphDb.getNodeById( (Long) entry.getValue() );
for ( Relationship rel : bucketNode.getRelationships(
RelTypes.INDEX_ENTRY, Direction.OUTGOING ) )
{
Node entryNode = rel.getEndNode();
if ( entryNode.getProperty( INDEX_KEY ).equals(
indexKey ) )
{
addOrReplace( entryNode, value );
tx.success();
return;
}
}
Node newEntry = graphDb.createNode();
bucketNode.createRelationshipTo( newEntry,
RelTypes.INDEX_ENTRY );
newEntry.setProperty( INDEX_KEY, indexKey );
newEntry.setProperty( INDEX_VALUES, value );
}
}
tx.success();
}
finally
{
tx.finish();
}
}
/**
* Removes a index mapping between a node and a key. If the specified
* mapping doesn't exist this method will quietly return.
*
* @param nodeToRemove the node that is mapped to the index key
* @param indexKey the index key
*/
public void remove( Node nodeToRemove, Object indexKey )
{
if ( indexKey == null )
{
throw new IllegalArgumentException( "Null index key" );
}
if ( nodeToRemove == null )
{
throw new IllegalArgumentException( "Null node" );
}
Transaction tx = graphDb.beginTx();
try
{
int hashCode = indexKey.hashCode();
KeyEntry entry = bTree.getAsKeyEntry( hashCode );
if ( entry != null )
{
Object goOtherNode = entry.getKeyValue();
if ( !goOtherNode.equals( GOTO_NODE ) )
{
if ( goOtherNode.equals( indexKey ) )
{
if ( removeAllOrOne( entry, nodeToRemove.getId() ) )
{
entry.remove();
}
}
}
else
{
Node bucketNode = graphDb.getNodeById(
(Long) entry.getValue() );
for ( Relationship rel : bucketNode.getRelationships(
RelTypes.INDEX_ENTRY, Direction.OUTGOING ) )
{
Node entryNode = rel.getEndNode();
if ( entryNode.getProperty( INDEX_KEY ).equals(
indexKey ) )
{
if ( removeAllOrOne( entryNode,
nodeToRemove.getId() ) )
{
rel.delete();
entryNode.delete();
}
}
}
}
}
tx.success();
}
finally
{
tx.finish();
}
}
/**
* Public only for testing purposes. Validates the interal b-tree index and
* if any error is found a runtime exception is thrown.
*/
public void validate()
{
bTree.validateTree();
}
/**
* Retuns a iterable over all nodes mapped to the specified key or
* empty iterable if no node is mapped to the specified key.
*
* @param indexKey the key
* @return the node mapped to the key
*/
public IndexHits<Node> getNodesFor( Object indexKey )
{
if ( indexKey == null )
{
throw new IllegalArgumentException( "Null index key" );
}
Transaction tx = graphDb.beginTx();
try
{
int hashCode = indexKey.hashCode();
KeyEntry entry = bTree.getAsKeyEntry( hashCode );
if ( entry != null )
{
Object goOtherNode = entry.getKeyValue();
if ( !goOtherNode.equals( GOTO_NODE ) )
{
if ( goOtherNode.equals( indexKey ) )
{
long[] nodeIds = getValues( entry );
Node[] nodes = new Node[nodeIds.length];
for ( int i = 0; i < nodeIds.length; i++ )
{
nodes[i] = graphDb.getNodeById( nodeIds[i] );
}
tx.success();
return new SimpleIndexHits<Node>(
Arrays.asList( nodes ), nodes.length );
}
}
else
{
Node bucketNode = graphDb.getNodeById(
(Long) entry.getValue() );
for ( Relationship rel : bucketNode.getRelationships(
RelTypes.INDEX_ENTRY, Direction.OUTGOING ) )
{
Node entryNode = rel.getEndNode();
if ( entryNode.getProperty( INDEX_KEY ).equals(
indexKey ) )
{
long[] nodeIds = getValues( entryNode );
Node[] nodes = new Node[nodeIds.length];
for ( int i = 0; i < nodeIds.length; i++ )
{
nodes[i] = graphDb.getNodeById( nodeIds[i] );
}
tx.success();
return new SimpleIndexHits<Node>(
Arrays.asList( nodes ), nodes.length );
}
}
}
}
return new SimpleIndexHits<Node>(
Collections.<Node>emptyList(), 0 );
}
finally
{
tx.finish();
}
}
public Node getSingleNodeFor( Object indexKey )
{
if ( indexKey == null )
{
throw new IllegalArgumentException( "Null index key" );
}
Transaction tx = graphDb.beginTx();
try
{
int hashCode = indexKey.hashCode();
KeyEntry entry = bTree.getAsKeyEntry( hashCode );
if ( entry != null )
{
Object goOtherNode = entry.getKeyValue();
if ( !goOtherNode.equals( GOTO_NODE ) )
{
if ( goOtherNode.equals( indexKey ) )
{
long nodeId = getSingleValue( entry );
Node node = graphDb.getNodeById( nodeId );
tx.success();
return node;
}
}
else
{
Node bucketNode = graphDb.getNodeById(
(Long) entry.getValue() );
for ( Relationship rel : bucketNode.getRelationships(
RelTypes.INDEX_ENTRY, Direction.OUTGOING ) )
{
Node entryNode = rel.getEndNode();
if ( entryNode.getProperty( INDEX_KEY ).equals(
indexKey ) )
{
long nodeId = getSingleValue( entryNode );
Node node = graphDb.getNodeById( nodeId );
tx.success();
return node;
}
}
}
}
return null;
}
finally
{
tx.finish();
}
}
/**
* Deletes this index in a single (or the current) transaction including
* the underlying node.
*/
public void drop()
{
for ( KeyEntry entry : bTree.entries() )
{
Object goOtherNode = entry.getKeyValue();
if ( goOtherNode.equals( GOTO_NODE ) )
{
Node bucketNode = graphDb.getNodeById(
(Long) entry.getValue() );
for ( Relationship rel : bucketNode.getRelationships(
RelTypes.INDEX_ENTRY, Direction.OUTGOING ) )
{
Node entryNode = rel.getEndNode();
rel.delete();
entryNode.delete();
}
bucketNode.delete();
}
}
bTree.delete();
underlyingNode.delete();
}
/**
* Deletes this index in a single (or the current) transaction including
* the underlying node.
*/
public void clear()
{
for ( KeyEntry entry : bTree.entries() )
{
Object goOtherNode = entry.getKeyValue();
if ( goOtherNode.equals( GOTO_NODE ) )
{
Node bucketNode = graphDb.getNodeById(
(Long) entry.getValue() );
for ( Relationship rel : bucketNode.getRelationships(
RelTypes.INDEX_ENTRY, Direction.OUTGOING ) )
{
Node entryNode = rel.getEndNode();
rel.delete();
entryNode.delete();
}
bucketNode.delete();
}
}
bTree.delete();
Node bTreeNode = graphDb.createNode();
underlyingNode.createRelationshipTo( bTreeNode,
org.neo4j.index.impl.btree.BTree.RelTypes.TREE_ROOT );
bTree = new BTree( graphDb, bTreeNode );
}
/**
* Deletes this index using a commit interval including the underlying node.
*
* @param commitInterval number of index removes before a new transaction
* is started.
*/
public void drop( int commitInterval )
{
int count = 0;
for ( KeyEntry entry : bTree.entries() )
{
Object goOtherNode = entry.getKeyValue();
if ( goOtherNode.equals( GOTO_NODE ) )
{
Node bucketNode = graphDb.getNodeById(
(Long) entry.getValue() );
for ( Relationship rel : bucketNode.getRelationships(
RelTypes.INDEX_ENTRY, Direction.OUTGOING ) )
{
Node entryNode = rel.getEndNode();
rel.delete();
entryNode.delete();
}
bucketNode.delete();
}
count++;
if ( count >= commitInterval )
{
try
{
((AbstractGraphDatabase) graphDb).getConfig().getTxModule().
getTxManager().getTransaction().commit();
}
catch ( Exception e )
{
throw new RuntimeException( e );
}
graphDb.beginTx();
count = 0;
}
}
bTree.delete( commitInterval );
underlyingNode.delete();
}
/**
* Not supported yet, throws <CODE>UnsupportedOperationException</CODE>.
*/
public Iterable<Node> values()
{
return new IndexIterator( this, bTree, graphDb );
}
private static class IndexIterator implements Iterable<Node>,
Iterator<Node>
{
private Iterator<Node> currentNodes;
private final Iterator<KeyEntry> bTreeIterator;
private final GraphDatabaseService graphDb;
private final AbstractIndex index;
private IndexIterator( AbstractIndex index, BTree bTree,
GraphDatabaseService graphDb )
{
this.index = index;
this.bTreeIterator = bTree.entries().iterator();
this.graphDb = graphDb;
}
public boolean hasNext()
{
if ( currentNodes != null && currentNodes.hasNext() )
{
return true;
}
Transaction tx = graphDb.beginTx();
try
{
if ( bTreeIterator.hasNext() )
{
nextKeyEntry();
return hasNext();
}
}
finally
{
tx.finish();
}
return false;
}
private void nextKeyEntry()
{
KeyEntry entry = bTreeIterator.next();
Object goOtherNode = entry.getKeyValue();
if ( !goOtherNode.equals( GOTO_NODE ) )
{
long[] nodeIds = index.getValues( entry );
Node[] nodes = new Node[nodeIds.length];
for ( int i = 0; i < nodeIds.length; i++ )
{
nodes[i] = graphDb.getNodeById( nodeIds[i] );
}
currentNodes = Arrays.asList( nodes ).iterator();
}
else
{
Node bucketNode = graphDb.getNodeById(
(Long) entry.getValue() );
List<Node> nodeList = new ArrayList<Node>();
for ( Relationship rel : bucketNode.getRelationships(
RelTypes.INDEX_ENTRY, Direction.OUTGOING ) )
{
Node entryNode = rel.getEndNode();
long[] nodeIds = index.getValues( entryNode );
Node[] nodes = new Node[nodeIds.length];
for ( int i = 0; i < nodeIds.length; i++ )
{
nodes[i] = graphDb.getNodeById( nodeIds[i] );
}
nodeList.addAll( Arrays.asList( nodes ) );
}
currentNodes = nodeList.iterator();
}
}
public Node next()
{
if ( currentNodes == null || !currentNodes.hasNext() )
{
nextKeyEntry();
}
return currentNodes.next();
}
public void remove()
{
throw new UnsupportedOperationException();
}
public Iterator<Node> iterator()
{
return this;
}
}
}