/*
 * CacheObjetos.java
 *
 * Created on 13 de Novembro de 2001, 11:03
 */

package sac.persistencia;

import java.util.Map;
import java.util.HashMap;
import java.lang.ref.SoftReference;
import java.lang.ref.ReferenceQueue;
import java.util.Properties;
import java.io.IOException;
import java.util.Iterator;


/**
 * Classe que implementa um cache de objetos.
 * Note que as classes que utilizarem o <code>CacheObjetos</code> devem
 * controlar com cuidado a invalidação de objetos, para mantê-los sincronizados
 * com o armazenamento permanente (tipicamente após um update). Para tal deve-se
 * usar o método {@link #invalidarObjeto(OID) invalidarObjeto} ou o
 * {@link #limparCache() limparCache}. Este último método invalida todos os
 * objetos no cache.
 * O funcionamento correto do cache também depende da implementação correta dos
 * métodos <code>hashCode()</code> e <code>equals(Object)</code> de
 * {@link sac.persistencia.OID OID}.
 * Esta classe é um <em>singleton</em>.
 * A política de cache é bastante simples, <em>não</em> é garantido um
 * comportamento LRU (embora os objetos utilizados recentemente tenham menor
 * chance de serem descartados que os mais antigos).
 *
 * O cache usa um arquivo de configuração chamado "<code>cache.properties</code>", que deve
 * estar no <code>CLASSPATH</code>. Caso o arquivo não exista, serão assumidos
 * valores <em>default</em>.
 *
 * Esta classe de cache é dividida em 2 níveis, um chamado "cache primário"  e
 * outro chamado "cache secundário". O cache terá (desde que tenha sido inserido
 * durante a vida do cache mais do que o tamanho do cache primário de objetos)
 * sempre entre (tamanho do cache primário) e (tamanho do cache primário +
 * tamanho do cache secundário) de objetos armazenados, dependendo da quantidade
 * de memória disponível para a JVM.
 *
 * O arquivo <code>cache.properties</code> possui as seguintes propriedades:
 * <br><br>
 * <code>tam_cache_prim</code><br>
 * o tamanho do cache primário, em número de objetos (default 250)
 * <br><br>
 * <code>tam_cache_sec</code><br>
 * o tamanho do cache secundário, em número de objetos (default 500)
 * <br><br>
 * <code>ocup_max_prim</code><br>
 * ocupação máxima do cache primário, antes de ser compactado (default 0.75).
 * Esse valor é um <code>float</code> maior que 0.0 e menor que 1.0.
 * <br><br>
 * <code>ocup_max_sec</code><br>
 * ocupação máxima do cache secundário, antes de ser compactado (default 0.75).
 * Esse valor é um <code>float</code> maior que 0.0 e menor que 1.0. <br><br>
 * Exemplo de arquivo cache.properties:
 * <code>
 * tam_cache_prim=250
 * tam_cache_sec=500
 * ocup_max_prim=0.75
 * ocup_max_sec=0.75
 * </code>
 * @author  srmq
 */
public class CacheObjetos {

    private static CacheObjetos instancia = null;
    private static final String CONFIG_FILE = "/cache.properties";
    private static final String TAMANHO_PRIMARIO = "tam_cache_prim";
    private static final String TAMANHO_SECUNDARIO = "tam_cache_sec";
    private static final String OCUPACAO_PRIMARIO = "ocup_max_prim";
    private static final String OCUPACAO_SECUNDARIO   = "ocup_max_sec";
    private static final String TAM_PADRAO_PRIM = "250";
    private static final String TAM_PADRAO_SEC = "500";
    private static final String OCUP_PADRAO_PRIM = "0.75";
    private static final String OCUP_PADRAO_SEC = "0.75";

    private Map cachePrimario;
    private Map cacheSecundario;

    private int tamanhoCachePrimario;
    private int maxOcupacaoCachePrimario;

    private int tamanhoCacheSecundario;
    private int maxOcupacaoCacheSecundario;

    private ReferenceQueue refQueue;


    /** Método do Singleton para retornar a instância única desta classe.
     * @return A instância única de <CODE>CacheObjetos</CODE>
     */
    public static synchronized CacheObjetos getInstancia() {
        if (instancia == null) {
            instancia = new CacheObjetos();
        }
        return instancia;
    }

    private class ElementoFraco extends SoftReference {
        Object chave;	        /* Chave, tem que ser guardado aqui,
                                 * pois o GC destruirá o do objeto interno */

        private ElementoFraco(Object chave, Object valor) {
            super(valor);
            this.chave = chave;
        }

        private ElementoFraco(Object chave, Object valor, ReferenceQueue q) {
            super(valor, q);
            this.chave = chave;
        }

        public boolean equals(Object o) { /* sempre tentando usar o == */
            boolean resultado = false;    /* primeiro, por razões de   */
            if (this == o) {              /* eficiência                */
                resultado = true;
            } else if(o instanceof ElementoFraco) {
                Object x = this.get();
                Object u = ((ElementoFraco)o).get();
                if (x != null && u != null) { /* se pelo menos um dos dois   */
                    if (x == u) {             /* for nulo, ou nao sao iguais */
                        resultado = true;     /* ou e' impossivel determinar */
                    } else {                  /* assumindo-se que nao sao    */
                        resultado = x.equals(u);
                    }
                }
            }
            return resultado;
        }

        public int hashCode() {
            return chave.hashCode();
        }

    } // fim da classe ElementoFraco


    /** Adiciona um objeto no cache. Caso o objeto já exista ele é
     * substituído.
     * @param chave OID do objeto a ser inserido
     * @param objeto Objeto a ser inserido no cache.
     */
    public synchronized void inserirObjeto(OID chave, Object objeto) {
        if (this.cachePrimario.size() >= this.tamanhoCachePrimario) {
            compactarCachePrimario();
        }
        this.cachePrimario.put(chave, objeto);
        this.cacheSecundario.remove(chave);
    }

    /** Retorna o objeto com a chave especificada que está no cache.
     * Caso o objeto não esteja presente, é retornado <CODE>null</CODE>.
     * @param chave Chave do objeto desejado.
     * @return O objeto com a chave especificada, se estiver no cache.
     * <CODE>null</CODE>, caso contrário.
     */
    public synchronized Object pegarObjeto(OID chave) {
	Object objetoRetornado = this.cachePrimario.get(chave);
        if (objetoRetornado == null) {
            this.removerElementosDescartados();
            ElementoFraco elem =
                (ElementoFraco) this.cacheSecundario.remove(chave);
            if (elem != null) {
                Object elemProcurado = elem.get();
                if (elemProcurado != null) {
                    this.inserirObjeto(chave, elemProcurado);
                    objetoRetornado = elemProcurado;
                }
            }
        }
        return objetoRetornado;
    }

    /** Invalida (remove) o objeto especificado no cache
     * @param chave Chave (OID) do objeto a ser invalidado
     */
    public synchronized void invalidarObjeto(OID chave) {
        Object objetoInvalidado = this.cachePrimario.remove(chave);
        if (objetoInvalidado == null) {
            this.cacheSecundario.remove(chave);
        }
    }


    /** Remove todos os elementos do cache.
     */
    public synchronized void limparCache() {
        this.cachePrimario.clear();
        this.cacheSecundario.clear();
    }

    // A partir daqui só há métodos privados

    /** Creates new CacheObjetos */
    private CacheObjetos() {
        init();
    }

    private void init() {
        Properties props = new Properties();
        try {
            props.load(this.getClass().getResourceAsStream(CONFIG_FILE));
        } catch (IOException ex) {
            System.err.println("ATENÇÃO: Arquivo de configuração " +
                "\"" + CONFIG_FILE + "\" da classe " +
                this.getClass().getName() + "não foi encontrado." +
                " Usando configuração padrão.");
        }

        // definindo tamanhos e criando Maps
        {
            String tamanhoPrimario = props.getProperty(TAMANHO_PRIMARIO,
                                                       TAM_PADRAO_PRIM);
            this.tamanhoCachePrimario = Integer.parseInt(tamanhoPrimario);
            this.cachePrimario = new HashMap(tamanhoCachePrimario);

            String tamanhoSecundario = props.getProperty(TAMANHO_SECUNDARIO,
                                                         TAM_PADRAO_SEC);
            this.tamanhoCacheSecundario = Integer.parseInt(tamanhoSecundario);
            this.cacheSecundario = new HashMap(tamanhoCacheSecundario);
        }

        // definindo ocupacoes maximas
        {
            String ocupacaoPrimario = props.getProperty(OCUPACAO_PRIMARIO,
                                                        OCUP_PADRAO_PRIM);
            this.maxOcupacaoCachePrimario =
                (int) (Float.parseFloat(ocupacaoPrimario)
                       * this.tamanhoCachePrimario);

            String ocupacaoSecundario = props.getProperty(OCUPACAO_SECUNDARIO,
                                                          OCUP_PADRAO_SEC);
            this.maxOcupacaoCacheSecundario =
                (int) (Float.parseFloat(ocupacaoSecundario)
                       * this.tamanhoCacheSecundario);

        }

        this.refQueue = new ReferenceQueue();
    }

    private void removerElementosDescartados() {
        ElementoFraco ef;
        while ((ef = (ElementoFraco)this.refQueue.poll()) != null) {
            this.cacheSecundario.remove(ef.chave);
        }
    }

    private void compactarCachePrimario() {
        int tam = this.cachePrimario.size();
        for (Iterator i = this.cachePrimario.entrySet().iterator();
                tam >= this.maxOcupacaoCachePrimario; tam--) {
            Map.Entry mapEntry = (Map.Entry) i.next();
            OID chaveRemovida = (OID) mapEntry.getKey();
            Object objetoRemovido = mapEntry.getValue();
            i.remove();
            inserirCacheSecundario(chaveRemovida, objetoRemovido);
        }

    }

    private void inserirCacheSecundario(OID chave, Object valor) {
        this.removerElementosDescartados();
        if (this.cacheSecundario.size() >= this.tamanhoCacheSecundario) {
            compactarCacheSecundario();
        }
        ElementoFraco ef = new ElementoFraco(chave, valor);
        this.cacheSecundario.put(chave, ef);
    }

    private void compactarCacheSecundario() {
        int tam = this.cacheSecundario.size();
        for (Iterator i = this.cacheSecundario.keySet().iterator();
            tam >= this.maxOcupacaoCacheSecundario; tam--) {

            i.next();
            i.remove();
        }
    }

}
