/* * Copyright (c) 2008, Rickard Öberg. 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.prefs; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.json.JSONStringer; import org.json.JSONTokener; import org.qi4j.api.cache.CacheOptions; import org.qi4j.api.common.QualifiedName; import org.qi4j.api.entity.EntityReference; import org.qi4j.api.injection.scope.Structure; import org.qi4j.api.injection.scope.This; import org.qi4j.api.injection.scope.Uses; 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.unitofwork.NoSuchEntityException; import org.qi4j.api.usecase.Usecase; import org.qi4j.api.usecase.UsecaseBuilder; 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.ManyAssociationState; import org.qi4j.spi.entity.NamedAssociationState; import org.qi4j.spi.entity.association.AssociationDescriptor; import org.qi4j.spi.entity.association.AssociationType; import org.qi4j.spi.entity.association.NamedEntityReference; import org.qi4j.spi.entitystore.DefaultEntityStoreUnitOfWork; 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.PropertyType; import org.qi4j.spi.property.PropertyTypeDescriptor; import org.qi4j.spi.property.ValueType; import org.qi4j.spi.service.ServiceDescriptor; import org.qi4j.spi.structure.ModuleSPI; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.prefs.BackingStoreException; import java.util.prefs.Preferences; /** * Implementation of EntityStore that is backed by the Preferences API. * * @see Preferences */ public class PreferencesEntityStoreMixin implements Activatable, EntityStore, EntityStoreSPI { @This private EntityStoreSPI entityStoreSpi; @Uses private ServiceDescriptor descriptor; @Structure private Application application; private Preferences root; protected String uuid; private int count; public Logger logger; public ScheduledThreadPoolExecutor reloadExecutor; public void activate() throws Exception { root = getApplicationRoot(); logger = LoggerFactory.getLogger( PreferencesEntityStoreService.class.getName() ); logger.info( "Preferences store:" + root.absolutePath() ); uuid = UUID.randomUUID().toString() + "-"; // Reload underlying store every 60 seconds reloadExecutor = new ScheduledThreadPoolExecutor( 1 ); reloadExecutor.setExecuteExistingDelayedTasksAfterShutdownPolicy( false ); reloadExecutor.scheduleAtFixedRate( new Runnable() { public void run() { try { synchronized( root ) { root.sync(); } } catch( BackingStoreException e ) { logger.warn( "Could not reload preferences", e ); } } }, 0, 60, TimeUnit.SECONDS ); } private Preferences getApplicationRoot() { PreferencesEntityStoreInfo storeInfo = descriptor.metaInfo( PreferencesEntityStoreInfo.class ); Preferences preferences; if( storeInfo == null ) { // Default to use system root + application name preferences = Preferences.systemRoot(); String name = application.name(); preferences = preferences.node( name ); } else { preferences = storeInfo.getRootNode(); } return preferences; } public void passivate() throws Exception { reloadExecutor.shutdown(); reloadExecutor.awaitTermination( 10, TimeUnit.SECONDS ); } 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 { UsecaseBuilder builder = UsecaseBuilder.buildUsecase( "qi4j.entitystore.preferences.visit" ); Usecase visitUsecase = builder.with( CacheOptions.NEVER ).newUsecase(); final DefaultEntityStoreUnitOfWork uow = new DefaultEntityStoreUnitOfWork( entityStoreSpi, newUnitOfWorkId(), module, visitUsecase, System.currentTimeMillis()); try { String[] identities = root.childrenNames(); for( String identity : identities ) { EntityState entityState = uow.getEntityState( EntityReference.parseEntityReference( identity ) ); receiver.receive( entityState ); } } catch( BackingStoreException e ) { throw new EntityStoreException( e ); } } }); } }; } public EntityState newEntityState( EntityStoreUnitOfWork unitOfWork, EntityReference identity, EntityDescriptor entityDescriptor ) { return new DefaultEntityState( (DefaultEntityStoreUnitOfWork) unitOfWork, identity, entityDescriptor ); } public EntityState getEntityState( EntityStoreUnitOfWork unitOfWork, EntityReference identity ) { try { DefaultEntityStoreUnitOfWork desuw = (DefaultEntityStoreUnitOfWork) unitOfWork; ModuleSPI module = desuw.module(); if( !root.nodeExists( identity.identity() ) ) { throw new NoSuchEntityException( identity ); } Preferences entityPrefs = root.node( identity.identity() ); String type = entityPrefs.get( "type", null ); EntityStatus status = EntityStatus.LOADED; EntityDescriptor entityDescriptor = module.entityDescriptor( type ); if( entityDescriptor == null ) { throw new EntityTypeNotFoundException( type ); } Map<QualifiedName, Object> properties = new HashMap<QualifiedName, Object>(); if( !entityDescriptor.state().properties().isEmpty() ) { Preferences propsPrefs = entityPrefs.node( "properties" ); for( PropertyTypeDescriptor propertyDescriptor : entityDescriptor.state() .<PropertyTypeDescriptor>properties() ) { if( propertyDescriptor.qualifiedName().name().equals( "identity" ) ) { // Fake identity property properties.put( propertyDescriptor.qualifiedName(), identity.identity() ); continue; } ValueType propertyType = propertyDescriptor.propertyType().type(); if( propertyType.isNumber() ) { if( propertyType.type().name().equals( "java.lang.Long" ) ) { properties.put( propertyDescriptor.qualifiedName(), this.getNumber( propsPrefs, propertyDescriptor, new NumberParser<Long>() { public Long parse( String str ) { return Long.parseLong( str ); } } ) ); } else if( propertyType.type().name().equals( "java.lang.Integer" ) ) { properties.put( propertyDescriptor.qualifiedName(), this.getNumber( propsPrefs, propertyDescriptor, new NumberParser<Integer>() { public Integer parse( String str ) { return Integer.parseInt( str ); } } ) ); } else if( propertyType.type().name().equals( "java.lang.Double" ) ) { properties.put( propertyDescriptor.qualifiedName(), this.getNumber( propsPrefs, propertyDescriptor, new NumberParser<Double>() { public Double parse( String str ) { return Double.parseDouble( str ); } } ) ); } else if( propertyType.type().name().equals( "java.lang.Float" ) ) { properties.put( propertyDescriptor.qualifiedName(), this.getNumber( propsPrefs, propertyDescriptor, new NumberParser<Float>() { public Float parse( String str ) { return Float.parseFloat( str ); } } ) ); } else { // Load as string even though it's a number String json = propsPrefs.get( propertyDescriptor.qualifiedName().name(), "null" ); json = "[" + json + "]"; JSONTokener tokener = new JSONTokener( json ); JSONArray array = (JSONArray) tokener.nextValue(); Object jsonValue = array.get( 0 ); Object value; if( jsonValue == JSONObject.NULL ) { value = null; } else { value = propertyDescriptor.propertyType().type().fromJSON( jsonValue, module ); } properties.put( propertyDescriptor.qualifiedName(), value ); } } else if( propertyType.isBoolean() ) { Boolean initialValue = (Boolean) propertyDescriptor.initialValue(); properties.put( propertyDescriptor.qualifiedName(), propsPrefs.getBoolean( propertyDescriptor.qualifiedName().name(), initialValue == null ? false : initialValue) ); } else if( propertyType.isValue() ) { String json = propsPrefs.get( propertyDescriptor.qualifiedName().name(), "null" ); JSONTokener tokener = new JSONTokener( json ); Object composite = tokener.nextValue(); if( composite.equals( JSONObject.NULL ) ) { properties.put( propertyDescriptor.qualifiedName(), null ); } else { Object value = propertyType.fromJSON( composite, module ); properties.put( propertyDescriptor.qualifiedName(), value ); } } else if( propertyType.isString() ) { String json = propsPrefs.get( propertyDescriptor.qualifiedName().name(), (String) propertyDescriptor .initialValue() ); if( json == null ) { properties.put( propertyDescriptor.qualifiedName(), null ); } else { Object value = propertyType.fromJSON( json, module ); properties.put( propertyDescriptor.qualifiedName(), value ); } } else { String json = propsPrefs.get( propertyDescriptor.qualifiedName().name(), "null" ); json = "[" + json + "]"; JSONTokener tokener = new JSONTokener( json ); JSONArray array = (JSONArray) tokener.nextValue(); Object jsonValue = array.get( 0 ); Object value; if( jsonValue == JSONObject.NULL ) { value = null; } else { value = propertyDescriptor.propertyType().type().fromJSON( jsonValue, module ); } properties.put( propertyDescriptor.qualifiedName(), value ); } } } // Associations Map<QualifiedName, EntityReference> associations = new HashMap<QualifiedName, EntityReference>(); if( !entityDescriptor.state().associations().isEmpty() ) { Preferences assocs = entityPrefs.node( "associations" ); for( AssociationDescriptor associationType : entityDescriptor.state().associations() ) { String associatedEntity = assocs.get( associationType.qualifiedName().name(), null ); EntityReference value = associatedEntity == null ? null : EntityReference.parseEntityReference( associatedEntity ); associations.put( associationType.qualifiedName(), value ); } } Map<QualifiedName, List<EntityReference>> manyAssociations = createManyAssociations( entityPrefs, entityDescriptor ); Map<QualifiedName, Map<String,EntityReference>> namedAssociations = createNamedAssociations( entityPrefs, entityDescriptor ); return new DefaultEntityState( desuw, entityPrefs.get( "version", "" ), entityPrefs.getLong( "modified", unitOfWork.currentTime() ), identity, status, entityDescriptor, properties, associations, manyAssociations, namedAssociations ); } catch( JSONException e ) { throw new EntityStoreException( e ); } catch( BackingStoreException e ) { throw new EntityStoreException( e ); } } private Map<QualifiedName, List<EntityReference>> createManyAssociations( Preferences entityPrefs, EntityDescriptor entityDescriptor ) { Map<QualifiedName, List<EntityReference>> manyAssociations = new HashMap<QualifiedName, List<EntityReference>>(); if( !entityDescriptor.state().manyAssociations().isEmpty() ) { Preferences manyAssocs = entityPrefs.node( "manyassociations" ); for( AssociationDescriptor manyAssociationType : entityDescriptor.state().manyAssociations() ) { List<EntityReference> references = new ArrayList<EntityReference>(); String entityReferences = manyAssocs.get( manyAssociationType.qualifiedName().name(), null ); if( entityReferences == null ) { // ManyAssociation not found, default to empty one manyAssociations.put( manyAssociationType.qualifiedName(), references ); } else { String[] refs = entityReferences.split( "\n" ); for( String ref : refs ) { EntityReference value = ref == null ? null : EntityReference.parseEntityReference( ref ); references.add( value ); } manyAssociations.put( manyAssociationType.qualifiedName(), references ); } } } return manyAssociations; } private Map<QualifiedName, Map<String, EntityReference>> createNamedAssociations( Preferences entityPrefs, EntityDescriptor entityDescriptor ) throws BackingStoreException { Map<QualifiedName, Map<String,EntityReference>> namedAssociations = new HashMap<QualifiedName, Map<String,EntityReference>>(); if( !entityDescriptor.state().namedAssociations().isEmpty() ) { Preferences prefNode = entityPrefs.node( "namedassociations" ); for( AssociationDescriptor namedAssociationType : entityDescriptor.state().namedAssociations() ) { Map<String, EntityReference> references = new HashMap<String,EntityReference>(); Preferences namedNodes = prefNode.node( namedAssociationType.qualifiedName().name()); for( String name : namedNodes.keys() ) { String ref = namedNodes.get( name, null ); EntityReference value = ref == null ? null : EntityReference.parseEntityReference( ref ); references.put( name, value ); namedAssociations.put( namedAssociationType.qualifiedName(), references ); } } } return namedAssociations; } public StateCommitter applyChanges( final EntityStoreUnitOfWork unitofwork, final Iterable<EntityState> state ) { return new StateCommitter() { public void commit() { try { synchronized( root ) { for( EntityState entityState : state ) { DefaultEntityState state = (DefaultEntityState) entityState; if( state.status().equals( EntityStatus.NEW ) ) { Preferences entityPrefs = root.node( state.identity().identity() ); writeEntityState( state, entityPrefs, unitofwork.identity(), unitofwork.currentTime() ); } else if( state.status().equals( EntityStatus.UPDATED ) ) { Preferences entityPrefs = root.node( state.identity().identity() ); writeEntityState( state, entityPrefs, unitofwork.identity(), unitofwork.currentTime() ); } else if( state.status().equals( EntityStatus.REMOVED ) ) { root.node( state.identity().identity() ).removeNode(); } } root.flush(); } } catch( BackingStoreException e ) { throw new EntityStoreException( e ); } } public void cancel() { } }; } protected void writeEntityState( DefaultEntityState state, Preferences entityPrefs, String identity, long lastModified ) throws EntityStoreException { try { // Store into Preferences API EntityType entityType = state.entityDescriptor().entityType(); entityPrefs.put( "type", state.entityDescriptor().entityType().type().name() ); entityPrefs.put( "version", identity ); entityPrefs.putLong( "modified", lastModified ); // Properties Preferences propsPrefs = entityPrefs.node( "properties" ); for( PropertyType propertyType : entityType.properties() ) { if( propertyType.qualifiedName().name().equals( "identity" ) ) { continue; // Skip Identity.identity() } Object value = state.properties().get( propertyType.qualifiedName() ); if( value == null ) { propsPrefs.remove( propertyType.qualifiedName().name() ); } else { if( propertyType.type().isNumber() ) { if( propertyType.type().type().name().equals( "java.lang.Long" ) ) { propsPrefs.putLong( propertyType.qualifiedName().name(), (Long) value ); } else if( propertyType.type().type().name().equals( "java.lang.Integer" ) ) { propsPrefs.putInt( propertyType.qualifiedName().name(), (Integer) value ); } else if( propertyType.type().type().name().equals( "java.lang.Double" ) ) { propsPrefs.putDouble( propertyType.qualifiedName().name(), (Double) value ); } else if( propertyType.type().type().name().equals( "java.lang.Float" ) ) { propsPrefs.putFloat( propertyType.qualifiedName().name(), (Float) value ); } else { // Store as string even though it's a number JSONStringer json = new JSONStringer(); json.array(); propertyType.type().toJSON( value, json ); json.endArray(); String jsonString = json.toString(); jsonString = jsonString.substring( 1, jsonString.length() - 1 ); propsPrefs.put( propertyType.qualifiedName().name(), jsonString ); } } else if( propertyType.type().isBoolean() ) { propsPrefs.putBoolean( propertyType.qualifiedName().name(), (Boolean) value ); } else if( propertyType.type().isValue() ) { JSONStringer json = new JSONStringer(); propertyType.type().toJSON( value, json ); String jsonString = json.toString(); propsPrefs.put( propertyType.qualifiedName().name(), jsonString ); } else if( propertyType.type().isString() ) { JSONStringer json = new JSONStringer(); json.array(); propertyType.type().toJSON( value, json ); json.endArray(); String jsonString = json.toString(); jsonString = jsonString.substring( 2, jsonString.length() - 2 ); propsPrefs.put( propertyType.qualifiedName().name(), jsonString ); } else { JSONStringer json = new JSONStringer(); json.array(); propertyType.type().toJSON( value, json ); json.endArray(); String jsonString = json.toString(); jsonString = jsonString.substring( 1, jsonString.length() - 1 ); propsPrefs.put( propertyType.qualifiedName().name(), jsonString ); } } } // Associations if( !entityType.associations().isEmpty() ) { Preferences assocsPrefs = entityPrefs.node( "associations" ); for( AssociationType associationType : entityType.associations() ) { EntityReference ref = state.getAssociation( associationType.qualifiedName() ); if( ref == null ) { assocsPrefs.remove( associationType.qualifiedName().name() ); } else { assocsPrefs.put( associationType.qualifiedName().name(), ref.identity() ); } } } // ManyAssociations if( !entityType.manyAssociations().isEmpty() ) { Preferences manyAssocsPrefs = entityPrefs.node( "manyassociations" ); for( AssociationType manyAssociationType : entityType.manyAssociations() ) { String manyAssocs = ""; ManyAssociationState manyAssoc = state.getManyAssociation( manyAssociationType.qualifiedName() ); for( EntityReference entityReference : manyAssoc ) { if( manyAssocs.length() > 0 ) { manyAssocs += "\n"; } manyAssocs += entityReference.identity(); } manyAssocsPrefs.put( manyAssociationType.qualifiedName().name(), manyAssocs ); } } // NamedAssociations if( !entityType.namedAssociations().isEmpty() ) { Preferences assocsNode = entityPrefs.node( "namedassociations" ); for( AssociationType namedAssociationType : entityType.namedAssociations() ) { Preferences namedNode = assocsNode.node( namedAssociationType.qualifiedName().name()); NamedAssociationState namedAssoc = state.getNamedAssociation( namedAssociationType.qualifiedName() ); for( NamedEntityReference namedReference : namedAssoc ) { namedNode.put( namedReference.name(), namedReference.entityReference().identity() ); } } } } catch( JSONException e ) { throw new EntityStoreException( "Could not store EntityState", e ); } } protected String newUnitOfWorkId() { return uuid + Integer.toHexString( count++ ); } private interface NumberParser<T> { T parse( String str ); } private <T> T getNumber( Preferences prefs, PropertyTypeDescriptor pDesc, NumberParser<T> parser ) { Object initialValue = pDesc.initialValue(); String str = prefs.get( pDesc.qualifiedName().name(), initialValue == null ? null : initialValue.toString() ); T result = null; if( str != null ) { result = parser.parse( str ); } return result; } }