/*
 * jPOS Project [http://jpos.org]
 * Copyright (C) 2000-2015 Alejandro P. Revilla
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.jpos.space;
import java.io.*;
import java.util.Map;
import java.util.HashMap;
import java.util.Set;
import java.util.concurrent.Future;
import java.util.concurrent.Semaphore;
import com.sleepycat.je.*;
import com.sleepycat.persist.EntityStore; 
import com.sleepycat.persist.StoreConfig; 
import com.sleepycat.persist.EntityCursor;
import com.sleepycat.persist.PrimaryIndex;
import com.sleepycat.persist.SecondaryIndex;
import com.sleepycat.persist.model.Entity;
import com.sleepycat.persist.model.Persistent;
import com.sleepycat.persist.model.PrimaryKey;
import com.sleepycat.persist.model.SecondaryKey;
import com.sleepycat.persist.model.Relationship;
import java.util.HashSet;
import java.util.concurrent.TimeUnit;
import org.jpos.util.Log;
import org.jpos.util.Loggeable;
/**
 * BerkeleyDB Jave Edition based persistent space implementation
 *
 * @author Alejandro Revilla
 * @since 1.6.5
 */
@SuppressWarnings("unchecked")
public class JESpace<K,V> extends Log implements LocalSpace<K,V>, Loggeable, Runnable {
    Environment dbe = null;
    EntityStore store = null;
    PrimaryIndex<Long, Ref> pIndex = null;
    PrimaryIndex<Long,GCRef> gcpIndex = null;
    SecondaryIndex<String,Long, Ref> sIndex = null;
    SecondaryIndex<Long,Long,GCRef> gcsIndex = null;
    Semaphore gcSem = new Semaphore(1);
    LocalSpace<Object,SpaceListener> sl;
    private static final long NRD_RESOLUTION = 500L;
    public static final long GC_DELAY = 60*1000L;
    private Future gcTask;
    static final Map<String,Space> spaceRegistrar = 
        new HashMap<String,Space> ();
    public JESpace(String name, String path) throws SpaceError {
        super();
        try {
            EnvironmentConfig envConfig = new EnvironmentConfig();
            StoreConfig storeConfig = new StoreConfig();
            envConfig.setAllowCreate (true);
            envConfig.setTransactional(true);
            // envConfig.setTxnTimeout(5L, TimeUnit.MINUTES);
            envConfig.setLockTimeout(5, TimeUnit.SECONDS);
            storeConfig.setAllowCreate (true);
            storeConfig.setTransactional (true);
            File dir = new File(path);
            dir.mkdirs();
            dbe = new Environment (dir, envConfig);
            store = new EntityStore (dbe, name, storeConfig);
            pIndex = store.getPrimaryIndex (Long.class, Ref.class);
            gcpIndex = store.getPrimaryIndex (Long.class, GCRef.class);
            sIndex = store.getSecondaryIndex (pIndex, String.class, "key");
            gcsIndex = store.getSecondaryIndex (gcpIndex, Long.class, "expires");
            gcTask = SpaceFactory.getGCExecutor().scheduleAtFixedRate(this, GC_DELAY, GC_DELAY, TimeUnit.MILLISECONDS);
        } catch (Exception e) {
            throw new SpaceError (e);
        }
    }
    public void out (K key, V value) {
        out (key, value, 0L);
    }
    public void out (K key, V value, long timeout) {
        Transaction txn = null;
        try {
            txn = dbe.beginTransaction (null, null);
            Ref ref = new Ref(key.toString(), value, timeout);
            pIndex.put (ref);
            if (timeout > 0L)
                gcpIndex.putNoReturn (
                    new GCRef (ref.getId(), ref.getExpiration())
                );
            txn.commit();
            txn = null;
            synchronized (this) {
                notifyAll ();
            }
            if (sl != null)
                notifyListeners(key, value);
        } catch (Exception e) {
            throw new SpaceError (e);
        } finally {
            if (txn != null)
                abort (txn);
        }
    }
    public void push (K key, V value, long timeout) {
        Transaction txn = null;
        try {
            txn = dbe.beginTransaction (null, null);
            Ref ref = new Ref(key.toString(), value, timeout);
            pIndex.put (ref);
            pIndex.delete (ref.getId());
            ref.reverseId();
            pIndex.put (ref);
            txn.commit();
            txn = null;
            synchronized (this) {
                notifyAll ();
            }
            if (sl != null)
                notifyListeners(key, value);
        } catch (Exception e) {
            throw new SpaceError (e);
        } finally {
            if (txn != null)
                abort (txn);
        }
    }
    public void push (K key, V value) {
        push (key, value, 0L);
    }
    @SuppressWarnings("unchecked")
    public V rdp (Object key) {
        try {
            return (V) getObject (key, false);
        } catch (DatabaseException e) {
            throw new SpaceError (e);
        }
    }
    @SuppressWarnings("unchecked")
    public synchronized V in (Object key) {
        Object obj;
        while ((obj = inp (key)) == null) {
            try {
                this.wait ();
            } catch (InterruptedException ignored) { }
        }
        return (V) obj;
    }
    @SuppressWarnings("unchecked")
    public synchronized V in (Object key, long timeout) {
        Object obj;
        long now = System.currentTimeMillis();
        long end = now + timeout;
        while ((obj = inp (key)) == null &&
                (now = System.currentTimeMillis()) < end)
        {
            try {
                this.wait (end - now);
            } catch (InterruptedException ignored) { }
        }
        return (V) obj;
    }
    @SuppressWarnings("unchecked")
    public synchronized V rd  (Object key) {
        Object obj;
        while ((obj = rdp (key)) == null) {
            try {
                this.wait ();
            } catch (InterruptedException ignored) { }
        }
        return (V) obj;
    }
    @SuppressWarnings("unchecked")
    public synchronized V rd  (Object key, long timeout) {
        Object obj;
        long now = System.currentTimeMillis();
        long end = now + timeout;
        while ((obj = rdp (key)) == null &&
                (now = System.currentTimeMillis()) < end)
        {
            try {
                this.wait (end - now);
            } catch (InterruptedException ignored) { }
        }
        return (V) obj;
    }
    public synchronized void nrd  (Object key) {
        while (rdp (key) != null) {
            try {
                this.wait (NRD_RESOLUTION);
            } catch (InterruptedException ignored) { }
        }
    }
    public synchronized V nrd  (Object key, long timeout) {
        Object obj;
        long now = System.currentTimeMillis();
        long end = now + timeout;
        while ((obj = rdp (key)) != null &&
                (now = System.currentTimeMillis()) < end)
        {
            try {
                this.wait (Math.min(NRD_RESOLUTION, end - now));
            } catch (InterruptedException ignored) { }
        }
        return (V) obj;
    }
    @SuppressWarnings("unchecked")
    public V inp (Object key) {
        try {
            return (V) getObject (key, true);
        } catch (DatabaseException e) {
            throw new SpaceError (e);
        }
    }
    public boolean existAny (Object[] keys) {
        for (Object key : keys) {
            if (rdp(key) != null) {
                return true;
            }
        }
        return false;
    }
    public boolean existAny (Object[] keys, long timeout) {
        long now = System.currentTimeMillis();
        long end = now + timeout;
        while ((now = System.currentTimeMillis()) < end) {
            if (existAny (keys))
                return true;
            synchronized (this) {
                try {
                    wait (end - now);
                } catch (InterruptedException ignored) { }
            }
        }
        return false;
    }
    public synchronized void put (K key, V value, long timeout) {
        while (inp (key) != null)
            ;
        out (key, value, timeout);
    }
    public synchronized void put (K key, V value) {
        while (inp (key) != null)
            ;
        out (key, value);
    }
    public void gc () throws DatabaseException {
        Transaction txn = null;
        EntityCursor<GCRef> cursor = null;
        try {
            if (!gcSem.tryAcquire())
                return;
            txn = dbe.beginTransaction (null, null);
            cursor = gcsIndex.entities (
                txn, 0L, true, System.currentTimeMillis(), false, null
            );
            for (GCRef gcRef: cursor) {
                pIndex.delete (gcRef.getId());
                cursor.delete ();
            }
            cursor.close();
            cursor = null;
            txn.commit();
            txn = null;
            if (sl != null) {
                synchronized (this) {
                    if (sl != null && sl.getKeySet().isEmpty())
                        sl = null;
                }
            }
        } finally {
            if (cursor != null)
                cursor.close();
            if (txn != null)
                abort (txn);
            gcSem.release();
        }
    }
    public void run() {
        try {
            gc();
        } catch (DatabaseException e) {
            warn(e);
        }
    }
    public void close () throws DatabaseException {
        gcSem.acquireUninterruptibly();
        gcTask.cancel(false);
        while (!gcTask.isDone()) {
            try {
                Thread.sleep(500L);
            } catch (InterruptedException ignored) { }
        }
        store.close ();
        dbe.close();
    }
    public synchronized static JESpace getSpace (String name, String path)
    {
        JESpace sp = (JESpace) spaceRegistrar.get (name);
        if (sp == null) {


Answer:             sp = new JESpace(name, path);