module Neo4j
module Core
# == Handles Transactional Events
#
# You can use this to receive event before the transaction commits.
# The following events are supported:
# * on_neo4j_started
# * on_neo4j_shutdown
# * on_node_created
# * on_node_deleted
# * on_relationship_created
# * on_relationship_deleted
# * on_property_changed
# * on_rel_property_changed
# * on_after_commit
#
# ==== on_neo4j_started(db)
#
# Called when the neo4j engine starts.
# Notice that the neo4j will be started automatically when the first neo4j operation is performed.
# You can also start Neo4j: Neo4j.start
#
# * db :: the Neo4j::Database instance
#
# ==== on_neo4j_shutdown(db)
#
# Called when the neo4j engine shutdown. You don't need to call Neo4j.shutdown since
# the it will automatically be shutdown when the application exits (using the at_exit ruby hook).
#
# * db :: the Neo4j::Database instance
#
# ==== on_after_commit(data, state)
#
# Called after the transaction has successfully committed.
# See http://api.neo4j.org/1.4/org/neo4j/graphdb/event/TransactionEventHandler.html for the data and state parameter.
#
# ==== on_node_created(node)
#
# * node :: the node that was created
#
# ==== on_node_deleted(node, old_props, deleted_relationship_set, deleted_node_identity_map)
#
# * node :: the node that was deleted
# * old_props :: a hash of the old properties this node had
# * deleted_relationship_set :: the set of deleted relationships. See Neo4j::Core::RelationshipSet
# * deleted_node_identity_map :: the identity map of deleted nodes. The key is the node id, and the value is the node
#
# ==== on_relationship_created(rel, created_node_identity_map)
#
# * rel :: the relationship that was created
# * created_node_identity_map :: the identity map of created nodes. The key is the node id, and the value is the node
#
# ==== on_relationship_deleted(rel, old_props, deleted_relationship_set, deleted_node_identity_map)
#
# * rel :: the relationship that was created
# * old_props :: a hash of the old properties this relationship had
# * deleted_relationship_set :: the set of deleted relationships. See Neo4j::Core::RelationshipSet
# * deleted_node_identity_map :: the identity map of deleted nodes. The key is the node id, and the value is the node
#
# ==== on_property_changed(node, key, old_value, new_value)
#
# * node :: the node
# * key :: the name of the property that was changed (String)
# * old_value :: old value of the property
# * new_value :: new value of the property
#
# ==== on_rel_property_changed(rel, key, old_value, new_value)
#
# * rel :: the node that was created
# * key :: the name of the property that was changed (String)
# * old_value :: old value of the property
# * new_value :: new value of the property
#
# ==== classes_changed(class_change_map)
# * class_change_map :: a hash with class names as keys, and class changes as values. See Neo4j::ClassChanges
#
# == Usage
#
# class MyListener
# def on_node_deleted(node, old_props, deleted_relationship_set, deleted_node_identity_map)
# end
# end
#
# # to add an listener without starting neo4j:
# Neo4j.unstarted_db.event_handler.add(MyListener.new)
#
# You only need to implement the methods that you need.
#
class EventHandler
include Java::OrgNeo4jGraphdbEvent::TransactionEventHandler
def initialize
@listeners = []
end
def after_commit(data, state)
@listeners.each { |li| li.on_after_commit(data, state) if li.respond_to?(:on_after_commit) }
end
def after_rollback(data, state)
end
def before_commit(data)
class_change_map = java.util.HashMap.new
created_node_identity_map = iterate_created_nodes(data.created_nodes, class_change_map)
deleted_node_identity_map = deleted_node_identity_map(data.deleted_nodes)
deleted_relationship_set = relationship_set(data.deleted_relationships)
removed_node_properties_map = property_map(data.removed_node_properties)
removed_relationship_properties_map = property_map(data.removed_relationship_properties)
add_deleted_nodes(data, class_change_map, removed_node_properties_map)
empty_map = java.util.HashMap.new
data.assigned_node_properties.each { |tx_data| property_changed(tx_data.entity, tx_data.key, tx_data.previously_commited_value, tx_data.value) unless tx_data.key == '_classname' }
data.removed_node_properties.each { |tx_data| property_changed(tx_data.entity, tx_data.key, tx_data.previously_commited_value, nil) unless deleted_node_identity_map.containsKey(tx_data.entity.getId) }
data.deleted_nodes.each { |node| node_deleted(node, removed_node_properties_map.get(node.getId)||empty_map, deleted_relationship_set, deleted_node_identity_map) }
data.created_relationships.each { |rel| relationship_created(rel, created_node_identity_map) }
data.deleted_relationships.each { |rel| relationship_deleted(rel, removed_relationship_properties_map.get(rel.getId)||empty_map, deleted_relationship_set, deleted_node_identity_map) }
data.assigned_relationship_properties.each { |tx_data| rel_property_changed(tx_data.entity, tx_data.key, tx_data.previously_commited_value, tx_data.value) }
data.removed_relationship_properties.each { |tx_data| rel_property_changed(tx_data.entity, tx_data.key, tx_data.previously_commited_value, nil) unless deleted_relationship_set.contains_rel?(tx_data.entity) }
classes_changed(class_change_map)
rescue Exception => e
# since these exceptions gets swallowed
puts "ERROR in before commit hook: #{e.class} - #{e}"
puts e.backtrace.join("\n")
end
def iterate_created_nodes(nodes, class_change_map)
identity_map = java.util.HashMap.new
nodes.each do |node|
identity_map.put(node.neo_id, node) #using put due to a performance regression in JRuby 1.6.4
instance_created(node, class_change_map)
node_created(node)
end
identity_map
end
def deleted_node_identity_map(nodes)
identity_map = java.util.HashMap.new
nodes.each { |node| identity_map.put(node.neo_id, node) } #using put due to a performance regression in JRuby 1.6.4
identity_map
end
def relationship_set(relationships)
relationship_set = Neo4j::Core::RelationshipSet.new #(relationships.size)
relationships.each { |rel| relationship_set.add(rel) }
relationship_set
end
def property_map(properties)
map = java.util.HashMap.new
properties.each do |property|
map(property.entity.getId, map).put(property.key, property.previously_commited_value)
end
map
end
def map(key, map)
map.get(key) || add_map(key, map)
end
def add_map(key, map)
map.put(key, java.util.HashMap.new)
map.get(key)
end
def add(listener)
@listeners << listener unless @listeners.include?(listener)
end
def remove(listener)
@listeners.delete(listener)
end
def remove_all
@listeners = []
end
def print
puts "Listeners #{@listeners.size}"
@listeners.each { |li| puts " Listener '#{li}'" }
end
def neo4j_started(db)
@listeners.each { |li| li.on_neo4j_started(db) if li.respond_to?(:on_neo4j_started) }
end
def neo4j_shutdown(db)
@listeners.each { |li| li.on_neo4j_shutdown(db) if li.respond_to?(:on_neo4j_shutdown) }
end
def node_created(node)
@listeners.each { |li| li.on_node_created(node) if li.respond_to?(:on_node_created) }
end
def node_deleted(node, old_properties, deleted_relationship_set, deleted_node_identity_map)
@listeners.each { |li| li.on_node_deleted(node, old_properties, deleted_relationship_set, deleted_node_identity_map) if li.respond_to?(:on_node_deleted) }
end
def relationship_created(relationship, created_node_identity_map)
@listeners.each { |li| li.on_relationship_created(relationship, created_node_identity_map) if li.respond_to?(:on_relationship_created) }
end
def relationship_deleted(relationship, old_props, deleted_relationship_set, deleted_node_identity_map)
@listeners.each { |li| li.on_relationship_deleted(relationship, old_props, deleted_relationship_set, deleted_node_identity_map) if li.respond_to?(:on_relationship_deleted) }
end
def property_changed(node, key, old_value, new_value)
@listeners.each { |li| li.on_property_changed(node, key, old_value, new_value) if li.respond_to?(:on_property_changed) }
end
def rel_property_changed(rel, key, old_value, new_value)
@listeners.each { |li| li.on_rel_property_changed(rel, key, old_value, new_value) if li.respond_to?(:on_rel_property_changed) }
end
def add_deleted_nodes(data, class_change_map, removed_node_properties_map)
data.deleted_nodes.each { |node| instance_deleted(node, removed_node_properties_map, class_change_map) }
end
def instance_created(node, class_change_map)
classname = node[:_classname]
class_change(classname, class_change_map).add(node) if classname
end
def instance_deleted(node, removed_node_properties_map, class_change_map)
properties = removed_node_properties_map.get(node.getId)
if properties
classname = properties.get("_classname")
class_change(classname, class_change_map).delete(node) if classname
end
end
def class_change(classname, class_change_map)
class_change_map.put(classname, ClassChanges.new) if class_change_map.get(classname).nil?
class_change_map.get(classname)
end
def classes_changed(changed_class_map)
@listeners.each { |li| li.classes_changed(changed_class_map) if li.respond_to?(:classes_changed) }
end
end
class ClassChanges
attr_accessor :added, :deleted
def initialize
self.added = []
self.deleted = []
end
def add(node)
self.added << node
end
def delete(node)
self.deleted << node
end
def net_change
self.added.size - self.deleted.size
end
end
end
end