/*
 * Copyright (c) 1998-2008 Caucho Technology -- all rights reserved
 *
 * This file is part of Resin(R) Open Source
 *
 * Each copy or derived work must preserve the copyright notice and this
 * notice unmodified.
 *
 * Resin Open Source is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * Resin Open Source 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, or any warranty
 * of NON-INFRINGEMENT.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Resin Open Source; if not, write to the
 *
 *   Free Software Foundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307  USA
 *
 * @author Scott Ferguson
 */

package com.caucho.server.deploy;

import com.caucho.config.types.FileSetType;
import com.caucho.loader.DynamicClassLoader;
import com.caucho.loader.Environment;
import com.caucho.util.L10N;
import com.caucho.util.Log;
import com.caucho.vfs.Depend;
import com.caucho.vfs.Path;
import com.caucho.vfs.ReadStream;
import com.caucho.vfs.Vfs;
import com.caucho.vfs.WriteStream;

import java.io.IOException;
import java.util.*;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

/**
 * A deployment entry that expands from an archive (Jar/Zip) file.
 */
abstract public class ExpandDeployController<I extends DeployInstance>
  extends DeployController<I> {
  private static final L10N L = new L10N(ExpandDeployController.class);
  private static final Logger log = Log.open(ExpandDeployController.class);

  private Object _archiveExpandLock = new Object();

  private Path _rootDirectory;
  private Path _archivePath;

  private FileSetType _expandCleanupFileSet;

  // classloader for the manifest entries
  private DynamicClassLoader _manifestLoader;
  private Manifest _manifest;

  protected ExpandDeployController(String id)
  {
    this(id, null, null);
  }

  protected ExpandDeployController(String id,
                                   ClassLoader loader,
                                   Path rootDirectory)
  {
    super(id, loader);

    if (rootDirectory == null)
      rootDirectory = Vfs.getPwd(getParentClassLoader());

    _rootDirectory = rootDirectory;
  }

  /**
   * Gets the root directory
   */
  public Path getRootDirectory()
  {
    return _rootDirectory;
  }

  /**
   * Sets the root directory
   */
  protected void setRootDirectory(Path rootDirectory)
  {
    _rootDirectory = rootDirectory;
  }

  /**
   * Gets the archive path.
   */
  public Path getArchivePath()
  {
    return _archivePath;
  }

  /**
   * Sets the archive path.
   */
  public void setArchivePath(Path path)
  {
    _archivePath = path;
  }

  /**
   * Returns the manifest.
   */
  public Manifest getManifest()
  {
    return _manifest;
  }

  /**
   * Returns the manifest as an attribute map
   */
  public Map<String,String> getManifestAttributes()
  {
    if (_manifest == null)
      return null;
    
    Map<String,String> map = new TreeMap<String,String>();

    Attributes attr = _manifest.getMainAttributes();

    if (attr != null) {
      for (Map.Entry entry : attr.entrySet()) {
	map.put(String.valueOf(entry.getKey()),
		String.valueOf(entry.getValue()));
      }
    }

    return map;
  }

  /**
   * Sets the manifest class loader.
   */
  public void setManifestClassLoader(DynamicClassLoader loader)
  {
    _manifestLoader = loader;
  }

  /**
   * Sets the archive auto-remove file set.
   */
  public void setExpandCleanupFileSet(FileSetType fileSet)
  {
    _expandCleanupFileSet = fileSet;
  }

  /**
   * Merges with the old controller.
   */
  @Override
  protected void mergeController(DeployController oldControllerV)
  {
    super.mergeController(oldControllerV);

    ExpandDeployController<I> oldController;
    oldController = (ExpandDeployController<I>) oldControllerV;

    if (oldController._expandCleanupFileSet != null)
      _expandCleanupFileSet = oldController._expandCleanupFileSet;
  }

  /**
   * Expand an archive file.  The _archiveExpandLock must be obtained
   * before the expansion.
   */
  protected void expandArchive()
    throws IOException
  {
    synchronized (_archiveExpandLock) {
      if (! expandArchiveImpl()) {
        try {
          Thread.sleep(2000);
        } catch (InterruptedException e) {
        }

        expandArchiveImpl();
      }

      Path path = getRootDirectory().lookup("META-INF/MANIFEST.MF");
      if (path.canRead()) {
        ReadStream is = path.openRead();
        try {
          _manifest = new Manifest(is);
        } catch (IOException e) {
          log.warning(L.l("Manifest file cannot be read for '{0}'.\n{1}",
                          getRootDirectory(), e));

          log.log(Level.FINE, e.toString(), e);
        } finally {
          is.close();
        }
      }
    }
  }

  /**
   * Adds any class path from the manifest.
   */
  protected void addManifestClassPath()
    throws IOException
  {
    DynamicClassLoader loader = Environment.getDynamicClassLoader();
    if (loader == null)
      return;

    Manifest manifest = getManifest();

    if (manifest == null)
      return;

    Attributes main = manifest.getMainAttributes();

    if (main == null)
      return;

    String classPath = main.getValue("Class-Path");

    Path pwd = null;

    if (getArchivePath() != null)
      pwd = getArchivePath().getParent();
    else
      pwd = getRootDirectory();

    if (classPath == null) {
    }
    else if (_manifestLoader != null)
      _manifestLoader.addManifestClassPath(classPath, pwd);
    else
      loader.addManifestClassPath(classPath, pwd);
  }

  /**
   * Expand an archive.  The _archiveExpandLock must be obtained before the
   * expansion.
   */
  private boolean expandArchiveImpl()
    throws IOException
  {
    Path archivePath = getArchivePath();

    if (archivePath == null)
      return true;

    if (! archivePath.canRead())
      return true;

    Path expandDir = getRootDirectory();
    Path parent = expandDir.getParent();

    try {
      parent.mkdirs();
    } catch (Throwable e) {
    }

    Path dependPath = expandDir.lookup("META-INF/resin-war.digest");
    Depend depend = null;

    // XXX: change to a hash
    if (dependPath.canRead()) {
      ReadStream is = null;
      try {
        is = dependPath.openRead();

        String line = is.readLine();

        long digest;

        if (line != null) {
          digest = Long.parseLong(line.trim());

          depend = new Depend(archivePath, digest);

          if (! depend.isModified())
            return true;
        }
      } catch (Throwable e) {
        log.log(Level.FINE, e.toString(), e);
      } finally {
        if (is != null)
          is.close();
      }
    }

    if (depend == null)
      depend = new Depend(archivePath);

    try {
      if (log.isLoggable(Level.INFO))
        getLog().info("expanding " + archivePath + " to " + expandDir);

      removeExpandDirectory(expandDir);

      expandDir.mkdirs();

      ReadStream rs = archivePath.openRead();
      ZipInputStream zis = new ZipInputStream(rs);

      try {
        ZipEntry entry = zis.getNextEntry();

        byte []buffer = new byte[1024];

        while (entry != null) {
          String name = entry.getName();
          Path path = expandDir.lookup(name);

          if (entry.isDirectory())
            path.mkdirs();
          else {
            long entryLength = entry.getSize();
            long length = entryLength;

            // XXX: avoids unexpected end of ZLIB input stream.
            // XXX: This should be a really temp. workaround.
            int bufferLen = buffer.length;

            // XXX: avoids unexpected end of ZLIB input stream.
            if (length < 0) {
              // bufferLen = 1;
              length = Long.MAX_VALUE / 2;
            }
            else if (length < bufferLen) {
              bufferLen = (int) length;
            }

            long lastModified = entry.getTime();
            path.getParent().mkdirs();

            WriteStream os = path.openWrite();
            int len = 0;
            try {
              if (bufferLen == 1) {
                for (int ch = zis.read(); ch != -1; ch = zis.read())
                  os.write(ch);
              }
              else {
                while ((len = zis.read(buffer, 0, bufferLen)) > 0) {
                  os.write(buffer, 0, len);

                  // XXX: avoids unexpected end of ZLIB input stream.
                  /*
                  if (len < bufferLen) {
                    for (int ch = zis.read(); ch != -1; ch = zis.read())
                      os.write(ch);

                    break;
                  }
                  */

                  length -= len;

                  if (length < bufferLen) {
                    bufferLen = (int) length;
                  }
                }
              }
            } catch (IOException e) {
              Exception ex = new Exception("IOException when expanding entry "+entry+" in "+archivePath+", entry.length: "+entryLength+" entry.compressed: "+entry.getCompressedSize()+", bufferLen: "+bufferLen+", read len: "+len+", remaining: "+length,e);

              log.log(Level.FINE, ex.toString(), ex);
            } finally {
              os.close();
            }

            if (lastModified > 0)
              path.setLastModified(lastModified);
          }

          try {
            entry = zis.getNextEntry();
          } catch (IOException e) {
            log.log(Level.FINE, e.toString(), e);

            // XXX: avoids unexpected end of ZLIB input stream.
            break;
          }
        }
      } finally {
        try {
          zis.close();
        } catch (IOException e) {
        }

        rs.close();
      }
    } catch (IOException e) {
      log.log(Level.WARNING, e.toString(), e);
      // If the jar is incomplete, it should throw an exception here.
      return false;
    }

    try {
      dependPath.getParent().mkdirs();
      WriteStream os = dependPath.openWrite();

      os.println(depend.getDigest());

      os.close();
    } catch (Throwable e) {
      log.log(Level.WARNING, e.toString(), e);
    }

    return true;
  }

  /**
   * Recursively remove all files in a directory.  Used for wars when
   * they change.
   *
   * @param path root directory to start removal
   */
  protected void removeExpandDirectory(Path path)
  {
    String prefix = path.getPath();

    if (! prefix.endsWith("/"))
      prefix = prefix + "/";

    removeExpandDirectory(path, prefix);
  }

  /**
   * Recursively remove all files in a directory.  Used for wars when
   * they change.
   *
   * @param dir root directory to start removal
   */
  protected void removeExpandDirectory(Path path, String prefix)
  {
    try {
      if (path.isDirectory()) {
        String []list = path.list();
        for (int i = 0; list != null && i < list.length; i++) {
          removeExpandDirectory(path.lookup(list[i]), prefix);
        }
      }

      removeExpandFile(path, prefix);
    } catch (Throwable e) {
      log.log(Level.WARNING, e.toString(), e);
    }
  }

  /**
   * Removes an expanded file.
   */
  protected void removeExpandFile(Path path, String prefix)
    throws IOException
  {
    if (_expandCleanupFileSet == null ||
        _expandCleanupFileSet.isMatch(path, prefix))
      path.remove();
  }

  /**
   * Returns the hash code.
   */
  public int hashCode()
  {
    return getId().hashCode();
  }

  /**
   * Returns equality.
   */
  public boolean equals(Object o)
  {
    // server/125g
    if (o == null || ! getClass().equals(o.getClass()))
      return false;

    DeployController controller = (DeployController) o;

    // XXX: s/b getRootDirectory?
    return getId().equals(controller.getId());
  }
}
