package fr.lteconsulting.hexa.persistence.client.legacy.persistence; import java.util.ArrayList; import java.util.List; import java.util.Map; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import javax.persistence.EntityTransaction; import javax.persistence.FlushModeType; import javax.persistence.GenerationType; import javax.persistence.LockModeType; import javax.persistence.Query; import javax.persistence.TypedQuery; import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaQuery; import javax.persistence.metamodel.Metamodel; import com.google.gwt.core.client.JavaScriptObject; import com.google.gwt.core.shared.GWT; import fr.lteconsulting.hexa.client.classinfo.Clazz; import fr.lteconsulting.hexa.client.classinfo.Field; import fr.lteconsulting.hexa.client.sql.SQLite; import fr.lteconsulting.hexa.client.sql.SQLiteResult; import fr.lteconsulting.hexa.client.sql.SQLiteTypeManagerManager; import fr.lteconsulting.hexa.client.sql.SQLiteTypeManagerManager.SQLiteTypeManager; import fr.lteconsulting.hexa.persistence.client.legacy.persistence.ManagedObjectPool.AttachedObjectInfo; import fr.lteconsulting.hexa.persistence.client.legacy.persistence.PersistenceConfiguration.EntityConfiguration; import fr.lteconsulting.hexa.persistence.client.legacy.persistence.PersistenceConfiguration.FieldConfiguration; import fr.lteconsulting.hexa.persistence.client.legacy.persistence.PersistenceConfiguration.ManyToOneFieldConfiguration; import fr.lteconsulting.hexa.persistence.client.legacy.persistence.PersistenceConfiguration.OneToManyFieldConfiguration; public class EntityManagerImpl implements EntityManager { PersistenceConfiguration configuration; private final SQLite sqlite; ManagedObjectPool pool = new ManagedObjectPool(); private TransactionImpl currentTx; public EntityManagerImpl( String name, PersistenceConfiguration configuration, SQLite sqlite ) { this.configuration = configuration; this.sqlite = sqlite; } private int generateId( String tableName ) { JavaScriptObject res = sqlite.execute( "select nextId from NEXTID where tableName='"+tableName+"'" ); SQLiteResult sqlRes = new SQLiteResult( res ); if( sqlRes.size() == 0 ) { sqlite.execute( "insert into NEXTID (tableName, nextId) values ('"+tableName+"', 2)" ); return 1; } else { int nextId = Integer.parseInt( sqlRes.getRow( 0 ).getColumnValue( "nextId" ) ); sqlite.execute( "update NEXTID set nextId=nextId+1 where tableName='"+tableName+"'" ); return nextId; } } @Override public void clear() { assert false; } @Override public void close() { assert false; } @Override public boolean contains( Object arg0 ) { assert false; return false; } @Override public Query createNamedQuery( String arg0 ) { assert false; return null; } @Override public <T> TypedQuery<T> createNamedQuery( String arg0, Class<T> arg1 ) { assert false; return null; } @Override public Query createNativeQuery( String arg0 ) { assert false; return null; } @Override public Query createNativeQuery( String arg0, @SuppressWarnings( "rawtypes" ) Class arg1 ) { assert false; return null; } @Override public Query createNativeQuery( String arg0, String arg1 ) { assert false; return null; } @Override public Query createQuery( String arg0 ) { assert false; return null; } @Override public <T> TypedQuery<T> createQuery( CriteriaQuery<T> arg0 ) { return new TypedQueryImpl<T>( (CriteriaQueryImpl<T>) arg0, this ); } @Override public <T> TypedQuery<T> createQuery( String arg0, Class<T> arg1 ) { assert false; return null; } @Override public void detach( Object arg0 ) { AttachedObjectInfo info = pool.findAttachedObjectByReference( arg0 ); if( info != null ) pool.detachObject( info ); } // update the db with the current persistence context... private void commitChanges( ManagedObjectPool pool ) { GWT.log( "Committing changed into database..." ); // first do inserts List<AttachedObjectInfo> toInsert = pool.getObjectsToBeInserted(); while( ! toInsert.isEmpty() ) { AttachedObjectInfo info = toInsert.remove( 0 ); boolean postpone = false; // SQL : "insert into TABLE (fields) (values)" StringBuilder sb = new StringBuilder(); sb.append( "INSERT INTO " ); sb.append( info.entityConfiguration.tableName ); sb.append( "(" ); StringBuilder sbValues = new StringBuilder(); boolean fComa = false; // also insert object's ID (if GenerationType != IDENTITY) if( info.entityConfiguration.idGenerationType != GenerationType.IDENTITY ) { fComa = true; SQLiteTypeManager mng = SQLiteTypeManagerManager.get( info.entityConfiguration.idField.fieldClass ); Field field = info.entityConfiguration.entityClazz.getAllField( info.entityConfiguration.idField.fieldName ); sb.append( info.entityConfiguration.idField.columnName + " " ); if( ! mng.appendUpdateValueSql( sbValues, field, info.managedObject ) ) throw new RuntimeException( "Cannot append SQL update for ID field " + info.entityConfiguration.idField.fieldName + " of type " + info.entityConfiguration.idField.fieldClass.getName() ); } for( FieldConfiguration fieldConfiguration : info.entityConfiguration.directFields ) { SQLiteTypeManager mng = SQLiteTypeManagerManager.get( fieldConfiguration.fieldClass ); Field field = info.entityConfiguration.entityClazz.getAllField( fieldConfiguration.fieldName ); if( fComa ) { sb.append( ", " ); sbValues.append( ", " ); } fComa = true; sb.append( fieldConfiguration.columnName + " " ); if( ! mng.appendUpdateValueSql( sbValues, field, info.managedObject ) ) throw new RuntimeException( "Cannot append SQL update for field " + fieldConfiguration.fieldName + " of type " + fieldConfiguration.fieldClass.getName() ); } // check that there is not a relationship that forbids us to insert it for( ManyToOneFieldConfiguration fieldConfig : info.entityConfiguration.manyToOneFields ) { Field field = info.entityConfiguration.entityClazz.getAllField( fieldConfig.fieldName ); Object value = field.getValue( info.managedObject ); if( value != null ) { AttachedObjectInfo relatedObjectInfo = pool.findAttachedObjectByReference( value ); assert relatedObjectInfo != null : "You are trying to persist an object which references a non managed object, this is bad !"; if( relatedObjectInfo.isToBeInserted() ) { toInsert.add( info ); postpone = true; break; } // insert the id... if( fComa ) { sb.append( ", " ); sbValues.append( ", " ); } fComa = true; sb.append( fieldConfig.columnName + " " ); EntityConfiguration targetEntityConfiguration = configuration.getConfigurationForEntity( fieldConfig.fieldClass ); assert targetEntityConfiguration != null : "Targetted entity has not been configured !"; SQLiteTypeManager mng = SQLiteTypeManagerManager.get( targetEntityConfiguration.idField.fieldClass ); if( ! mng.appendUpdateValueSql( sbValues, targetEntityConfiguration.entityClazz.getAllField( targetEntityConfiguration.idField.fieldName ), relatedObjectInfo.managedObject ) ) throw new RuntimeException( "Cannot append SQL update for FK field " + fieldConfig.fieldName + " of type " + fieldConfig.fieldClass.getName() ); } } if( postpone ) continue; sb.append( ") VALUES (" ); sb.append( sbValues.toString() ); sb.append( ");" ); String sql = sb.toString(); sqlite.execute( sql ); // update object's ID (if GenerationType == IDENTITY) if( info.entityConfiguration.idGenerationType == GenerationType.IDENTITY ) { assert info.entityConfiguration.idField.fieldClass == int.class; int lastId = sqlite.getLastInsertedId(); if( lastId > 0 ) { Field idField = info.entityConfiguration.entityClazz.getAllField( info.entityConfiguration.idField.fieldName ); idField.setValue( info.managedObject, lastId ); } } info.markAsInserted(); } // then do updates for( AttachedObjectInfo info : pool.attachedObjects ) { if( info.row == null || info.fToDelete ) continue; // SQL : update from TABLE SET xxx=vvv WHERE id=ID // UPDATE users SET name = 'toto', xxx = value WHERE expression; StringBuilder sb = new StringBuilder(); sb.append( "UPDATE " ); sb.append( info.entityConfiguration.tableName ); sb.append( " SET " ); // TODO also update record id if it has changed (the id from the database is in info.managedObjectId) boolean fComa = false; for( FieldConfiguration fieldConfiguration : info.entityConfiguration.directFields ) { SQLiteTypeManager mng = SQLiteTypeManagerManager.get( fieldConfiguration.fieldClass ); Field field = info.entityConfiguration.entityClazz.getAllField( fieldConfiguration.fieldName ); // is the original data in the database the same as the one we have got now ? String dbValue = info.row.getColumnValue( fieldConfiguration.columnName ); String currentValue = mng.getStringForValue( field.getValue( info.managedObject ) ); if( stringsEqual( dbValue, currentValue ) ) continue; if( fComa ) sb.append( ", " ); else fComa = true; sb.append( fieldConfiguration.columnName + " " ); sb.append( " = " ); mng.appendUpdateValueSql( sb, field, info.managedObject ); } // do the same for ManyToOne fields (referenced ids might have changed) for( ManyToOneFieldConfiguration fieldConfiguration : info.entityConfiguration.manyToOneFields ) { EntityConfiguration targetConfiguration = configuration.getConfigurationForEntity( fieldConfiguration.fieldClass ); SQLiteTypeManager mng = SQLiteTypeManagerManager.get( targetConfiguration.idField.fieldClass ); // has the referencing id changed ? String dbValue = info.row.getColumnValue( fieldConfiguration.columnName ); Object targetEntity = info.entityConfiguration.entityClazz.getAllField( fieldConfiguration.fieldName ).getValue( info.managedObject ); Object targetEntityId = targetConfiguration.entityClazz.getAllField( targetConfiguration.idField.fieldName ).getValue( targetEntity ); String currentValue = mng.getStringForValue( targetEntityId ); if( stringsEqual( dbValue, currentValue ) ) continue; if( fComa ) sb.append( ", " ); else fComa = true; sb.append( fieldConfiguration.columnName + " " ); sb.append( " = " ); mng.appendUpdateValueSql( sb, targetConfiguration.entityClazz.getAllField( targetConfiguration.idField.fieldName ), targetEntity ); } // TODO : take id type in account if( ! fComa ) continue; // nothing to update sb.append( " WHERE id=" ); sb.append( info.managedObjectId ); sb.append( ";" ); String sql = sb.toString(); sqlite.execute( sql ); } // and eventually, do deletes for( AttachedObjectInfo info : pool.attachedObjects ) { if( ! info.fToDelete ) continue; // SQL : delete from TABLE where id=ID if( info.managedObjectId == null ) continue; // no need to delete a record that does not exist in database StringBuilder sb = new StringBuilder(); sb.append( "delete from " ); sb.append( info.entityConfiguration.tableName ); sb.append( " WHERE id=" ); sb.append( info.managedObjectId ); sb.append( ";" ); String sql = sb.toString(); sqlite.execute( sql ); } } private boolean stringsEqual( String s1, String s2 ) { if( s1==null && s2==null ) return true; if( s1==null || s2==null ) return false; return s1.equals( s2 ); } @SuppressWarnings( "unchecked" ) public <T> List<T> executeTypedQueryAndGetResultList( TypedQueryImpl<T> query ) { // compute SQLite's SQL dialect String sql = query.criteriaQuery.queryStructure.getSQL(); // execute query on DB JavaScriptObject r = sqlite.execute( sql ); SQLiteResult results = new SQLiteResult( r ); // interpret results // feed the entity manager return (List<T>) query.criteriaQuery.queryStructure.interpretResultAndFeedEntityManager( results, this ); } @Override public <T> T find( Class<T> arg0, Object arg1 ) { // SELECT * FROM entity.tableName WHERE idFieldName=arg1 EntityConfiguration config = configuration.entityConfigurations.get( arg0.getName() ); assert config != null : "Class " + arg0.getName() + " is not part of the configured entities !"; // if already managed, return it AttachedObjectInfo objectInfo = pool.findAttachedObjectByTableAndId( config.tableName, arg1 ); if( objectInfo != null ) { // TODO : maybe load data if not yet loaded... @SuppressWarnings( "unchecked" ) T object = (T) objectInfo.managedObject; return object; } SQLiteResult.Row row = readObjectFromDatabase( config, arg1 ); if( row == null ) return null; @SuppressWarnings( "unchecked" ) T object = (T) config.entityClazz.NEW(); readSQLiteResultToEntityObject( row, object, config ); pool.attachObject( config, arg1, object, row, false ); return object; } @SuppressWarnings( "unchecked" ) public <T> List<T> getCollectionList( Object owner, OneToManyFieldConfiguration fieldConfiguration ) { AttachedObjectInfo ownerInfo = pool.findAttachedObjectByReference( owner ); assert ownerInfo != null : "Bad !! ownerInfo is not in the managed pool. That must be a bug !"; // sql to get the records of the collection EntityConfiguration targetConfiguration = configuration.getConfigurationForEntity( fieldConfiguration.targetClass ); ManyToOneFieldConfiguration manyToOneFieldConfiguration = targetConfiguration.getManyToOneFieldConfiguration( fieldConfiguration.mappedBy ); assert manyToOneFieldConfiguration != null : "A OneToMany relationship should have the inverse relationship defined in the target entity !"; SQLiteResult dbResults = readObjectsFromDatabase( targetConfiguration, manyToOneFieldConfiguration.columnName, ownerInfo.managedObjectId ); List<T> list = new ArrayList<T>(); // for each record, check if already in the pool. In that case add the already registered object in the pool SQLiteTypeManager mng = SQLiteTypeManagerManager.get( targetConfiguration.idField.fieldClass ); for( SQLiteResult.Row row : dbResults ) { String idColumnValue = row.getColumnValue( targetConfiguration.idField.columnName ); Object rowId = mng.getValueFromString( idColumnValue ); AttachedObjectInfo rowInfo = pool.findAttachedObjectByTableAndId( targetConfiguration.tableName, rowId ); if( rowInfo != null ) { // if the record already exist in the pool, use the existing list.add( (T) rowInfo.managedObject ); } else { // if not, create and attach a object representing the db row T object = (T) targetConfiguration.entityClazz.NEW(); readSQLiteResultToEntityObject( row, object, targetConfiguration ); pool.attachObject( targetConfiguration, rowId, object, row, false ); list.add( object ); } } // return the list return list; } private SQLiteResult.Row readObjectFromDatabase( EntityConfiguration config, Object id ) { JavaScriptObject result = sqlite.execute( "select * from " + config.tableName + " where " + config.idField.columnName + " = " + id ); SQLiteResult sqliteResults = new SQLiteResult( result ); if( sqliteResults.size() == 0 ) return null; return sqliteResults.getRow( 0 ); } private SQLiteResult readObjectsFromDatabase( EntityConfiguration config, String columnName, Object columnValue ) { JavaScriptObject result = sqlite.execute( "select * from " + config.tableName + " where " + columnName + " = " + columnValue ); SQLiteResult sqliteResults = new SQLiteResult( result ); return sqliteResults; } <T> T createObjectAndRegisterIt( SQLiteResult.Row row, Object id, EntityConfiguration config ) { // create the object @SuppressWarnings( "unchecked" ) T object = (T) config.entityClazz.NEW(); readSQLiteResultToEntityObject( row, object, config ); pool.attachObject( config, id, object, row, false ); return object; } private <T> void readSQLiteResultToEntityObject( SQLiteResult.Row row, T object, EntityConfiguration config ) { // id field fillValueFromSQLiteResult( config.entityClazz, object, row, config.idField ); // direct fields for( FieldConfiguration fieldConfiguration : config.directFields ) fillValueFromSQLiteResult( config.entityClazz, object, row, fieldConfiguration ); // ManyToOne fields for( ManyToOneFieldConfiguration fieldConfiguration : config.manyToOneFields ) { EntityConfiguration referencedEntityConfiguration = configuration.getConfigurationForEntity( fieldConfiguration.fieldClass ); // sql type of the field is the type of the referenced table's id field SQLiteTypeManager mng = SQLiteTypeManagerManager.get( referencedEntityConfiguration.idField.fieldClass ); String columnValue = row.getColumnValue( fieldConfiguration.columnName ); // if reference id IS NULL, do nothing if( columnValue == null ) continue; // do we already have a managed instance for that record ? Object id = mng.getValueFromString( columnValue ); AttachedObjectInfo attachedObjectInfo = pool.findAttachedObjectByTableAndId( referencedEntityConfiguration.tableName, id ); if( attachedObjectInfo == null ) { // create a Proxy initialized only with the id field Object proxy = createProxyFor( referencedEntityConfiguration, id ); attachedObjectInfo = pool.attachObject( referencedEntityConfiguration, id, proxy, null, true ); } // put the proxy in the entity field config.entityClazz.getAllField( fieldConfiguration.fieldName ).setValue( object, attachedObjectInfo.managedObject ); } // OneToMany fields for( OneToManyFieldConfiguration fieldConfiguration : config.oneToManyFields ) { // create collection proxy for a List of Article... parameter : config and fieldName assert fieldConfiguration.containerClass == List.class : "For the moment, only List collection type is supported..."; ListProxy<?> listProxy = new ListProxy<T>( this, object, fieldConfiguration ); // put the list proxy in the entity field config.entityClazz.getAllField( fieldConfiguration.fieldName ).setValue( object, listProxy ); } } private Object createProxyFor( EntityConfiguration configuration, Object id ) { Object proxy = configuration.createEntityProxy( this ); // id field configuration.entityClazz.getAllField( configuration.idField.fieldName ).setValue( proxy, id ); return proxy; } public void loadProxyInternalObject( Object proxy ) { AttachedObjectInfo info = pool.findAttachedObjectByReference( proxy ); assert info != null : "Error, proxy should be found in the managed pool, you should not try to access not loaded proxified instances on a detached proxy..."; SQLiteResult.Row row = readObjectFromDatabase( info.entityConfiguration, info.managedObjectId ); assert row != null : "Error, proxy should be found in the managed pool, you should not try to access not loaded proxified instances on a detached proxy (bis)..."; readSQLiteResultToEntityObject( row, info.managedObject, info.entityConfiguration ); info.row = row; } private void copyValues( Object from, Object to, EntityConfiguration config ) { // id field copyValue( from, to, config.entityClazz, config.idField.fieldName ); // direct fields for( FieldConfiguration fieldConfiguration : config.directFields ) copyValue( from, to, config.entityClazz, fieldConfiguration.fieldName ); // ManyToOne fields for( ManyToOneFieldConfiguration fieldConfiguration : config.manyToOneFields ) copyValue( from, to, config.entityClazz, fieldConfiguration.fieldName ); } private void copyValue( Object from, Object to, Clazz<?> clazz, String fieldName ) { Field field = clazz.getAllField( fieldName ); field.setValue( to, field.getValue( from ) ); } private void fillValueFromSQLiteResult( Clazz<?> objectType, Object object, SQLiteResult.Row row, FieldConfiguration fieldConfiguration ) { SQLiteTypeManager mng = SQLiteTypeManagerManager.get( fieldConfiguration.fieldClass ); String columnValue = row.getColumnValue( fieldConfiguration.columnName ); mng.setFieldValueFromString( objectType.getAllField( fieldConfiguration.fieldName ), object, columnValue ); } @Override public <T> T find( Class<T> arg0, Object arg1, Map<String, Object> arg2 ) { assert false; return null; } @Override public <T> T find( Class<T> arg0, Object arg1, LockModeType arg2 ) { assert false; return null; } @Override public <T> T find( Class<T> arg0, Object arg1, LockModeType arg2, Map<String, Object> arg3 ) { assert false; return null; } @Override public void flush() { commitChanges( pool ); } @Override public CriteriaBuilder getCriteriaBuilder() { return new CriteriaBuilderImpl( this ); } @Override public Object getDelegate() { assert false; return null; } @Override public EntityManagerFactory getEntityManagerFactory() { assert false; return null; } @Override public FlushModeType getFlushMode() { assert false; return null; } @Override public LockModeType getLockMode( Object arg0 ) { assert false; return null; } @Override public Metamodel getMetamodel() { assert false; return null; } @Override public Map<String, Object> getProperties() { assert false; return null; } @Override public <T> T getReference( Class<T> arg0, Object arg1 ) { assert false; return null; } private class TransactionImpl implements EntityTransaction { boolean active; @Override public void begin() { assert ! active : "Transaction already active !"; active = true; sqlite.execute( "begin transaction;" ); } @Override public void commit() { commitChanges( pool ); active = false; sqlite.execute( "commit transaction;" ); pool.clear(); } @Override public boolean getRollbackOnly() { return false; } @Override public boolean isActive() { return active; } @Override public void rollback() { sqlite.execute( "rollback transaction;" ); pool.clear(); } @Override public void setRollbackOnly() { } } @Override public EntityTransaction getTransaction() { if( currentTx == null ) currentTx = new TransactionImpl(); return currentTx; } @Override public boolean isOpen() { assert false; return false; } @Override public void joinTransaction() { assert false; } @Override public void lock( Object arg0, LockModeType arg1 ) { assert false; } @Override public void lock( Object arg0, LockModeType arg1, Map<String, Object> arg2 ) { assert false; } @SuppressWarnings( "unchecked" ) @Override public <T> T merge( T arg0 ) { EntityConfiguration config = configuration.entityConfigurations.get( arg0.getClass().getName() ); assert config != null : "Class " + arg0.getClass().getName() + " is not part of the configured entities !"; T result = null; if( hasId( arg0, config ) ) { result = (T) find( arg0.getClass(), getEntityObjectId( arg0, config ) ); } if( result == null ) result = (T) config.entityClazz.NEW(); // copy field values from arg0 to managed object copyValues( arg0, result, config ); // TODO : maybe cascade merge new records... return result; } @Override public void persist( Object arg0 ) { EntityConfiguration config = configuration.entityConfigurations.get( arg0.getClass().getName() ); assert config != null : "Class " + arg0.getClass().getName() + " is not part of the configured entities !"; if( hasId( arg0, config ) ) { // TODO if object has an id, it should be already part of the persistence context. Otherwise we should throw an exception... AttachedObjectInfo attachedObject = pool.findAttachedObjectByReference( arg0 ); if( attachedObject == null ) throw new RuntimeException( "persist() called with an object that already has an id. And this object is not the one we already have in the persistence context : forbidden by JPA law !" ); // resurrect object if needed... attachedObject.fToDelete = false; } else { Object id = null; if( config.idGenerationType != GenerationType.IDENTITY ) { // generate an id assert config.idField.fieldClass == int.class : "id fields different than int not supported yet..."; id = generateId( config.tableName ); // and put it in the corresponding object's field setEntityObjectId( arg0, config, id ); } // TODO If a relationship is not cascade persist, and a related object is new, then an exception may be thrown if you do not first call persist on the related object. // then if a relationship is cascade.persist, go on, cascade ! // register the object in the persistence context pool.attachObject( config, id, arg0, null, false ); } // TODO : maybe cascade persist... } private boolean hasId( Object o, EntityConfiguration config ) { Object id = getEntityObjectId( o, config ); if( id instanceof Integer ) return (Integer)id > 0; return id != null; } private Object getEntityObjectId( Object o, EntityConfiguration config ) { Field idField = config.entityClazz.getAllField( config.idField.fieldName ); Object id = idField.getValue( o ); return id; } private void setEntityObjectId( Object o, EntityConfiguration config, Object id ) { Field idField = config.entityClazz.getAllField( config.idField.fieldName ); idField.setValue( o, id ); } @Override public void refresh( Object arg0 ) { assert false; } @Override public void refresh( Object arg0, Map<String, Object> arg1 ) { assert false; } @Override public void refresh( Object arg0, LockModeType arg1 ) { assert false; } @Override public void refresh( Object arg0, LockModeType arg1, Map<String, Object> arg2 ) { assert false; } @Override public void remove( Object arg0 ) { AttachedObjectInfo info = pool.findAttachedObjectByReference( arg0 ); if( info == null ) throw new RuntimeException( "Calling remove on a non-managed object is forbidden !" ); info.markAsToBeDeleted(); } @Override public void setFlushMode( FlushModeType arg0 ) { assert false; } @Override public void setProperty( String arg0, Object arg1 ) { assert false; } @Override public <T> T unwrap( Class<T> arg0 ) { assert false; return null; } }