/*
 * JBoss, Home of Professional Open Source
 * Copyright 2005, JBoss Inc., and individual contributors as indicated
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.jboss.cache.loader;

import org.jboss.cache.TreeCache;
import org.jboss.cache.Fqn;
import org.jboss.cache.Modification;
import org.jboss.logging.Logger;
import org.jboss.invocation.MarshalledValueInputStream;
import org.jboss.invocation.MarshalledValueOutputStream;

import java.util.*;
import java.io.*;

/**
 * Simple file-based CacheLoader implementation. Nodes are directories, attributes of a node is a file in the directory
 * @author Bela Ban
 * @version $Id: FileCacheLoader.java,v 1.11.2.5 2005/12/01 19:48:41 dimitris Exp $
 */
public class FileCacheLoader implements CacheLoader {
   File      root=null;
   TreeCache cache=null;
   Logger    log=Logger.getLogger(getClass());

   /** HashMap<Object,List<Modification>>. List of open transactions. Note that this is purely transient, as
    * we don't use a log, recovery is not available */
   HashMap   transactions=new HashMap();

   public static final String DATA="data.dat";
   public static final String DIR_SUFFIX="fdb";


   public FileCacheLoader() {

   }


   public void setConfig(Properties props) {
      String location=props != null? props.getProperty("location") : null;
      root=new File(location);
   }

   public void setCache(TreeCache c) {
      cache=c;
   }

   public void create() throws Exception {
      if(!root.exists())
         throw new FileNotFoundException(root.toString());
   }

   public void start() throws Exception {
   }

   public void stop() {
   }

   public void destroy() {
   }



   public Set getChildrenNames(Fqn fqn) throws Exception {
      File parent=getDirectory(fqn, false);
      if(parent == null) return null;
      File[] children=parent.listFiles();
      HashSet s=new HashSet();
      for(int i=0; i < children.length; i++) {
         File child=children[i];
         if(child.isDirectory() && child.getName().endsWith(DIR_SUFFIX)) {
            String child_name=child.getName();
            child_name=child_name.substring(0, child_name.lastIndexOf(DIR_SUFFIX)-1);
            s.add(child_name);
         }
      }
      return s.size() == 0? null : s;
   }

   public Object get(Fqn fqn, Object key) throws Exception {
      Map m=loadAttributes(fqn);
      if(m == null) return null;
      return m.get(key);
   }

   public Map get(Fqn fqn) throws Exception {
      Map m=loadAttributes(fqn);
      if(m == null || m.size() == 0) return null;
      return m;
   }

   public boolean exists(Fqn fqn) throws Exception {
      File f=getDirectory(fqn, false);
      return f != null;
   }

   public Object put(Fqn fqn, Object key, Object value) throws Exception {
      Object retval=null;
      Map m=loadAttributes(fqn);
      if(m == null) m=new HashMap();
      retval=m.put(key, value);
      storeAttributes(fqn, m);
      return retval;
   }

   public void put(Fqn fqn, Map attributes) throws Exception {
      put(fqn, attributes, false);
   }


   public void put(Fqn fqn, Map attributes, boolean erase) throws Exception {
      Map m=erase? new HashMap() : loadAttributes(fqn);
      if(m == null) m=new HashMap();
      if(attributes != null)
         m.putAll(attributes);
      storeAttributes(fqn, m);
   }

   private void put(Fqn fqn) throws Exception {
      getDirectory(fqn, true);
   }

   /**
    *
    * @param modifications List<Modification>
    * @throws Exception
    */
   public void put(List modifications) throws Exception {
      if(modifications == null) return;
      for(Iterator it=modifications.iterator(); it.hasNext();) {
         Modification m=(Modification)it.next();
         switch(m.getType()) {
            case Modification.PUT_DATA:
               put(m.getFqn(), m.getData());
               break;
            case Modification.PUT_DATA_ERASE:
               put(m.getFqn(), m.getData(), true);
               break;
            case Modification.PUT_KEY_VALUE:
               put(m.getFqn(), m.getKey(), m.getValue());
               break;
            case Modification.REMOVE_DATA:
               removeData(m.getFqn());
               break;
            case Modification.REMOVE_KEY_VALUE:
               remove(m.getFqn(), m.getKey());
               break;
            case Modification.REMOVE_NODE:
               remove(m.getFqn());
               break;
            default:
               log.error("modification type " + m.getType() + " not known");
               break;
         }
      }
   }

   public Object remove(Fqn fqn, Object key) throws Exception {
      Object retval=null;
      Map m=loadAttributes(fqn);
      if(m == null) return null;
      retval=m.remove(key);
      storeAttributes(fqn, m);
      return retval;
   }

   public void remove(Fqn fqn) throws Exception {
      File dir=getDirectory(fqn, false);
      if(dir != null) {
         boolean flag=removeDirectory(dir, true);
         if(flag == false)
            log.warn("failed removing " + fqn);
      }
   }

   public void removeData(Fqn fqn) throws Exception {
      File f=getDirectory(fqn, false);
      if(f != null) {
         File data=new File(f, DATA);
         if(data != null && data.exists()) {
            boolean flag=data.delete();
            if(!flag)
               log.warn("failed removing file " + data.getName());
         }
      }
   }

   public void prepare(Object tx, List modifications, boolean one_phase) throws Exception {
      if(one_phase)
         put(modifications);
      else
         transactions.put(tx, modifications);
   }

   public void commit(Object tx) throws Exception {
      List modifications=(List)transactions.get(tx);
      if(modifications == null)
         throw new Exception("transaction " + tx + " not found in transaction table");
      put(modifications);

      // Added by Tom Bakken
      // Without this line, we get a memory leak.
      transactions.remove(tx);
   }

   public void rollback(Object tx) {
      transactions.remove(tx);
   }

   /**
    * Loads the entire state from the filesystem and returns it as a byte buffer. The format of the byte buffer
    * must be a list of NodeData elements
    * @return
    * @throws Exception
    */
   public byte[] loadEntireState() throws Exception {
      ByteArrayOutputStream out_stream=new ByteArrayOutputStream(1024);
      ObjectOutputStream    out=new MarshalledValueOutputStream(out_stream);
      loadStateFromFilessystem(Fqn.fromString("/"), out);
      out.close();
      return out_stream.toByteArray();
   }




   /** Store the state given as a byte buffer to the filesystem. The byte buffer contains a list
    * of zero or more NodeData elements
    * @param state
    * @throws Exception
    */
   public void storeEntireState(byte[] state) throws Exception {
      ByteArrayInputStream in_stream=new ByteArrayInputStream(state);
      MarshalledValueInputStream in=new MarshalledValueInputStream(in_stream);
      NodeData nd;

      // remove entire existing state
      this.remove(Fqn.fromString("/"));

      // store new state
      try {
         while(true) {
            nd=(NodeData)in.readObject();
            if(nd.attrs != null)
               this.put(nd.fqn, nd.attrs, true); // creates a node with 0 or more attributes
            else
               this.put(nd.fqn);  // creates a node with null attributes
         }
      }
      catch(EOFException eof_ex) {
         ;
      }
   }

   /* ----------------------- Private methods ------------------------ */


   /**
    * Do a preorder traversal: visit the node first, then the node's children
    * @param fqn Start node
    * @param out
    * @throws Exception
    */
   void loadStateFromFilessystem(Fqn fqn, ObjectOutputStream out) throws Exception {
      Map       attrs;
      Set       children_names;
      String    child_name;
      Fqn       tmp_fqn;
      NodeData  nd;

      // first handle the current node
      attrs=get(fqn);
      if(attrs == null || attrs.size() == 0)
         nd=new NodeData(fqn);
      else
         nd=new NodeData(fqn, attrs);
      out.writeObject(nd);

      // then visit the children
      children_names=getChildrenNames(fqn);
      if(children_names == null)
         return;
      for(Iterator it=children_names.iterator(); it.hasNext();) {
         child_name=(String)it.next();
         tmp_fqn=new Fqn(fqn, child_name);
         loadStateFromFilessystem(tmp_fqn, out);
      }
   }


   File getDirectory(Fqn fqn, boolean create) {
      File f=new File(getFullPath(fqn));
      if(!f.exists()) {
         if(create)
            f.mkdirs();
         else
            return null;
      }
      return f;
   }


   /**
    * Recursively removes this and all subdirectories, plus all DATA files in them. To prevent damage, we only
    * remove files that are named DATA (data.dat) and directories which end in ".fdb". If there is a dir or file
    * that isn't named this way, the recursive removal will fail
    * @param fqn
    * @return True if directory was removed, false if not.
    */
   boolean removeDirectory(File dir, boolean include_start_dir) {
      boolean success=true;
      File[] subdirs=dir.listFiles();
      for(int i=0; i < subdirs.length; i++) {
         File file=subdirs[i];
         if(file.isFile() && file.getName().equals(DATA)) {
            if(file.delete() == false)
               success=false;
            continue;
         }
         if(file.isDirectory() && file.getName().endsWith(DIR_SUFFIX)) {
            if(removeDirectory(file, false) == false)
               success=false;
            if(file.delete() == false)
               success=false;
         }
      }

      if(include_start_dir) {
         if(dir.equals(root)) {
            ;
         }
         else {
            if(dir.delete() == false)
               success=false;
         }
      }

      return success;
   }

   String getFullPath(Fqn fqn) {
      StringBuffer sb=new StringBuffer(root.getAbsolutePath() + File.separator);
      for(int i=0; i < fqn.size(); i++) {
         Object tmp=fqn.get(i);
         String tmp_dir;
         if(tmp instanceof String)
            tmp_dir=(String)tmp;
         else
            tmp_dir=tmp.toString();
         sb.append(tmp_dir).append(".").append(DIR_SUFFIX).append(File.separator);
      }
      return sb.toString();
   }

   Map loadAttributes(Fqn fqn) throws Exception {
      File f=getDirectory(fqn, false);
      if(f == null) return null;
      File child=new File(f, DATA);
      // if(!child.exists()) return new HashMap();
      if(!child.exists()) return null;
      FileInputStream in=new FileInputStream(child);
      MarshalledValueInputStream input=new MarshalledValueInputStream(in);
      Map m=(Map)input.readObject();
      in.close();
      return m;
   }

   void storeAttributes(Fqn fqn, Map attrs) throws Exception {
      File f=getDirectory(fqn, true);
      File child=new File(f, DATA);
      if(!child.exists())
         child.createNewFile();
      FileOutputStream out=new FileOutputStream(child);
      ObjectOutputStream output=new ObjectOutputStream(out);
      output.writeObject(attrs);
      out.close();
   }
}
