/* * Copyright (c) 2010, Stanislav Muhametsin. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package org.qi4j.entitystore.sql; import java.io.IOException; import java.io.PrintWriter; import java.io.Reader; import java.io.StringWriter; import java.io.Writer; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.json.JSONTokener; import org.json.JSONWriter; import org.qi4j.api.cache.CacheOptions; import org.qi4j.api.common.Optional; import org.qi4j.api.common.QualifiedName; import org.qi4j.api.entity.EntityReference; import org.qi4j.api.injection.scope.Service; import org.qi4j.api.injection.scope.Structure; import org.qi4j.api.injection.scope.This; import org.qi4j.api.io.Input; import org.qi4j.api.io.Output; import org.qi4j.api.io.Receiver; import org.qi4j.api.io.Sender; import org.qi4j.api.service.Activatable; import org.qi4j.api.structure.Application; import org.qi4j.api.unitofwork.EntityTypeNotFoundException; import org.qi4j.api.usecase.Usecase; import org.qi4j.api.usecase.UsecaseBuilder; import org.qi4j.entitystore.map.MapEntityStore; import org.qi4j.entitystore.map.MapEntityStoreMixin; import org.qi4j.entitystore.map.Migration; import org.qi4j.entitystore.map.StateStore; import org.qi4j.entitystore.sql.internal.DatabaseSQLService; import org.qi4j.entitystore.sql.internal.DatabaseSQLService.EntityValueResult; import org.qi4j.library.sql.api.SQLEntityState; import org.qi4j.library.sql.api.SQLEntityState.DefaultSQLEntityState; import org.qi4j.library.sql.common.SQLUtil; import org.qi4j.spi.entity.EntityDescriptor; import org.qi4j.spi.entity.EntityState; import org.qi4j.spi.entity.EntityStatus; import org.qi4j.spi.entity.EntityType; import org.qi4j.spi.entity.association.AssociationDescriptor; import org.qi4j.spi.entitystore.DefaultEntityStoreUnitOfWork; import org.qi4j.spi.entitystore.EntityNotFoundException; import org.qi4j.spi.entitystore.EntityStore; import org.qi4j.spi.entitystore.EntityStoreException; import org.qi4j.spi.entitystore.EntityStoreSPI; import org.qi4j.spi.entitystore.EntityStoreUnitOfWork; import org.qi4j.spi.entitystore.StateCommitter; import org.qi4j.spi.entitystore.helpers.DefaultEntityState; import org.qi4j.spi.property.PropertyDescriptor; import org.qi4j.spi.property.PropertyType; import org.qi4j.spi.property.PropertyTypeDescriptor; import org.qi4j.spi.structure.ModuleSPI; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Most of this code is copy-paste from {@link MapEntityStoreMixin}. TODO refactor stuff that has to do with general * things than actual MapEntityStore from {@link MapEntityStoreMixin} so that this class could extend some * "AbstractJSONEntityStoreMixin". * */ public class SQLEntityStoreMixin implements EntityStore, EntityStoreSPI, StateStore, Activatable { private static final Logger LOGGER = LoggerFactory.getLogger( SQLEntityStoreMixin.class ); @Service private DatabaseSQLService database; @This private EntityStoreSPI entityStoreSPI; @Structure private Application application; @Optional @Service private Migration migration; private String uuid; private Integer count; public void activate() throws Exception { uuid = UUID.randomUUID().toString() + "-"; count = 0; database.startDatabase(); } public void passivate() throws Exception { database.stopDatabase(); } public StateCommitter applyChanges( final EntityStoreUnitOfWork unitofwork, final Iterable<EntityState> states ) { return new StateCommitter() { public void commit() { Connection connection = null; PreparedStatement insertPS = null; PreparedStatement updatePS = null; PreparedStatement removePS = null; try { connection = database.getConnection(); insertPS = database.prepareInsertEntityStatement( connection ); updatePS = database.prepareUpdateEntityStatement( connection ); removePS = database.prepareRemoveEntityStatement( connection ); for( EntityState state : states ) { EntityStatus status = state.status(); DefaultEntityState defState = ( (SQLEntityState) state ).getDefaultEntityState(); Long entityPK = ( (SQLEntityState) state ).getEntityPK(); if( EntityStatus.REMOVED.equals( status ) ) { database.populateRemoveEntityStatement( removePS, entityPK, state.identity() ); removePS.addBatch(); } else { StringWriter writer = new StringWriter(); writeEntityState( defState, writer, unitofwork.identity() ); writer.flush(); if( EntityStatus.UPDATED.equals( status ) ) { Long entityOptimisticLock = ( (SQLEntityState) state ).getEntityOptimisticLock(); database.populateUpdateEntityStatement( updatePS, entityPK, entityOptimisticLock, defState.identity(), writer.toString(), unitofwork.currentTime() ); updatePS.addBatch(); } else if( EntityStatus.NEW.equals( status ) ) { database.populateInsertEntityStatement( insertPS, entityPK, defState.identity(), writer.toString(), unitofwork.currentTime() ); insertPS.addBatch(); } } } removePS.executeBatch(); insertPS.executeBatch(); updatePS.executeBatch(); connection.commit(); } catch( SQLException sqle ) { SQLUtil.rollbackQuietly( connection ); if( LOGGER.isDebugEnabled() ) { StringWriter sb = new StringWriter(); sb.append( "SQLException during commit, logging nested exceptions before throwing EntityStoreException:\n" ); SQLException e = sqle; while( e != null ) { e.printStackTrace( new PrintWriter( sb, true ) ); e = e.getNextException(); } LOGGER.debug( sb.toString() ); } throw new EntityStoreException( sqle ); } catch( RuntimeException re ) { SQLUtil.rollbackQuietly( connection ); throw new EntityStoreException( re ); } finally { SQLUtil.closeQuietly( insertPS ); SQLUtil.closeQuietly( updatePS ); SQLUtil.closeQuietly( removePS ); SQLUtil.closeQuietly( connection ); } } public void cancel() { } }; } public EntityState getEntityState( EntityStoreUnitOfWork unitOfWork, EntityReference entityRef ) { EntityValueResult valueResult = getValue( entityRef ); return new DefaultSQLEntityState( readEntityState( (DefaultEntityStoreUnitOfWork) unitOfWork, valueResult.getReader() ), valueResult.getEntityPK(), valueResult.getEntityOptimisticLock() ); } public EntityState newEntityState( EntityStoreUnitOfWork unitOfWork, EntityReference entityRef, EntityDescriptor entityDescriptor ) { return new DefaultSQLEntityState( new DefaultEntityState( (DefaultEntityStoreUnitOfWork) unitOfWork, entityRef, entityDescriptor ), database.newPKForEntity(), null ); } public EntityStoreUnitOfWork newUnitOfWork( Usecase usecase, ModuleSPI module, long currentTime ) { return new DefaultEntityStoreUnitOfWork( entityStoreSPI, newUnitOfWorkId(), module, usecase, currentTime ); } public Input<EntityState, EntityStoreException> entityStates( final ModuleSPI module ) { return new Input<EntityState, EntityStoreException>() { @Override public <ReceiverThrowableType extends Throwable> void transferTo(Output<? super EntityState, ReceiverThrowableType> output) throws EntityStoreException, ReceiverThrowableType { output.receiveFrom( new Sender<EntityState, EntityStoreException>() { @Override public <ReceiverThrowableType extends Throwable> void sendTo(Receiver<? super EntityState, ReceiverThrowableType> receiver) throws ReceiverThrowableType, EntityStoreException { Connection connection = null; PreparedStatement ps = null; ResultSet rs = null; UsecaseBuilder builder = UsecaseBuilder.buildUsecase( "qi4j.entitystore.sql.visit" ); Usecase usecase = builder.with( CacheOptions.NEVER ).newUsecase(); final DefaultEntityStoreUnitOfWork uow = new DefaultEntityStoreUnitOfWork( entityStoreSPI, newUnitOfWorkId(), module, usecase, System.currentTimeMillis() ); try { connection = database.getConnection(); ps = database.prepareGetAllEntitiesStatement( connection ); database.populateGetAllEntitiesStatement( ps ); rs = ps.executeQuery(); while( rs.next() ) { receiver.receive( readEntityState( uow, database.getEntityValue( rs ).getReader() ) ); } } catch( SQLException sqle ) { throw new EntityStoreException( sqle ); } finally { SQLUtil.closeQuietly( rs ); SQLUtil.closeQuietly( ps ); SQLUtil.closeQuietly( connection ); } } }); } }; } @SuppressWarnings( "ValueOfIncrementOrDecrementUsed" ) protected String newUnitOfWorkId() { return uuid + Integer.toHexString( count++ ); } protected DefaultEntityState readEntityState( DefaultEntityStoreUnitOfWork unitOfWork, Reader entityState ) throws EntityStoreException { try { ModuleSPI module = unitOfWork.module(); JSONObject jsonObject = new JSONObject( new JSONTokener( entityState ) ); EntityStatus status = EntityStatus.LOADED; String version = jsonObject.getString( "version" ); long modified = jsonObject.getLong( "modified" ); String identity = jsonObject.getString( "identity" ); // Check if version is correct String currentAppVersion = jsonObject.optString( MapEntityStore.JSONKeys.application_version.name(), "0.0" ); if( !currentAppVersion.equals( application.version() ) ) { if( migration != null ) { migration.migrate( jsonObject, application.version(), this ); } else { // Do nothing - set version to be correct jsonObject.put( MapEntityStore.JSONKeys.application_version.name(), application.version() ); } LOGGER.trace( "Updated version nr on {} from {} to {}", new Object[]{ identity, currentAppVersion, application.version() } ); // State changed status = EntityStatus.UPDATED; } String type = jsonObject.getString( "type" ); EntityDescriptor entityDescriptor = module.entityDescriptor( type ); if( entityDescriptor == null ) { throw new EntityTypeNotFoundException( type ); } Map<QualifiedName, Object> properties = new HashMap<QualifiedName, Object>(); JSONObject props = jsonObject.getJSONObject( "properties" ); for( PropertyDescriptor propertyDescriptor : entityDescriptor.state().properties() ) { Object jsonValue; try { jsonValue = props.get( propertyDescriptor.qualifiedName().name() ); } catch( JSONException e ) { // Value not found, default it Object initialValue = propertyDescriptor.initialValue(); properties.put( propertyDescriptor.qualifiedName(), initialValue ); status = EntityStatus.UPDATED; continue; } if( jsonValue == JSONObject.NULL ) { properties.put( propertyDescriptor.qualifiedName(), null ); } else { Object value = ( (PropertyTypeDescriptor) propertyDescriptor ).propertyType().type().fromJSON( jsonValue, module ); properties.put( propertyDescriptor.qualifiedName(), value ); } } Map<QualifiedName, EntityReference> associations = new HashMap<QualifiedName, EntityReference>(); JSONObject assocs = jsonObject.getJSONObject( "associations" ); for( AssociationDescriptor associationType : entityDescriptor.state().associations() ) { try { Object jsonValue = assocs.get( associationType.qualifiedName().name() ); EntityReference value = jsonValue == JSONObject.NULL ? null : EntityReference.parseEntityReference( (String) jsonValue ); associations.put( associationType.qualifiedName(), value ); } catch( JSONException e ) { // Association not found, default it to null associations.put( associationType.qualifiedName(), null ); status = EntityStatus.UPDATED; } } Map<QualifiedName, List<EntityReference>> manyAssociations = createManyAssociations( jsonObject, entityDescriptor ); Map<QualifiedName, Map<String,EntityReference>> namedAssociations = createNamedAssociations( jsonObject, entityDescriptor ); return new DefaultEntityState( unitOfWork, version, modified, EntityReference.parseEntityReference( identity ), status, entityDescriptor, properties, associations, manyAssociations, namedAssociations ); } catch( JSONException e ) { throw new EntityStoreException( e ); } } private Map<QualifiedName, List<EntityReference>> createManyAssociations( JSONObject jsonObject, EntityDescriptor entityDescriptor ) throws JSONException { JSONObject manyAssocs = jsonObject.getJSONObject( "manyassociations" ); Map<QualifiedName, List<EntityReference>> manyAssociations = new HashMap<QualifiedName, List<EntityReference>>(); for( AssociationDescriptor manyAssociationType : entityDescriptor.state().manyAssociations() ) { List<EntityReference> references = new ArrayList<EntityReference>(); try { JSONArray jsonValues = manyAssocs.getJSONArray( manyAssociationType.qualifiedName().name() ); for( int i = 0; i < jsonValues.length(); i++ ) { Object jsonValue = jsonValues.getString( i ); EntityReference value = jsonValue == JSONObject.NULL ? null : EntityReference.parseEntityReference( (String) jsonValue ); references.add( value ); } manyAssociations.put( manyAssociationType.qualifiedName(), references ); } catch( JSONException e ) { // ManyAssociation not found, default to empty one manyAssociations.put( manyAssociationType.qualifiedName(), references ); } } return manyAssociations; } private Map<QualifiedName, Map<String, EntityReference>> createNamedAssociations( JSONObject jsonObject, EntityDescriptor entityDescriptor ) throws JSONException { JSONObject namedAssocs = jsonObject.getJSONObject( "namedassociations" ); Map<QualifiedName, Map<String,EntityReference>> namedAssociations = new HashMap<QualifiedName, Map<String, EntityReference>>(); for( AssociationDescriptor namedAssociationType : entityDescriptor.state().namedAssociations() ) { Map<String,EntityReference> references = new HashMap<String, EntityReference>(); try { JSONObject jsonValues = namedAssocs.getJSONObject( namedAssociationType.qualifiedName().name() ); for( String name : jsonValues ) { Object jsonValue = jsonValues.getString( name ); EntityReference value; if( jsonValue == JSONObject.NULL ) { value = null; } else { value = EntityReference.parseEntityReference((String) jsonValue ); } references.put( name, value ); } namedAssociations.put( namedAssociationType.qualifiedName(), references ); } catch( JSONException e ) { // NamedAssociation not found, default to empty one namedAssociations.put( namedAssociationType.qualifiedName(), references ); } } return namedAssociations; } public JSONObject getState( String id ) throws IOException { Reader reader = getValue( EntityReference.parseEntityReference( id ) ).getReader(); JSONObject jsonObject; try { jsonObject = new JSONObject( new JSONTokener( reader ) ); } catch( JSONException e ) { throw new IOException( e ); } reader.close(); return jsonObject; } protected EntityValueResult getValue( EntityReference ref ) { Connection connection = null; PreparedStatement ps = null; ResultSet rs = null; try { connection = database.getConnection(); ps = database.prepareGetEntityStatement( connection ); database.populateGetEntityStatement( ps, ref ); rs = ps.executeQuery(); if( !rs.next() ) { throw new EntityNotFoundException( ref ); } EntityValueResult result = database.getEntityValue( rs ); return result; } catch( SQLException sqle ) { throw new EntityStoreException( "Unable to get Entity " + ref, sqle ); } finally { SQLUtil.closeQuietly( rs ); SQLUtil.closeQuietly( ps ); SQLUtil.closeQuietly( connection ); } } protected void writeEntityState( DefaultEntityState state, Writer writer, String version ) throws EntityStoreException { try { JSONWriter json = new JSONWriter( writer ); JSONWriter properties = json.object(). key( "identity" ).value( state.identity().identity() ). key( "application_version" ).value( application.version() ). key( "type" ).value( state.entityDescriptor().entityType().type().name() ). key( "version" ).value( version ). key( "modified" ).value( state.lastModified() ). key( "properties" ).object(); EntityType entityType = state.entityDescriptor().entityType(); for( PropertyType propertyType : entityType.properties() ) { Object value = state.properties().get( propertyType.qualifiedName() ); json.key( propertyType.qualifiedName().name() ); if( value == null ) { json.value( null ); } else { propertyType.type().toJSON( value, json ); } } JSONWriter associations = properties.endObject().key( "associations" ).object(); for( Map.Entry<QualifiedName, EntityReference> stateNameEntityReferenceEntry : state.associations().entrySet() ) { EntityReference value = stateNameEntityReferenceEntry.getValue(); associations.key( stateNameEntityReferenceEntry.getKey().name() ). value( value != null ? value.identity() : null ); } JSONWriter manyAssociations = associations.endObject().key( "manyassociations" ).object(); for( Map.Entry<QualifiedName, List<EntityReference>> stateNameListEntry : state.manyAssociations().entrySet() ) { JSONWriter assocs = manyAssociations.key( stateNameListEntry.getKey().name() ).array(); for( EntityReference entityReference : stateNameListEntry.getValue() ) { assocs.value( entityReference.identity() ); } assocs.endArray(); } JSONWriter namedAssociations = associations.endObject().key( "namedassociations" ).object(); for( Map.Entry<QualifiedName, Map<String,EntityReference>> stateNameListEntry : state.namedAssociations().entrySet() ) { JSONWriter assocs = namedAssociations.key( stateNameListEntry.getKey().name() ).object(); Map<String, EntityReference> value = stateNameListEntry.getValue(); for( Map.Entry<String,EntityReference> entry : value.entrySet() ) { assocs.key( entry.getKey() ).value( entry.getValue() ); } assocs.endObject(); } manyAssociations.endObject().endObject(); } catch( JSONException e ) { throw new EntityStoreException( "Could not store EntityState", e ); } } }