/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2006, Red Hat Middleware LLC, and individual contributors
 * as indicated by the @author tags. See the copyright.txt file 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.system.pm;

import java.beans.PropertyEditor;
import java.beans.PropertyEditorManager;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamClass;
import java.io.OutputStream;
import java.io.Serializable;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import javax.management.Attribute;
import javax.management.AttributeList;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.jboss.logging.Logger;
import org.jboss.mx.persistence.AttributePersistenceManager;
import org.jboss.system.server.ServerConfigLocator;
import org.jboss.util.Classes;
import org.jboss.util.file.Files;
import org.w3c.dom.Comment;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;

/**
 * XMLAttributePersistenceManager
 * 
 * @author <a href="mailto:dimitris@jboss.org">Dimitris Andreadis</a>
 * @version $Revision: 57205 $
 */
public class XMLAttributePersistenceManager
   implements AttributePersistenceManager
{
   // Constants -----------------------------------------------------
   
   /** The XML configuration element */
   public static final String DATA_DIR_ELEMENT        = "data-directory";
   
   /** Default base directory if one not specified */
   public static final String DEFAULT_BASE_DIR        = "data/xmbean-attrs";
   
   /** The XML attribute-list elements and attributes */
   public static final String AL_ROOT_ELEMENT         = "attribute-list";
   public static final String AL_ID_ATTRIBUTE         = "id";
   public static final String AL_DATE_ATTRIBUTE       = "date";
   public static final String AL_ATTRIBUTE_ELEMENT    = "attribute";
   public static final String AL_NAME_ATTRIBUTE       = "name";
   public static final String AL_TYPE_ATTRIBUTE       = "type";
   public static final String AL_NULL_ATTRIBUTE       = "null";
   public static final String AL_SERIALIZED_ATTRIBUTE = "serialized";
   public static final String AL_TRUE_VALUE           = "true";
   public static final String AL_FALSE_VALUE          = "false";
    
   // Private Data --------------------------------------------------
   
   /** Logger object */
   private static final Logger log = Logger.getLogger(XMLAttributePersistenceManager.class);
   
   /** used for formating timestamps (date attribute) */
   private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss");
   
   /** for byte-to-hex conversions */
   private static final char[] hexDigits = new char[]
      { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };

   /** directory as used internally */
   private File dataDir;
   
   /** enable status */
   private boolean state;
   
   /** id to filename cache */
   private Map idMap;
   
   // Constructors -------------------------------------------------
    
   /**
    * Constructs a <tt>FileAttributePersistenceManager</tt>.
   **/
   public XMLAttributePersistenceManager()
   {
      log.debug("Constructed");
   }
   
   // AttributePersistenceManager Lifecycle -------------------------
   
   /**
    * Initializes the AttributePersistenceManager using
    * the supplied configuration element CONFIG_ELEMENT
    * whose content will be probably different for each
    * particular implementation.
    * 
    * The version string is a tag that must be used by the
    * AttributePersistenceManager implementation to make
    * sure that data saved/loaded under different version
    * tags are partitioned.
    * 
    * Once created, the configuration of the implementation
    * object cannot change.
    * 
    * Calling any other method before create() is executed
    * will result in a IllegalStateException
    * 
    * Finally, the implementation should be prepared to
    * receive multiple concurrent calls. 
    * 
    * @param  version   a tag to identify the version
    * @param  config    XML Element to load arbitrary config
    * @throws Exception	when any error occurs during create
    */
   public void create(String version, Element config)
      throws Exception
   {
      // ignore if already active
      if (getState()) {
         return;
      }
      
      // Decide on the base data directory to use
      String baseDir = null;
      
      if (config == null) {
         baseDir = DEFAULT_BASE_DIR;
      }
      else {
         if (!config.getTagName().equals(DATA_DIR_ELEMENT)) {
            throw new Exception("expected '" + DATA_DIR_ELEMENT +
                                "' XML configuration element, got '" +
                                config.getTagName() + "'");
         }
         else {
            baseDir = getElementContent(config);
         }
      }

      // Initialize the data dir
      this.dataDir = initDataDir(baseDir, version);
      
      log.debug("Using data directory: " + this.dataDir.getCanonicalPath());
      
      // initialize id cache
      this.idMap = Collections.synchronizedMap(new HashMap());

      // mark active status
      setState(true);
   }

   /**
    * Returns true if the AttributePersistenceManager
    * is "in-service" state, i.e. after create() and
    * before destroy() has been called, false otherwise.
    * 
    * @return true if in operational state
    */
   public boolean getState()
   {
      return this.state;
   }
   
   /**
    * Releases resources and destroys the AttributePersistenceManager.
    * The object is unusable after destroy() has been called.
    * 
    * Any call to any method will result to an
    * IllegalStateException.
    *
    */
   public void destroy()
   {
      setState(false);

      // instance can't be use anymore
      this.dataDir = null;
      this.idMap = null;
   }
   
   // AttributePersistenceManager Persistence -----------------------

   /**
    * Uses the specified id to retrieve a previously persisted
    * AttributeList. If no data can be found under the specified
    * id, a null will be returned.
    * 
    * @param id			the key for retrieving the data
    * @return			the data, or null
    * @throws Exception when an error occurs
    */
   public void store(String id, AttributeList attrs)
      throws Exception
   {
      log.debug("store(" + id + ") attrs=" + attrs);

      // make sure we are active
      checkActiveState();
      
      // map to filename - keep the original for storing
      String origId = id;
      id = mapId(id);
      
      if (attrs == null)
         throw new Exception("store() called with null AttributeList");
      
      // will throw an exception if file not r/w or it is a directory
      File file = checkFileForWrite(id);
      
      // build the XML in memory using DOM
      DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
      Document doc = builder.newDocument();

      // Add a comment
      Comment comment = doc.createComment(" automatically produced by XMLAttributePersistenceManager ");
      doc.appendChild(comment);
      
      // Insert root element
      Element root = doc.createElement(AL_ROOT_ELEMENT);
      root.setAttribute(AL_ID_ATTRIBUTE, origId);
      root.setAttribute(AL_DATE_ATTRIBUTE, dateFormat.format(new Date()));
      doc.appendChild(root);
      
      // iterate over the AttributeList
      for (int i = 0; i < attrs.size(); i++) {
         
         Attribute attr = (Attribute)attrs.get(i);
       
         String name  = attr.getName();
         Object value = attr.getValue();
         
         // create the Element and decide how to fill it in
         Element element = doc.createElement(AL_ATTRIBUTE_ELEMENT);
         element.setAttribute(AL_NAME_ATTRIBUTE, name);
         
         if (value == null) {
            // (a) null value - mark it as null
            element.setAttribute(AL_NULL_ATTRIBUTE, AL_TRUE_VALUE);
            
            // append the attribute to the attribute-list
            root.appendChild(element);
         }
         else if (value instanceof org.w3c.dom.Element) {
            // (b) XML Element - mark the type and append a copy of it
            element.setAttribute(AL_TYPE_ATTRIBUTE, "org.w3c.dom.Element");
            
            Node copy = doc.importNode((org.w3c.dom.Element)value, true);
            element.appendChild(copy);
            
            // append the attribute to the attribute-list
            root.appendChild(element);
         }
         else {
            Class clazz = value.getClass();
            String type = clazz.getName();
	        PropertyEditor peditor = PropertyEditorManager.findEditor(clazz);

	        if (peditor != null) {
		       // (c) use a PropertyEditor - mark the type and append the value as string  	           
	           peditor.setValue(value);
	           
	           element.setAttribute(AL_TYPE_ATTRIBUTE, type);
	           element.appendChild(doc.createTextNode(peditor.getAsText()));
	           
	           // append the attribute to the attribute-list
	           root.appendChild(element);
	        }
	        else if (value instanceof Serializable) {
	           // (d) serialize the object - mark type and serialized attribute
	           //                          - encode the value as stringfied sequence of hex
               String encoded = encodeAsHexString((Serializable)value);
               
               if (encoded != null) {
                  element.setAttribute(AL_TYPE_ATTRIBUTE, type);
                  element.setAttribute(AL_SERIALIZED_ATTRIBUTE, AL_TRUE_VALUE);
                  element.appendChild(doc.createTextNode(encoded));

                  // append the attribute to the attribute-list
                  root.appendChild(element);
               }
               else {
                  // could not serialize the object - write and log a warning
                  root.appendChild(doc.createComment(
                        " WARN <attribute name=\"" + name + "\" type=\"" + type + 
                        "\"/> could not be serialized "));
                  
                  log.warn("Could not serialize attribute '" + name + 
                           "' of type '" + type + "' and value: " + value);
               }
	        }
	        else {
	           // (e) could not find a way to persist - record and log a warning
	           root.appendChild(doc.createComment(
	                 " WARN <attribute name=\"" + name + "\" type=\"" + type + 
                     "\"/> could not be persisted "));
	           
	           log.warn("Could not find a way to persist attribute '" + name +
	                    "' of type '" + type + "' and value: " + value);
	        }
         }
      }
      
      // DOM document ready - save it
      try {
         outputXmlFile(doc, file);
      }
      catch (Exception e) {
         log.warn("Cannot persist AttributeList to: \"" + id + "\"", e);
         throw e;
      }
   }
   
   /**
    * Persists an AttributeList (name/value pair list),
    * under a specified id. The id can be used to retrieve the
    * AttributeList later on. The actual mechanism will differ
    * among implementations.
    * 
    * @param  id 		the key for retrieving the data later on, not null
    * @param  attrs 	the data to be persisted, not null
    * @throws Exception	when data cannot be persisted
    */
   public AttributeList load(String id)
      throws Exception
   {
      log.debug("load(" + id + ")");

      // make sure we are active
      checkActiveState();
      
      // map to filename
      id = mapId(id);
      
      if (!getState())
         return null;
      
      // Real stuff starts here
      AttributeList attrs = null;
      
      // returns null to indicate file does not exist
      File file = checkFileForRead(id);
      
      if (file != null) {
	     // parse the saved XML doc
	     Document doc = parseXmlFile(file);
	     
	     // top level - look for AL_ROOT_ELEMENT
	     NodeList docList = doc.getChildNodes();
	     Element root = null;
	     
	     for (int i = 0; i < docList.getLength(); i++) {
	        Node node = docList.item(i);
	        
	        if (node.getNodeType() == Node.ELEMENT_NODE && 
	            node.getNodeName().equals(AL_ROOT_ELEMENT)) {
	           
	           root = (Element)node;
	           break; // found
	        }
	     }
	     
	     // root element must be there
	     if (root == null) {
	        throw new Exception("Expected XML element: " + AL_ROOT_ELEMENT);
	     }
	     else {
	        // proceed iterating over AL_ATTRIBUTE_ELEMENT elements
	        // and fill the AttributeList
	        attrs = new AttributeList();
	        
	        NodeList rootList = root.getChildNodes();
	        
	        for (int i = 0; i < rootList.getLength(); i++) {
	           Node node = rootList.item(i);
	           
	           // only interested in ELEMENT nodes
	           if (node.getNodeType() == Node.ELEMENT_NODE &&
	               node.getNodeName().equals(AL_ATTRIBUTE_ELEMENT)) {
	              
	              Element element = (Element)node;
	              
	              // name attribute must always be there
	              String name = element.getAttribute(AL_NAME_ATTRIBUTE);
	              
	              if (!(name.length() > 0)) {
	                 throw new Exception("Attribute '" + AL_NAME_ATTRIBUTE +
	                                     "' must be specified for element '" + AL_ATTRIBUTE_ELEMENT + "'");
	              }
	              
	              // Process the attribute depending on how the attributes are set
	              
	              if (element.getAttribute(AL_NULL_ATTRIBUTE).toLowerCase().equals(AL_TRUE_VALUE)) {
                     
	                 //	(a) null value - just add it to the AttributeList
	                 attrs.add(new Attribute(name, null));
	              }
	              else if (element.getAttribute(AL_SERIALIZED_ATTRIBUTE).toLowerCase().equals(AL_TRUE_VALUE)) {
	                 
	                 // (b) serialized value - decode the HexString
	                 String hexStr = getElementContent(element);
	                 Serializable obj = decodeFromHexString(hexStr);
	                 
	                 if (obj == null) {
	                    throw new Exception("Failed to deserialize attribute '" + name + "'");
	                 }
	                 else {
	                    attrs.add(new Attribute(name, obj));
	                 }
	              }
	              else {
	                 String type = element.getAttribute(AL_TYPE_ATTRIBUTE);
	                 
	                 // type must be specified
	                 if (!(type.length() > 0)) {
	                    throw new Exception("Attribute '" + AL_TYPE_ATTRIBUTE +
	                                        "' must be specified for name='" + name + "'");
	                 }
	                 
	                 if (type.equals("org.w3c.dom.Element")) {

	                    // (c) org.w3c.dom.Element - deep copy first Element child node found
	                    
	                     NodeList nlist = element.getChildNodes();
	                     Element el = null;
	                     
	                     for (int j = 0; j < nlist.getLength(); j++) {
	                        
	                        Node n = nlist.item(j);
	                        if (n.getNodeType() == Node.ELEMENT_NODE)
	                        {
	                           el = (Element)n;
	                           break;
	                        }
	                     }
	                     
	                     if (el != null) {
	                        attrs.add(new Attribute(name, el.cloneNode(true)));
	                     }
	                     else {
	                        attrs.add(new Attribute(name, null));
	                     }
	                 }
	                 else {
	                    // Get the classloader for loading attribute classes.
	                    ClassLoader cl = Thread.currentThread().getContextClassLoader();
	                    Class clazz = null;
	                    
	                     try {
	                        clazz = cl.loadClass(type);
	                     }
	                     catch (ClassNotFoundException e) {
	                        throw new Exception("Class not found for attribute '" + name +
	                                            "' of type '" + type + "'");
	                     }

	                     PropertyEditor peditor = PropertyEditorManager.findEditor(clazz);

	         	         if (peditor != null) {
	         		        
	         	            // (d) use a PropertyEditor - extract the value
	         	            
	         	            String value = getElementContent(element);
	         	            peditor.setAsText(value);
	         	            
	         	            attrs.add(new Attribute(name, peditor.getValue()));
	         	         }
	         	         else {
	         	            throw new Exception("Cannot find a way to load attribute '" + name +
	         	                                "' of type '" + type + "'");
	         	         }
	                 }
	              }
	           }
	        } // end for()
	     }
      }
      log.debug("load() returns with: " + attrs);
      
      // will be null if a persistent file was not found
      return attrs;
   }

   // Administrative Functions --------------------------------------
   
   /**
    * Checks if a persistened AttributeList for this particular
    * id exists
    * 
    * @param  id		the key of the image
    * @return true 		if an image exists; false otherwise
    * @throws Exception on any error
    */
   public boolean exists(String id)
      throws Exception
   {
      // make sure we are active
      checkActiveState();
      
      return (new File(this.dataDir, mapId(id))).isFile();
   }
   
   /**
    * Removes the persisted AttributeList, if exists 
    *
    * @param  id		the key of the image
    * @throws Exception	when any error occurs
    */
   public void remove(String id)
      throws Exception
   {
      // make sure we are active
      checkActiveState();
      
      (new File(this.dataDir, mapId(id))).delete();
   }
   
   /**
    * Removes all the persisted data stored under
    * the configured version tag.
    *  
    * @throws Exception when any error occurs
    */
   public void removeAll()
      throws Exception
   {
      // make sure we are active
      checkActiveState();
      
      String[] files = this.dataDir.list(new XMLFilter());
      
      if (files != null) {
         for (int i = 0; i < files.length; i++) {
            (new File(this.dataDir, files[i])).delete();
         }
      }
   }
   
   /**
    * Returns a String array with all the saved ids
    * under the configured version tag.
    * 
    * @return			array with all persisted ids
    * @throws Exception when any error occurs
    */
   public String[] listAll()
      throws Exception
   {
      // make sure we are active
      checkActiveState();
      
      String[] files = this.dataDir.list(new XMLFilter());
      String[] result = null;      
      
      if (files != null) {
         result = new String[files.length];
         
         for (int i = 0; i < files.length; i++) {
            result[i] = mapFile(files[i]);
         }
      }
      return result;
   }
   
   // Private -------------------------------------------------------
   
   /**
    * Private status setter
    * 
    * lifecycle is controlled by create() and destroy()
    */
   private void setState(boolean state)
   {
      this.state = state;
   }
   
   /**
    * Create/check/point to the data directory.
    * 
    * Process the base dir first and append the versionTag dir
    * if specified.
    * 
    * Only a base dir relative to ServerHomeDir is created.
    * External base dirs are only used (i.e. they must exist
    * and we must read/write permission)
    */
   private File initDataDir(String baseDir, String versionTag)
      throws Exception
   {
      File dir = null;
      
      // Process the base directory first
      
	  // baseDir must be valid URL pointing to r/w dir
	  try {
	     URL fileURL = new URL(baseDir);
	      
	     File file = new File(fileURL.getFile());
	      
	     if(file.isDirectory() && file.canRead() && file.canWrite()) {
	        dir = file;
	     }
	  }
	  catch(Exception e) {
		 // Otherwise, try to make it inside the jboss directory hierarchy
	     
	     File homeDir = ServerConfigLocator.locate().getServerHomeDir();
	
	     dir = new File(homeDir, baseDir);
	
	     dir.mkdirs();
	      
	     if (!dir.isDirectory())
	        throw new Exception("The base directory is not valid: "
	                            + dir.getCanonicalPath());
	  }
	  
	  // Now add the versionTag dir, if specified
	  if (versionTag != null && !versionTag.equals("")) {
	     dir = new File(dir, versionTag);
	     
	     dir.mkdirs();
	      
	     if (!dir.isDirectory())
	        throw new Exception("The data directory is not valid: "
	                            + dir.getCanonicalPath());	     
	  }
	  return dir;
   }
   
   /**
    * Serialize an object as a Hex string so it can be saved as text
    * 
    * The length of the encoded object is twice it's byte image
    * 
    * Returns null if serialization fails
    */
   private String encodeAsHexString(Serializable obj)
   {
      String retn = null;
      
      if (obj != null) {
         try {
            ByteArrayOutputStream baos = new ByteArrayOutputStream(1024);
            ObjectOutputStream oos = new ObjectOutputStream(baos);
	      
	        oos.writeObject(obj);
	        byte[] bytes = baos.toByteArray();
	        
            StringBuffer sbuf = new StringBuffer(1024);

            for (int i = 0; i < bytes.length; i++) {
                sbuf.append(hexDigits[ (bytes[i] >> 4) & 0xF ]); // high order digit
                sbuf.append(hexDigits[ (bytes[i]     ) & 0xF ]); // low order digit
            }

            retn = sbuf.toString();	        
	     }
	     catch (IOException e) {
	        // will return null
	     }
      }
      return retn;
   }

   /**
    * Deserialize an object from its hex encoded string representation
    * 
    * Returns null if deserialization fails
    */
   private Serializable decodeFromHexString(String hexStr)
   {
      // hexStr must not contain white space!
      int len = hexStr.length() / 2;
      byte[] bytes = new byte[len];
      
      for (int i = 0; i < len; i++) {
         
          char h1 = hexStr.charAt(i * 2);		// high order hex digit
          char h2 = hexStr.charAt(i * 2 + 1);   // low order hex digit
          
          // convert hex digits to integers
          int d1 = (h1 >= 'a') ? (10 + h1 - 'a')
                : ((h1 >= 'A') ? (10 + h1 - 'A') 
                                   :  (h1 - '0'));
          
          int d2 = (h2 >= 'a') ? (10 + h2 - 'a')
                : ((h2 >= 'A') ? (10 + h2 - 'A')
                                    : (h2 - '0'));
          
          bytes[i] = (byte)(d1 * 16 + d2); // 255 max
      }
      
      Serializable retn = null;
      try
      {
         // Use ObjectInputStreamExt that will utilise the
         // ThreadContextClassLoader to deserialize objects
         ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
         ObjectInputStream ois = new ObjectInputStreamExt(bais);           
         retn = (Serializable)ois.readObject();
         ois.close();
      }
      catch (ClassNotFoundException e)
      {
         log.warn("Cannot deserialize object", e);
      }      
      catch (IOException e)
      {
         log.warn("Cought IOException", e);
      }
      return retn;
   }
   
   /**
    * Just output a DOM Document to a file with
    * indentation true
    */
   private void outputXmlFile(Document doc, File file)
      throws Exception
   {
      // Prepare the DOM document for writing
      Source source = new DOMSource(doc);    
      
      // Use an OutputStream rather than a File
      OutputStream out = new FileOutputStream(file);
      
      // Prepare the output
      Result result = new StreamResult(out);
      
      // Write the DOM document to the file
      Transformer xformer = TransformerFactory.newInstance().newTransformer();
      
      // Enable indentation
      xformer.setOutputProperty(OutputKeys.INDENT, "yes");
      
      try
      {
         xformer.transform(source, result);
      }
      finally
      {
         // Cleanup
         out.close();
      }
   }

   /**
    * Parse an XML file into a DOM Document
    * with validation set to false
    */
   private Document parseXmlFile(File file)
      throws Exception
   {
	   // Create a builder factory
	   DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
	   
	   // do not request validation against DTD
	   // no DTD specified in the saved document
	   factory.setValidating(false);

      // The builder and document to return
      DocumentBuilder builder = factory.newDocumentBuilder();
      Document doc;
      
      // Use an InputStreamer rather than a File when parsing,
      // to avoid pathname encoding problems, like spaces in
      // the pathname encoded as %20
      InputStream in = new FileInputStream(file);
      try
      {
         // Parse the file
         doc = builder.parse(in);
      }
      finally
      {
         // close anyways
         in.close();
      }
      // Return the parsed document
      return doc;
   }
   
   /**
    * Makes sure we can write to this file
    */
   private File checkFileForWrite(String filename)
      throws Exception
   {
      File file = new File(this.dataDir, filename);
      
      if (file.isFile()) {
         if (file.canRead() && file.canWrite()) {
            return file;  // everything fine
         }
         else {
            throw new Exception("file '" + filename + "' is not r/w"); // oops
         }
      }
      else if (file.isDirectory()) {
         throw new Exception(filename + " is a directory!"); // oops
      }
      else {
         return file; // not found - ok
      }     
   }
   
   /**
    * Makes sure we can read from this file.
    * 
    * Returns null if not found
    */
   private File checkFileForRead(String filename)
      throws Exception
   {
      File file = new File(this.dataDir, filename);
      
      if (file.isFile()) {
         if (file.canRead() && file.canWrite()) {
            return file;  // everything fine
         }
         else {
            throw new Exception("file '" + filename + "' is not r/w"); // oops
         }
      }
      else if (file.isDirectory()) {
         throw new Exception(filename + " is a directory!"); // oops
      }
      else {
         return null; // not found
      }
   }
   
   /**
    * Returns the concatenated content of all
    * children TEXT nodes of this element
    */
   private String getElementContent(Element element)
   {
      NodeList nlist = element.getChildNodes();
      
      StringBuffer sbuf = new StringBuffer(1024);
   
      // concatenate Text nodes
      for (int i = 0; i < nlist.getLength(); i++) {
         Node node = nlist.item(i);
         
         if (node.getNodeType() == Node.TEXT_NODE) {
            sbuf.append(((Text)node).getData());
         }
      }
      return sbuf.toString();
   }
   
   /**
    * Make sure we are active before processing a call
    */
   private void checkActiveState()
   {
      if (!getState()) {
         throw new IllegalStateException("AttributePersistenceManager not active");
      }
   }
   
   /**
    * Simple filter for .xml files in a directory listing
    */
   private class XMLFilter
      implements FilenameFilter
   {
      public boolean accept(File dir, String name)
      {
         return name.endsWith(".xml");
      }
   }
   
   /**
    * Map the id to a filename and cache it
    * 
    * A null id results in an exception thrown
    */
   private String mapId(String id)
      throws Exception
   {
      if (id == null) {
         throw new Exception("called with null id");
      }
	  else {
	     // check the cache
	     String file = (String)this.idMap.get(id);
	     
	     // if not in cache encode, and cache it
	     if (file == null) {
	        file = Files.encodeFileName(id) + ".xml";
	        this.idMap.put(id, file);
	     }
	     return file;
      }
   }
   
   /**
    * Do the reverse mapping file to id
    */
   private String mapFile(String file)
   {
      if (file == null) {
         return null;
      }
      else {
         // remove .xml suffix
         file = file.substring(0, file.length() - 4);
         
         return Files.decodeFileName(file);
      }
   }
   
   /**
    * Default implementation ObjectInputStream uses the System
    * classloader to deserialize classes, by we need to use the
    * ThreadContextClassLoader, instead.
    */
   private class ObjectInputStreamExt extends ObjectInputStream
   {
      ObjectInputStreamExt(InputStream is) throws IOException
      {
         super(is);
      }

      protected Class resolveClass(ObjectStreamClass v) throws IOException, ClassNotFoundException
      {
         return Classes.loadClass(v.getName());
      }
   }   
}    
