/*
 * $Header: /home/cvspublic/jakarta-slide/src/stores/org/apache/slide/store/file/SimpleFileStore.java,v 1.5 2005/01/03 15:59:53 luetzkendorf Exp $
 * $Revision: 1.5 $
 * $Date: 2005/01/03 15:59:53 $
 *
 * ====================================================================
 *
 * Copyright 1999-2002 The Apache Software Foundation
 *
 * 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.apache.slide.store.file;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Date;
import java.util.Hashtable;

import org.apache.slide.common.ServiceAccessException;
import org.apache.slide.common.ServiceParameterErrorException;
import org.apache.slide.common.ServiceParameterMissingException;
import org.apache.slide.common.Uri;
import org.apache.slide.content.NodeRevisionContent;
import org.apache.slide.content.NodeRevisionDescriptor;
import org.apache.slide.content.NodeRevisionDescriptors;
import org.apache.slide.content.NodeRevisionNumber;
import org.apache.slide.content.RevisionAlreadyExistException;
import org.apache.slide.content.RevisionDescriptorNotFoundException;
import org.apache.slide.content.RevisionNotFoundException;
import org.apache.slide.structure.ObjectAlreadyExistsException;
import org.apache.slide.structure.ObjectNode;
import org.apache.slide.structure.ObjectNotFoundException;
import org.apache.slide.structure.SubjectNode;

/**
 * SimpleFileStore implements ContentStore, NodeStore, RevisionDescriptorStore,
 * and RevisionDescriptorsStore backed by a file system. Unlike the
 * org.apache.slide.store.txfile.* implementations, SimpleFileStore does not support
 * transactions, locking, security or versioning. But it will work by pointing it an
 * existing directory on your file system and does not need to keep track of any other
 * information other than what it can find in the file system.
 * <p>
 * See <a href="http://www.mail-archive.com/slide-user@jakarta.apache.org/msg07390.html">this
 * slide-user thread</a> for some history on this implementation.
 * <p>
 * SimpleFileStore needs to be deployed with implementations of LockStore and SecurityStore.
 * A sample Domain.xml entry looks like:
 * <p>
 * <pre>
 *  &lt;store name="simple">
 *      &lt;nodestore classname="org.apache.slide.store.file.SimpleFileStore">
 *          &lt;parameter name="rootpath">webapps/slidespace/files&lt;/parameter>
 *      &lt;/nodestore>
 *      &lt;contentstore>
 *        &lt;reference store="nodestore"/>
 *      &lt;/contentstore>
 *      &lt;revisiondescriptorsstore>
 *        &lt;reference store="nodestore"/>
 *      &lt;/revisiondescriptorsstore>
 *      &lt;revisiondescriptorstore>
 *        &lt;reference store="nodestore"/>
 *      &lt;/revisiondescriptorstore>
 *      &lt;securitystore classname="org.apache.slide.store.mem.TransientSecurityStore"/>
 *      &lt;lockstore classname="org.apache.slide.store.mem.TransientLockStore"/>
 *  &lt;/store>
 *  &lt;scope match="/simple" store="simple"/>
 * </pre>
 *
 * @author Alon Salant
 */
public class SimpleFileStore
  extends AbstractSimpleStore
{
  private static final String ROOT_PARAM = "rootpath";
  private static final String COLLECTION_TYPE = "<collection/>";

  private File root;

  public void setParameters(Hashtable parameters)
    throws ServiceParameterErrorException, ServiceParameterMissingException
  {
    log("setParameters(" + parameters + ")");

    if (parameters.get(ROOT_PARAM) == null) throw new ServiceParameterErrorException(this, ROOT_PARAM);
    File root = new File((String) parameters.get(ROOT_PARAM));
    if (!root.exists()) throw new ServiceParameterErrorException(this, ROOT_PARAM + ": " + root + " does not exist");
    this.root = root;
    log("Initialized with file root " + root);
  }

  public NodeRevisionContent retrieveRevisionContent(Uri uri, NodeRevisionDescriptor revisionDescriptor)
    throws ServiceAccessException, RevisionNotFoundException
  {
    log("retrieveRevisionContent(" + uri + ")");
    
    File file = getFile(uri);
    InputStream in = null;
    try
    {
      in = new FileInputStream(file);
      NodeRevisionContent nrc = new NodeRevisionContent();
      nrc.setContent(in);
      return nrc;
    }
    catch (FileNotFoundException e)
    {
      throw new RevisionNotFoundException(uri.toString(), revisionDescriptor.getRevisionNumber());
    }
  }

  public void createRevisionContent(Uri uri,
                                    NodeRevisionDescriptor revisionDescriptor,
                                    NodeRevisionContent revisionContent)
    throws ServiceAccessException, RevisionAlreadyExistException
  {
    log("createRevisionContent(" + uri + ")");
    File file = getFile(uri);
    if (file.exists()) throw new RevisionAlreadyExistException(uri.toString(), revisionDescriptor.getRevisionNumber());

    try
    {
      write(revisionContent.streamContent(), new FileOutputStream(file));
    }
    catch (IOException e)
    {
      throw new ServiceAccessException(this, e);
    }
  }

  public void storeRevisionContent(Uri uri,
                                   NodeRevisionDescriptor revisionDescriptor,
                                   NodeRevisionContent revisionContent)
    throws ServiceAccessException, RevisionNotFoundException
  {
    log("storeRevisionContent(" + uri + ")");
    File file = getFile(uri);
    if (!file.exists()) throw new RevisionNotFoundException(uri.toString(), revisionDescriptor.getRevisionNumber());

    if (file.isDirectory())
    {
      if (!file.delete()) throw new ServiceAccessException(this, "unable to remove directory " + file +
                                                                 " in order to create a file with the same name");
      revisionDescriptor.removeProperty(NodeRevisionDescriptor.RESOURCE_TYPE);
    }

    try
    {
      write(revisionContent.streamContent(), new FileOutputStream(file));
    }
    catch (IOException e)
    {
      throw new ServiceAccessException(this, e);
    }
  }

  public void storeObject(Uri uri, ObjectNode object)
    throws ServiceAccessException, ObjectNotFoundException
  {
    log("storeObject(" + uri + ")");
    File file = getFile(uri);
    if (!file.exists()) throw new ObjectNotFoundException(uri);
  }

  /**
   * Creates a new object as a directory. This will always create a directory, since there
   * appears to be no way to know the type of the resource being created.
   * <p>
   * storeRevisionContent() will be called after the object is created and will
   * remove the directory and create a file.
   */
  public void createObject(Uri uri, ObjectNode object)
    throws ServiceAccessException, ObjectAlreadyExistsException
  {
    log("createObject(" + uri + ")");
    File file = getFile(uri);
    if (file.exists()) throw new ObjectAlreadyExistsException(uri.toString());
    if (!file.mkdir()) throw new ServiceAccessException(this, "Unable to create " + uri.toString());
  }

  public void removeObject(Uri uri, ObjectNode object)
    throws ServiceAccessException, ObjectNotFoundException
  {
    log("removeObject(" + uri + ")");
    File file = getFile(uri);
    if (!file.exists()) throw new ObjectNotFoundException(uri);
    if (file.isDirectory() && !(file.listFiles().length == 0))
      throw new ServiceAccessException(this, uri.toString() + " is not empty");

    if (!file.delete()) throw new ServiceAccessException(this, "Unable to delete " + uri.toString());
  }

  /**
   * Return SubjectNode for normal use. Create SubjectNode with Uri.toString().
   * For folders add children as SubjectNodes created with file/folder name.
   */
  public ObjectNode retrieveObject(Uri uri)
    throws ServiceAccessException, ObjectNotFoundException
  {
    log("retrieveObject(" + uri + ")");
    File file = getFile(uri);
    if (!file.exists()) throw new ObjectNotFoundException(uri);

    SubjectNode subject = new SubjectNode(uri.toString());
    if (file.isFile()) return subject;

    File[] children = file.listFiles();
    for (int i = 0; i < children.length; i++)
    {
      subject.addChild(new SubjectNode(children[i].getName()));
    }
    return subject;
  }

  /**
   * Set resourceType to "<collection/>" for folders and set the contentLength for files.
   * Also set created and modified dates.
   */
  public NodeRevisionDescriptor retrieveRevisionDescriptor(Uri uri,
                                                           NodeRevisionNumber revisionNumber)
    throws ServiceAccessException, RevisionDescriptorNotFoundException
  {
    log("retrieveRevisionDescriptor(" + uri + ")");
    File file = getFile(uri);
    if (!file.exists()) throw new RevisionDescriptorNotFoundException(uri.toString());

    NodeRevisionDescriptor descriptor = new NodeRevisionDescriptor(new NodeRevisionNumber(1, 0),
                                                                   NodeRevisionDescriptors.MAIN_BRANCH);
    descriptor.setCreationDate(new Date(file.lastModified()));
    descriptor.setLastModified(descriptor.getCreationDateAsDate());
    descriptor.setModificationDate(descriptor.getCreationDateAsDate());
    if (file.isDirectory())
    {
      descriptor.setResourceType(COLLECTION_TYPE);
    }
    else
    {
      descriptor.setContentLength(file.length());
    }
    return descriptor;
  }

  public NodeRevisionDescriptors retrieveRevisionDescriptors(Uri uri)
	throws ServiceAccessException, RevisionDescriptorNotFoundException
  {
	File file = getFile(uri);
	if (!file.exists()) throw new RevisionDescriptorNotFoundException(uri.toString());
	return super.retrieveRevisionDescriptors(uri);
  }

  private void write(InputStream in, OutputStream out) throws IOException
  {
    byte buffer[] = new byte[2048];
    int len = buffer.length;

    try
    {
      while (true)
      {
        len = in.read(buffer);
        if (len == -1) break;
        out.write(buffer, 0, len);
      }
    }
    finally
    {
      try     { in.close(); }
      finally { out.close(); }
    }
  }

  private File getFile(Uri uri)
  {
    File file = new File(root, uri.getRelative());
    return file;
  }
}