package er.neo4jadaptor.storage.neo4j; import org.neo4j.graphdb.GraphDatabaseService; import org.neo4j.graphdb.Node; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.webobjects.eoaccess.EOAttribute; import com.webobjects.eoaccess.EOEntity; import com.webobjects.eocontrol.EOQualifier; import com.webobjects.foundation.NSArray; import com.webobjects.foundation.NSMutableDictionary; import er.neo4jadaptor.ersatz.Ersatz; import er.neo4jadaptor.ersatz.neo4j.Neo4JErsatz; import er.neo4jadaptor.ersatz.neo4j.Neo4JNodeErsatz; import er.neo4jadaptor.ersatz.webobjects.NSDictionaryErsatz; import er.neo4jadaptor.storage.Store; import er.neo4jadaptor.utils.EOUtilities; import er.neo4jadaptor.utils.cursor.Cursor; /** * Storage for entities that use only one numerical EO attribute for primary key. Each row is represented as a node. * Each to-one EO relationship is represented as a single outgoing relationship. Foreign key values are not stored, * those are calculated based on existing relationships whenever needed. * * @author Jedrzej Sobanski */ public class NodeStore implements Store<Ersatz, Neo4JErsatz> { private static final Logger log = LoggerFactory.getLogger(NodeStore.class); private final GraphDatabaseService db; private final EOEntity entity; private final NodeSpaceManager spaceManager; private final EOAttribute pk; private final TemporaryNodePool tempNodePool; public NodeStore( GraphDatabaseService db, EOEntity entity, NodeSpaceManager spaceManager, TemporaryNodePool tempNodePool) { validateEntity(entity); this.entity = entity; this.db = db; pk = EOUtilities.primaryKeyAttribute(entity); this.spaceManager = spaceManager; this.tempNodePool = tempNodePool; } public Neo4JErsatz insert(Ersatz row) { Object pkValue = row.get(pk); if (pkValue == null) { // it happens in case of inserts of rows with attributes like "NeededByEOF0" which seem not to be real rows. log.warn("Primary key value is null for {}. Ignoring it (not sure if it's correct behaviour)", row); return null; } if (pkValue != null && false == pkValue instanceof NodeNumber) { throw new IllegalStateException("Value for primary key " + entity.name() + "." + pk.name() + " was set manually. This adaptor only supports inserts where primary keys aren't set on insert time."); } long id = ((NodeNumber) pkValue).longValue(); final Node node = db.getNodeById(id); if (spaceManager.isPermanent(node)) { throw new IllegalStateException("Node with id=" + id + " already exists in permanent space"); } else { spaceManager.setIsPermanent(node, entity); } Neo4JErsatz ret = new Neo4JNodeErsatz(entity, node); Ersatz.copy(row, ret); return ret; } public void update(Ersatz newValues, Neo4JErsatz neoErsatz) { Ersatz.copy(newValues, neoErsatz); } public void delete(Neo4JErsatz neoErsatz) { neoErsatz.delete(); } public Ersatz newPrimaryKey() { Node node = tempNodePool.getNextTemporaryNode(); Number id = node.getId(); id = EOUtilities.convertToAttributeType(pk, id); NSMutableDictionary<EOAttribute, Object> dict = new NSMutableDictionary<>(new NodeNumber(id), pk); return NSDictionaryErsatz.fromDictionary(dict); } public Cursor<Neo4JErsatz> query(EOQualifier q) { throw new UnsupportedOperationException(); } protected static void validateEntity(EOEntity entity) throws UnsupportedEntityException { NSArray<EOAttribute> pks = entity.primaryKeyAttributes(); EOAttribute pk; if (pks.size() != 1) { throw new UnsupportedEntityException("Entity " + entity.name() + " must have one primary key attribute"); } else { pk = pks.get(0); } if (entity.classPropertyNames().contains(pk.name())) { // if (! pk.isReadOnly()) { // // primary key values are assigned node id values therefore shouldn't be possible to overwrite them // throw new UnsupportedEntityException("Primary key " + entity.name() + "." + pk.name() + " can only be read only when it's a class property"); // } log.warn("Primary key {}.{} is class property. Its value shouldn't be set manually.", entity.name(), pk.name()); } try { Class<?> valueType; // check primary key type valueType = Class.forName(pk.valueTypeClassName()); if (! Integer.class.equals(valueType) && ! Long.class.equals(valueType)) { throw new UnsupportedEntityException("Primary key " + entity.name() + "." + pk.name() + " value type must be integer or long type"); } } catch (ClassNotFoundException e) { throw new RuntimeException(e); } } /** * Only used to distinguish primary key values being result of a {@link #newPrimaryKey()} call from * those where primary key value is set manually. */ public static final class NodeNumber extends Number { private final Number number; private NodeNumber(Number number) { this.number = number; } @Override public int intValue() { return number.intValue(); } @Override public long longValue() { return number.longValue(); } @Override public float floatValue() { return number.floatValue(); } @Override public double doubleValue() { return number.doubleValue(); } @Override public boolean equals(Object obj) { return number.equals(obj); } @Override public int hashCode() { return number.hashCode(); } @Override public String toString() { return Long.toString(longValue()); } } }