# MongoMapEntityStoreMixin.java

    /*
     * Copyright 2011 Paul Merlin.
     *
     * 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.mongodb;

    import com.mongodb.BasicDBObject;
    import com.mongodb.DB;
    import com.mongodb.DBCollection;
    import com.mongodb.DBCursor;
    import com.mongodb.DBObject;
    import com.mongodb.MongoClient;
    import com.mongodb.MongoCredential;
    import com.mongodb.ServerAddress;
    import com.mongodb.WriteConcern;
    import com.mongodb.util.JSON;
    import java.io.IOException;
    import java.io.Reader;
    import java.io.StringReader;
    import java.io.StringWriter;
    import java.io.Writer;
    import java.net.UnknownHostException;
    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.List;
    import org.qi4j.api.configuration.Configuration;
    import org.qi4j.api.entity.EntityDescriptor;
    import org.qi4j.api.entity.EntityReference;
    import org.qi4j.api.injection.scope.This;
    import org.qi4j.api.service.ServiceActivation;
    import org.qi4j.io.Input;
    import org.qi4j.io.Output;
    import org.qi4j.io.Receiver;
    import org.qi4j.io.Sender;
    import org.qi4j.spi.entitystore.EntityNotFoundException;
    import org.qi4j.spi.entitystore.EntityStoreException;
    import org.qi4j.spi.entitystore.helpers.MapEntityStore;

    /**
     * MongoDB implementation of MapEntityStore.
     */
    public class MongoMapEntityStoreMixin
        implements ServiceActivation, MapEntityStore, MongoAccessors
    {
        private static final String DEFAULT_DATABASE_NAME = "qi4j:entitystore";
        private static final String DEFAULT_COLLECTION_NAME = "qi4j:entitystore:entities";
        public static final String IDENTITY_COLUMN = "_id";
        public static final String STATE_COLUMN = "state";
        @This
        private Configuration<MongoEntityStoreConfiguration> configuration;
        private List<ServerAddress> serverAddresses;
        private String databaseName;
        private String collectionName;
        private WriteConcern writeConcern;
        private String username;
        private char[] password;
        private MongoClient mongo;
        private DB db;

        @Override
        public void activateService()
            throws Exception
        {
            loadConfiguration();

            // Create Mongo driver and open the database
            if( username.isEmpty() )
            {
                mongo = new MongoClient( serverAddresses );
            }
            else
            {
                MongoCredential credential = MongoCredential.createMongoCRCredential( username, databaseName, password );
                mongo = new MongoClient( serverAddresses, Arrays.asList( credential ) );
            }
            db = mongo.getDB( databaseName );

            // Create index if needed
            db.requestStart();
            DBCollection entities = db.getCollection( collectionName );
            if( entities.getIndexInfo().isEmpty() )
            {
                entities.createIndex( new BasicDBObject( IDENTITY_COLUMN, 1 ) );
            }
            db.requestDone();
        }

        private void loadConfiguration()
            throws UnknownHostException
        {
            configuration.refresh();
            MongoEntityStoreConfiguration config = configuration.get();

            // Combine hostname, port and nodes configuration properties
            // If no configuration, use 127.0.0.1:27017
            serverAddresses = new ArrayList<>();
            int port = config.port().get() == null ? 27017 : config.port().get();
            if( config.nodes().get().isEmpty() )
            {
                String hostname = config.hostname().get() == null ? "127.0.0.1" : config.hostname().get();
                serverAddresses.add( new ServerAddress( hostname, port ) );
            }
            else
            {
                if( config.hostname().get() != null && !config.hostname().get().isEmpty() )
                {
                    serverAddresses.add( new ServerAddress( config.hostname().get(), port ) );
                }
                serverAddresses.addAll( config.nodes().get() );
            }

            // If database name not configured, set it to qi4j:entitystore
            databaseName = config.database().get();
            if( databaseName == null )
            {
                databaseName = DEFAULT_DATABASE_NAME;
            }

            // If collection name not configured, set it to qi4j:entitystore:entities
            collectionName = config.collection().get();
            if( collectionName == null )
            {
                collectionName = DEFAULT_COLLECTION_NAME;
            }

            // If write concern not configured, set it to normal
            switch( config.writeConcern().get() )
            {
                case FSYNC_SAFE:
                    writeConcern = WriteConcern.FSYNC_SAFE;
                    break;
                case JOURNAL_SAFE:
                    writeConcern = WriteConcern.JOURNAL_SAFE;
                    break;
                case MAJORITY:
                    writeConcern = WriteConcern.MAJORITY;
                    break;
                case REPLICAS_SAFE:
                    writeConcern = WriteConcern.REPLICAS_SAFE;
                    break;
                case SAFE:
                    writeConcern = WriteConcern.SAFE;
                    break;
                case NORMAL:
                default:
                    writeConcern = WriteConcern.NORMAL;
            }

            // Username and password are defaulted to empty strings
            username = config.username().get();
            password = config.password().get().toCharArray();
        }

        @Override
        public void passivateService()
            throws Exception
        {
            mongo.close();
            mongo = null;
            databaseName = null;
            collectionName = null;
            writeConcern = null;
            username = null;
            Arrays.fill( password, ' ' );
            password = null;
            db = null;
        }

        @Override
        public MongoClient mongoInstanceUsed()
        {
            return mongo;
        }

        @Override
        public DB dbInstanceUsed()
        {
            return db;
        }

        @Override
        public String collectionUsed()
        {
            return collectionName;
        }

        @Override
        public Reader get( EntityReference entityReference )
            throws EntityStoreException
        {
            db.requestStart();

            DBObject entity = db.getCollection( collectionName ).findOne( byIdentity( entityReference ) );
            if( entity == null )
            {
                throw new EntityNotFoundException( entityReference );
            }
            DBObject bsonState = (DBObject) entity.get( STATE_COLUMN );

            db.requestDone();

            String jsonState = JSON.serialize( bsonState );
            return new StringReader( jsonState );
        }

        @Override
        public void applyChanges( MapChanges changes )
            throws IOException
        {
            db.requestStart();
            final DBCollection entities = db.getCollection( collectionName );

            changes.visitMap( new MapChanger()
            {
                @Override
                public Writer newEntity( final EntityReference ref, EntityDescriptor entityDescriptor )
                    throws IOException
                {
                    return new StringWriter( 1000 )
                    {
                        @Override
                        public void close()
                            throws IOException
                        {
                            super.close();

                            String jsonState = toString();
                            DBObject bsonState = (DBObject) JSON.parse( jsonState );

                            BasicDBObject entity = new BasicDBObject();
                            entity.put( IDENTITY_COLUMN, ref.identity() );
                            entity.put( STATE_COLUMN, bsonState );
                            entities.insert( entity, writeConcern );
                        }
                    };
                }

                @Override
                public Writer updateEntity( final EntityReference ref, EntityDescriptor entityDescriptor )
                    throws IOException
                {
                    return new StringWriter( 1000 )
                    {
                        @Override
                        public void close()
                            throws IOException
                        {
                            super.close();

                            DBObject bsonState = (DBObject) JSON.parse( toString() );

                            BasicDBObject entity = new BasicDBObject();
                            entity.put( IDENTITY_COLUMN, ref.identity() );
                            entity.put( STATE_COLUMN, bsonState );
                            entities.update( byIdentity( ref ), entity, false, false, writeConcern );
                        }
                    };
                }

                @Override
                public void removeEntity( EntityReference ref, EntityDescriptor entityDescriptor )
                    throws EntityNotFoundException
                {
                    DBObject entity = entities.findOne( byIdentity( ref ) );
                    if( entity == null )
                    {
                        throw new EntityNotFoundException( ref );
                    }
                    entities.remove( entity, writeConcern );
                }
            } );

            db.requestDone();
        }

        @Override
        public Input<Reader, IOException> entityStates()
        {
            return new Input<Reader, IOException>()
            {
                @Override
                public <ReceiverThrowableType extends Throwable> void transferTo(
                    Output<? super Reader, ReceiverThrowableType> output )
                    throws IOException, ReceiverThrowableType
                {
                    output.receiveFrom( new Sender<Reader, IOException>()
                    {
                        @Override
                        public <ReceiverThrowableType extends Throwable> void sendTo(
                            Receiver<? super Reader, ReceiverThrowableType> receiver )
                            throws ReceiverThrowableType, IOException
                        {
                            db.requestStart();

                            DBCursor cursor = db.getCollection( collectionName ).find();
                            while( cursor.hasNext() )
                            {
                                DBObject eachEntity = cursor.next();
                                DBObject bsonState = (DBObject) eachEntity.get( STATE_COLUMN );
                                String jsonState = JSON.serialize( bsonState );
                                receiver.receive( new StringReader( jsonState ) );
                            }

                            db.requestDone();
                        }
                    } );
                }
            };
        }

        private DBObject byIdentity( EntityReference entityReference )
        {
            return new BasicDBObject( IDENTITY_COLUMN, entityReference.identity() );
        }
    }