# PreferencesEntityStoreMixin.java

    /*
     * Copyright (c) 2008-2011, Rickard Öberg. All Rights Reserved.
     * Copyright (c) 2013, Niclas Hedhman. All Rights Reserved.
     * Copyright (c) 2014, Paul Merlin. 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 java.util.ArrayList;
    import java.util.HashMap;
    import java.util.LinkedHashMap;
    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;
    import org.qi4j.api.association.AssociationDescriptor;
    import org.qi4j.api.cache.CacheOptions;
    import org.qi4j.api.common.QualifiedName;
    import org.qi4j.api.entity.EntityDescriptor;
    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.injection.scope.Uses;
    import org.qi4j.api.property.PropertyDescriptor;
    import org.qi4j.api.service.ServiceActivation;
    import org.qi4j.api.service.ServiceDescriptor;
    import org.qi4j.api.service.qualifier.Tagged;
    import org.qi4j.api.structure.Application;
    import org.qi4j.api.type.CollectionType;
    import org.qi4j.api.type.EnumType;
    import org.qi4j.api.type.MapType;
    import org.qi4j.api.type.ValueCompositeType;
    import org.qi4j.api.type.ValueType;
    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.api.value.ValueSerialization;
    import org.qi4j.api.value.ValueSerializationException;
    import org.qi4j.io.Input;
    import org.qi4j.io.Output;
    import org.qi4j.io.Receiver;
    import org.qi4j.io.Sender;
    import org.qi4j.spi.Qi4jSPI;
    import org.qi4j.spi.entity.EntityState;
    import org.qi4j.spi.entity.EntityStatus;
    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.ModuleEntityStoreUnitOfWork;
    import org.qi4j.spi.entitystore.StateCommitter;
    import org.qi4j.spi.entitystore.helpers.DefaultEntityState;
    import org.qi4j.spi.module.ModelModule;
    import org.qi4j.spi.module.ModuleSpi;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;

    import static org.qi4j.functional.Iterables.first;
    import static org.qi4j.functional.Iterables.map;

    /**
     * Implementation of EntityStore that is backed by the Preferences API.
     *
     * <p>@see Preferences</p>
     * <p>
     * Associations are stored as the identity of the referenced Entity, ManyAssociations are stored as multi-line strings
     * (one identity per line), and NamedAssociations are stored as multi-line strings (one name on a line, identity on the
     * next line).
     * </p>
     * <p>Nested ValuesComposites, Collections and Maps are stored using available ValueSerialization service.</p>
     */
    public class PreferencesEntityStoreMixin
        implements ServiceActivation, EntityStore, EntityStoreSPI
    {
        @Structure
        private Qi4jSPI spi;

        @This
        private EntityStoreSPI entityStoreSpi;

        @Uses
        private ServiceDescriptor descriptor;

        @Structure
        private Application application;

        @Service
        @Tagged( ValueSerialization.Formats.JSON )
        private ValueSerialization valueSerialization;

        private Preferences root;
        protected String uuid;
        private int count;
        public Logger logger;
        public ScheduledThreadPoolExecutor reloadExecutor;

        @Override
        public void activateService()
            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()
            {
                @Override
                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.rootNode();
            }

            return preferences;
        }

        @Override
        public void passivateService()
            throws Exception
        {
            reloadExecutor.shutdown();
            reloadExecutor.awaitTermination( 10, TimeUnit.SECONDS );
        }

        @Override
        public EntityStoreUnitOfWork newUnitOfWork( Usecase usecase, ModuleSpi module, long currentTime )
        {
            EntityStoreUnitOfWork storeUnitOfWork = new DefaultEntityStoreUnitOfWork( entityStoreSpi, newUnitOfWorkId(), usecase, currentTime );
            storeUnitOfWork = new ModuleEntityStoreUnitOfWork( module, storeUnitOfWork );
            return storeUnitOfWork;
        }

        @Override
        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.withMetaInfo( CacheOptions.NEVER ).newUsecase();
                            final EntityStoreUnitOfWork uow =
                                newUnitOfWork( visitUsecase, module, System.currentTimeMillis() );

                            try
                            {
                                String[] identities = root.childrenNames();
                                for( String identity : identities )
                                {
                                    EntityReference reference = EntityReference.parseEntityReference( identity );
                                    EntityState entityState = uow.entityStateOf( module, reference );
                                    receiver.receive( entityState );
                                }
                            }
                            catch( BackingStoreException e )
                            {
                                throw new EntityStoreException( e );
                            }
                        }
                    } );
                }
            };
        }

        @Override
        public EntityState newEntityState( EntityStoreUnitOfWork unitOfWork,
                                           ModuleSpi module,
                                           EntityReference identity,
                                           EntityDescriptor entityDescriptor
        )
        {
            return new DefaultEntityState( unitOfWork.currentTime(), identity, entityDescriptor );
        }

        @Override
        public EntityState entityStateOf( EntityStoreUnitOfWork unitOfWork, ModuleSpi module, EntityReference identity )
        {
            try
            {
                if( !root.nodeExists( identity.identity() ) )
                {
                    throw new NoSuchEntityException( identity, UnknownType.class, unitOfWork.usecase() );
                }

                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,
                                                           module.name(),
                                                           map( ModelModule.toStringFunction,
                                                                module.findVisibleEntityTypes()
                                                           ) );
                }

                Map<QualifiedName, Object> properties = new HashMap<>();
                Preferences propsPrefs = null;
                for( PropertyDescriptor persistentPropertyDescriptor : entityDescriptor.state().properties() )
                {
                    if( persistentPropertyDescriptor.qualifiedName().name().equals( "identity" ) )
                    {
                        // Fake identity property
                        properties.put( persistentPropertyDescriptor.qualifiedName(), identity.identity() );
                        continue;
                    }

                    if( propsPrefs == null )
                    {
                        propsPrefs = entityPrefs.node( "properties" );
                    }

                    ValueType propertyType = persistentPropertyDescriptor.valueType();
                    Class<?> mainType = propertyType.mainType();
                    if( Number.class.isAssignableFrom( mainType ) )
                    {
                        if( mainType.equals( Long.class ) )
                        {
                            properties.put( persistentPropertyDescriptor.qualifiedName(),
                                            this.getNumber( propsPrefs, persistentPropertyDescriptor, LONG_PARSER ) );
                        }
                        else if( mainType.equals( Integer.class ) )
                        {
                            properties.put( persistentPropertyDescriptor.qualifiedName(),
                                            this.getNumber( propsPrefs, persistentPropertyDescriptor, INT_PARSER ) );
                        }
                        else if( mainType.equals( Double.class ) )
                        {
                            properties.put( persistentPropertyDescriptor.qualifiedName(),
                                            this.getNumber( propsPrefs, persistentPropertyDescriptor, DOUBLE_PARSER ) );
                        }
                        else if( mainType.equals( Float.class ) )
                        {
                            properties.put( persistentPropertyDescriptor.qualifiedName(),
                                            this.getNumber( propsPrefs, persistentPropertyDescriptor, FLOAT_PARSER ) );
                        }
                        else
                        {
                            // Load as string even though it's a number
                            String json = propsPrefs.get( persistentPropertyDescriptor.qualifiedName().name(), null );
                            Object value;
                            if( json == null )
                            {
                                value = null;
                            }
                            else
                            {
                                value = valueSerialization.deserialize( persistentPropertyDescriptor.valueType(), json );
                            }
                            properties.put( persistentPropertyDescriptor.qualifiedName(), value );
                        }
                    }
                    else if( mainType.equals( Boolean.class ) )
                    {
                        Boolean initialValue = (Boolean) persistentPropertyDescriptor.initialValue( module );
                        properties.put( persistentPropertyDescriptor.qualifiedName(),
                                        propsPrefs.getBoolean( persistentPropertyDescriptor.qualifiedName().name(),
                                                               initialValue == null ? false : initialValue ) );
                    }
                    else if( propertyType instanceof ValueCompositeType
                             || propertyType instanceof MapType
                             || propertyType instanceof CollectionType
                             || propertyType instanceof EnumType )
                    {
                        String json = propsPrefs.get( persistentPropertyDescriptor.qualifiedName().name(), null );
                        Object value;
                        if( json == null )
                        {
                            value = null;
                        }
                        else
                        {
                            value = valueSerialization.deserialize( persistentPropertyDescriptor.valueType(), json );
                        }
                        properties.put( persistentPropertyDescriptor.qualifiedName(), value );
                    }
                    else
                    {
                        String json = propsPrefs.get( persistentPropertyDescriptor.qualifiedName().name(), null );
                        if( json == null )
                        {
                            if( persistentPropertyDescriptor.initialValue( module ) != null )
                            {
                                properties.put( persistentPropertyDescriptor.qualifiedName(), persistentPropertyDescriptor.initialValue( module ) );
                            }
                            else
                            {
                                properties.put( persistentPropertyDescriptor.qualifiedName(), null );
                            }
                        }
                        else
                        {
                            Object value = valueSerialization.deserialize( propertyType, json );
                            properties.put( persistentPropertyDescriptor.qualifiedName(), value );
                        }
                    }
                }

                // Associations
                Map<QualifiedName, EntityReference> associations = new HashMap<>();
                Preferences assocs = null;
                for( AssociationDescriptor associationType : entityDescriptor.state().associations() )
                {
                    if( assocs == null )
                    {
                        assocs = entityPrefs.node( "associations" );
                    }

                    String associatedEntity = assocs.get( associationType.qualifiedName().name(), null );
                    EntityReference value = associatedEntity == null
                                            ? null
                                            : EntityReference.parseEntityReference( associatedEntity );
                    associations.put( associationType.qualifiedName(), value );
                }

                // ManyAssociations
                Map<QualifiedName, List<EntityReference>> manyAssociations = new HashMap<>();
                Preferences manyAssocs = null;
                for( AssociationDescriptor manyAssociationType : entityDescriptor.state().manyAssociations() )
                {
                    if( manyAssocs == null )
                    {
                        manyAssocs = entityPrefs.node( "manyassociations" );
                    }

                    List<EntityReference> references = new ArrayList<>();
                    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 );
                    }
                }

                // NamedAssociations
                Map<QualifiedName, Map<String, EntityReference>> namedAssociations = new HashMap<>();
                Preferences namedAssocs = null;
                for( AssociationDescriptor namedAssociationType : entityDescriptor.state().namedAssociations() )
                {
                    if( namedAssocs == null )
                    {
                        namedAssocs = entityPrefs.node( "namedassociations" );
                    }

                    Map<String, EntityReference> references = new LinkedHashMap<>();
                    String entityReferences = namedAssocs.get( namedAssociationType.qualifiedName().name(), null );
                    if( entityReferences == null )
                    {
                        // NamedAssociation not found, default to empty one
                        namedAssociations.put( namedAssociationType.qualifiedName(), references );
                    }
                    else
                    {
                        String[] namedRefs = entityReferences.split( "\n" );
                        if( namedRefs.length % 2 != 0 )
                        {
                            throw new EntityStoreException( "Invalid NamedAssociation storage format" );
                        }
                        for( int idx = 0; idx < namedRefs.length; idx += 2 )
                        {
                            String name = namedRefs[ idx ];
                            String ref = namedRefs[ idx + 1 ];
                            references.put( name, EntityReference.parseEntityReference( ref ) );
                        }
                        namedAssociations.put( namedAssociationType.qualifiedName(), references );
                    }
                }

                return new DefaultEntityState( entityPrefs.get( "version", "" ),
                                               entityPrefs.getLong( "modified", unitOfWork.currentTime() ),
                                               identity,
                                               status,
                                               entityDescriptor,
                                               properties,
                                               associations,
                                               manyAssociations,
                                               namedAssociations
                );
            }
            catch( ValueSerializationException | BackingStoreException e )
            {
                throw new EntityStoreException( e );
            }
        }

        @Override
        public StateCommitter applyChanges( final EntityStoreUnitOfWork unitofwork, final Iterable<EntityState> state )
        {
            return new StateCommitter()
            {
                @SuppressWarnings( "SynchronizeOnNonFinalField" )
                @Override
                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 );
                    }
                }

                @Override
                public void cancel()
                {
                }
            };
        }

        protected void writeEntityState( DefaultEntityState state,
                                         Preferences entityPrefs,
                                         String identity,
                                         long lastModified
        )
            throws EntityStoreException
        {
            try
            {
                // Store into Preferences API
                entityPrefs.put( "type", first( state.entityDescriptor().types() ).getName() );
                entityPrefs.put( "version", identity );
                entityPrefs.putLong( "modified", lastModified );

                // Properties
                Preferences propsPrefs = entityPrefs.node( "properties" );
                for( PropertyDescriptor persistentProperty : state.entityDescriptor().state().properties() )
                {
                    if( persistentProperty.qualifiedName().name().equals( "identity" ) )
                    {
                        continue; // Skip Identity.identity()
                    }

                    Object value = state.properties().get( persistentProperty.qualifiedName() );

                    if( value == null )
                    {
                        propsPrefs.remove( persistentProperty.qualifiedName().name() );
                    }
                    else
                    {
                        ValueType valueType = persistentProperty.valueType();
                        Class<?> mainType = valueType.mainType();
                        if( Number.class.isAssignableFrom( mainType ) )
                        {
                            if( mainType.equals( Long.class ) )
                            {
                                propsPrefs.putLong( persistentProperty.qualifiedName().name(), (Long) value );
                            }
                            else if( mainType.equals( Integer.class ) )
                            {
                                propsPrefs.putInt( persistentProperty.qualifiedName().name(), (Integer) value );
                            }
                            else if( mainType.equals( Double.class ) )
                            {
                                propsPrefs.putDouble( persistentProperty.qualifiedName().name(), (Double) value );
                            }
                            else if( mainType.equals( Float.class ) )
                            {
                                propsPrefs.putFloat( persistentProperty.qualifiedName().name(), (Float) value );
                            }
                            else
                            {
                                // Store as string even though it's a number
                                String jsonString = valueSerialization.serialize( value );
                                propsPrefs.put( persistentProperty.qualifiedName().name(), jsonString );
                            }
                        }
                        else if( mainType.equals( Boolean.class ) )
                        {
                            propsPrefs.putBoolean( persistentProperty.qualifiedName().name(), (Boolean) value );
                        }
                        else if( valueType instanceof ValueCompositeType
                                 || valueType instanceof MapType
                                 || valueType instanceof CollectionType
                                 || valueType instanceof EnumType )
                        {
                            String jsonString = valueSerialization.serialize( value );
                            propsPrefs.put( persistentProperty.qualifiedName().name(), jsonString );
                        }
                        else
                        {
                            String jsonString = valueSerialization.serialize( value );
                            propsPrefs.put( persistentProperty.qualifiedName().name(), jsonString );
                        }
                    }
                }

                // Associations
                if( !state.associations().isEmpty() )
                {
                    Preferences assocsPrefs = entityPrefs.node( "associations" );
                    for( Map.Entry<QualifiedName, EntityReference> association : state.associations().entrySet() )
                    {
                        if( association.getValue() == null )
                        {
                            assocsPrefs.remove( association.getKey().name() );
                        }
                        else
                        {
                            assocsPrefs.put( association.getKey().name(), association.getValue().identity() );
                        }
                    }
                }

                // ManyAssociations
                if( !state.manyAssociations().isEmpty() )
                {
                    Preferences manyAssocsPrefs = entityPrefs.node( "manyassociations" );
                    for( Map.Entry<QualifiedName, List<EntityReference>> manyAssociation : state.manyAssociations()
                        .entrySet() )
                    {
                        StringBuilder manyAssocs = new StringBuilder();
                        for( EntityReference entityReference : manyAssociation.getValue() )
                        {
                            if( manyAssocs.length() > 0 )
                            {
                                manyAssocs.append( "\n" );
                            }
                            manyAssocs.append( entityReference.identity() );
                        }
                        if( manyAssocs.length() > 0 )
                        {
                            manyAssocsPrefs.put( manyAssociation.getKey().name(), manyAssocs.toString() );
                        }
                    }
                }

                // NamedAssociations
                if( !state.namedAssociations().isEmpty() )
                {
                    Preferences namedAssocsPrefs = entityPrefs.node( "namedassociations" );
                    for( Map.Entry<QualifiedName, Map<String, EntityReference>> namedAssociation : state.namedAssociations()
                        .entrySet() )
                    {
                        StringBuilder namedAssocs = new StringBuilder();
                        for( Map.Entry<String, EntityReference> namedRef : namedAssociation.getValue().entrySet() )
                        {
                            if( namedAssocs.length() > 0 )
                            {
                                namedAssocs.append( "\n" );
                            }
                            namedAssocs.append( namedRef.getKey() ).append( "\n" ).append( namedRef.getValue().identity() );
                        }
                        if( namedAssocs.length() > 0 )
                        {
                            namedAssocsPrefs.put( namedAssociation.getKey().name(), namedAssocs.toString() );
                        }
                    }
                }
            }
            catch( ValueSerializationException 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 static final NumberParser<Long> LONG_PARSER = new NumberParser<Long>()
        {
            @Override
            public Long parse( String str )
            {
                return Long.parseLong( str );
            }
        };

        private static final NumberParser<Integer> INT_PARSER = new NumberParser<Integer>()
        {
            @Override
            public Integer parse( String str )
            {
                return Integer.parseInt( str );
            }
        };

        private static final NumberParser<Double> DOUBLE_PARSER = new NumberParser<Double>()
        {
            @Override
            public Double parse( String str )
            {
                return Double.parseDouble( str );
            }
        };

        private static final NumberParser<Float> FLOAT_PARSER = new NumberParser<Float>()
        {
            @Override
            public Float parse( String str )
            {
                return Float.parseFloat( str );
            }
        };

        private <T> T getNumber( Preferences prefs, PropertyDescriptor pDesc, NumberParser<T> parser )
        {
            Object initialValue = pDesc.initialValue( null );
            String str = prefs.get( pDesc.qualifiedName().name(), initialValue == null ? null : initialValue.toString() );
            T result = null;
            if( str != null )
            {
                result = parser.parse( str );
            }
            return result;
        }

        private static class UnknownType
        {
        }
    }