package org.qi4j.entitystore.neo4j; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.json.JSONStringer; import org.json.JSONTokener; import org.neo4j.graphdb.Direction; import org.neo4j.graphdb.DynamicRelationshipType; import org.neo4j.graphdb.Node; import org.neo4j.graphdb.Relationship; import org.neo4j.graphdb.RelationshipType; import org.qi4j.api.common.QualifiedName; import org.qi4j.api.common.TypeName; import org.qi4j.api.entity.EntityReference; import org.qi4j.spi.entity.EntityDescriptor; import org.qi4j.spi.entity.EntityState; import org.qi4j.spi.entity.EntityStatus; import org.qi4j.spi.entity.ManyAssociationState; import org.qi4j.spi.entity.NamedAssociationState; import org.qi4j.spi.entitystore.EntityStoreException; import org.qi4j.spi.entitystore.EntityStoreUnitOfWork; import org.qi4j.spi.property.PropertyType; public class NeoEntityState implements EntityState { static final String ENTITY_ID = "entity_id"; static final String VERSION = "version"; static final String MODIFIED = "modified"; private final Node underlyingNode; private final NeoEntityStoreUnitOfWork uow; private EntityStatus status; NeoEntityState( NeoEntityStoreUnitOfWork work, Node node, EntityStatus status ) { this.underlyingNode = node; this.uow = work; this.status = status; } protected void setUpdated() { if( status == EntityStatus.LOADED ) { status = EntityStatus.UPDATED; Long version = (Long) underlyingNode.getProperty( VERSION ); underlyingNode.setProperty( VERSION, version + 1 ); underlyingNode.setProperty( MODIFIED, uow.currentTime() ); } } static RelationshipType manyAssociation( QualifiedName stateName ) { return DynamicRelationshipType.withName( "many_association::" + stateName.toString() ); } static RelationshipType namedAssociation( QualifiedName stateName ) { return DynamicRelationshipType.withName( "named_association::" + stateName.toString() ); } static RelationshipType association( QualifiedName stateName ) { return DynamicRelationshipType.withName( "association::" + stateName.toString() ); } public ManyAssociationState getManyAssociation( QualifiedName stateName ) { RelationshipType manyAssociation = manyAssociation( stateName ); Relationship rel = underlyingNode.getSingleRelationship( manyAssociation, Direction.OUTGOING ); if( rel != null ) { return new NeoManyAssociationState( uow, this, rel.getEndNode() ); } Node node = uow.getNeo().createNode(); node.setProperty( NeoManyAssociationState.COUNT, 0 ); underlyingNode.createRelationshipTo( node, manyAssociation ); return new NeoManyAssociationState( uow, this, node ); } @Override public NamedAssociationState getNamedAssociation( QualifiedName stateName ) { RelationshipType namedAssociation = namedAssociation( stateName ); Relationship rel = underlyingNode.getSingleRelationship( namedAssociation, Direction.OUTGOING ); if( rel != null ) { return new NeoNamedAssociationState( uow, this, rel.getEndNode() ); } Node node = uow.getNeo().createNode(); node.setProperty( NeoManyAssociationState.COUNT, 0 ); underlyingNode.createRelationshipTo( node, namedAssociation ); return new NeoNamedAssociationState( uow, this, node ); } public EntityReference getAssociation( QualifiedName stateName ) { Relationship rel = underlyingNode.getSingleRelationship( association( stateName ), Direction.OUTGOING ); if( rel != null ) { String entityId = (String) rel.getEndNode().getProperty( ENTITY_ID ); return new EntityReference( entityId ); } return null; } public void setAssociation( QualifiedName stateName, EntityReference newEntity ) { RelationshipType association = association( stateName ); Relationship rel = underlyingNode.getSingleRelationship( association, Direction.OUTGOING ); if( rel != null ) { Node otherNode = rel.getEndNode(); if( otherNode.getProperty( ENTITY_ID ).equals( identity().identity() ) ) { otherNode.delete(); } rel.delete(); } if( newEntity != null ) { Node otherNode = uow.getEntityStateNode( newEntity ); if( otherNode.equals( underlyingNode ) ) { // create a blank node for self reference otherNode = uow.getNeo().createNode(); otherNode.setProperty( ENTITY_ID, identity().identity() ); } underlyingNode.createRelationshipTo( otherNode, association ); } } public Object getProperty( QualifiedName stateName ) { try { Object prop = underlyingNode.getProperty( "prop::" + stateName.toString(), null ); if( prop == null ) { return null; } else if( isPrimitiveType( prop ) ) { return prop; } else { // why is it a set and not a Map? for( PropertyType propertyType : entityDescriptor().entityType().properties() ) { if( propertyType.qualifiedName().equals( stateName ) ) { String json = "[" + prop + "]"; JSONTokener tokener = new JSONTokener( json ); JSONArray array = (JSONArray) tokener.nextValue(); Object jsonValue = array.get( 0 ); if( jsonValue == JSONObject.NULL ) { return null; } else { return propertyType.type().fromJSON( jsonValue, uow.getModule() ); } } } } return underlyingNode.getProperty( "prop::" + stateName.toString() ).toString(); } catch( JSONException e ) { throw new EntityStoreException( e ); } } public void setProperty( QualifiedName stateName, Object prop ) { try { if( prop != null ) { if( isPrimitiveType( prop ) ) { underlyingNode.setProperty( "prop::" + stateName.toString(), prop ); } else { // why is it a set and not a Map? for( PropertyType propertyType : entityDescriptor().entityType().properties() ) { if( propertyType.qualifiedName().equals( stateName ) ) { if( prop instanceof String && propertyType.type().isString() ) { underlyingNode.setProperty( "prop::" + stateName.toString(), prop ); } else { JSONStringer json = new JSONStringer(); json.array(); propertyType.type().toJSON( prop, json ); json.endArray(); String jsonString = json.toString(); jsonString = jsonString.substring( 1, jsonString.length() - 1 ); underlyingNode.setProperty( "prop::" + stateName.toString(), jsonString ); } break; } } } } else { underlyingNode.removeProperty( stateName.toString() ); } setUpdated(); } catch( JSONException e ) { throw new EntityStoreException( e ); } } private boolean isPrimitiveType( Object prop ) { if( prop instanceof Number || prop instanceof Character || prop instanceof Boolean ) { return true; } if( prop.getClass().isArray() ) { return isPrimitiveArrayType( prop ); } return false; } private boolean isPrimitiveArrayType( Object array ) { if( array instanceof int[] || array instanceof Integer[] || array instanceof String[] || array instanceof boolean[] || array instanceof Boolean[] || array instanceof double[] || array instanceof Double[] || array instanceof float[] || array instanceof Float[] || array instanceof long[] || array instanceof Long[] || array instanceof byte[] || array instanceof Byte[] || array instanceof char[] || array instanceof Character[] || array instanceof short[] || array instanceof Short[] ) { return true; } return false; } public void remove() { // Apparently remove should just force remove associations instead // of throwing exception if the entity has incomming associations // if ( underlyingNode.hasRelationship( Direction.INCOMING ) ) // { // throw new IllegalStateException( // "Cannot remove entity with reference: " + identity() // + ". It has incoming associtaions."); // } // remove of all incomming associations for( Relationship rel : underlyingNode.getRelationships( Direction.INCOMING ) ) { rel.delete(); } uow.getIndexService().removeIndex( underlyingNode, NeoEntityStoreUnitOfWork.ENTITY_STATE_ID, underlyingNode.getProperty( ENTITY_ID ) ); for( Relationship rel : underlyingNode.getRelationships( Direction.OUTGOING ) ) { Node endNode = rel.getEndNode(); boolean manyAssocNode = false; for( Relationship manyRel : endNode.getRelationships( RelTypes.MANY_ASSOCIATION, Direction.OUTGOING ) ) { manyRel.delete(); manyAssocNode = true; } if( manyAssocNode ) { endNode.delete(); } rel.delete(); } underlyingNode.delete(); status = EntityStatus.REMOVED; } public EntityDescriptor entityDescriptor() { Node typeNode = underlyingNode.getSingleRelationship( RelTypes.IS_OF_TYPE, Direction.OUTGOING ).getEndNode(); String type = (String) typeNode.getProperty( NeoEntityStoreUnitOfWork.ENTITY_TYPE ); return uow.getEntityDescriptor( type ); } public void hasBeenApplied() { // TODO } public EntityReference identity() { return new EntityReference( (String) underlyingNode.getProperty( ENTITY_ID ) ); } public boolean isOfType( TypeName type ) { Node typeNode = underlyingNode.getSingleRelationship( RelTypes.IS_OF_TYPE, Direction.OUTGOING ).getEndNode(); String typeName = (String) typeNode.getProperty( NeoEntityStoreUnitOfWork.ENTITY_TYPE ); return typeName.equals( type.name() ); } public long lastModified() { long modified = (Long) underlyingNode.getProperty( MODIFIED ); return modified; } public EntityStatus status() { return status; } public String version() { long version = (Long) underlyingNode.getProperty( VERSION ); if( status == EntityStatus.UPDATED ) { version--; } return "" + version; } public EntityStoreUnitOfWork getUnitOfWork() { return uow; } }