/FileExtraction/Java_unseen/dlitz_resin/modules/resin/src/com/caucho/env/deploy/ExpandDeployController.java
/*
 * Copyright (c) 1998-2011 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.env.deploy;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.Map;
import java.util.TreeMap;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.caucho.config.ConfigException;
import com.caucho.config.types.FileSetType;
import com.caucho.env.repository.CommitBuilder;
import com.caucho.env.repository.Repository;
import com.caucho.env.repository.RepositorySystem;
import com.caucho.env.repository.RepositorySpi;
import com.caucho.env.repository.RepositoryTagEntry;
import com.caucho.env.repository.RepositoryTagListener;
import com.caucho.env.service.ResinSystem;
import com.caucho.loader.DynamicClassLoader;
import com.caucho.loader.Environment;
import com.caucho.make.DependencyContainer;
import com.caucho.util.IoUtil;
import com.caucho.util.L10N;
import com.caucho.vfs.Depend;
import com.caucho.vfs.Dependency;
import com.caucho.vfs.Path;
import com.caucho.vfs.ReadStream;
import com.caucho.vfs.Vfs;
import com.caucho.vfs.WriteStream;

/**
 * A deployment entry that expands from an archive (Jar/Zip) file.
 */
abstract public class ExpandDeployController<I extends DeployInstance>
  extends DeployController<I>
  implements RepositoryTagListener {
  private static final L10N L = new L10N(ExpandDeployController.class);
  private static final Logger log
    = Logger.getLogger(ExpandDeployController.class.getName());
  
  public static final String APPLICATION_HASH_PATH
    = "META-INF/resin.application-hash";

  private final String _autoDeployStage;
  
  private Path _rootDirectory;
  private Path _archivePath;
  
  private DeployContainerApi<?> _container;
  
  private String _rootHash;

  private boolean _isAllowRepository = true;
  private Repository _repository;
  private RepositorySpi _repositorySpi;

  private FileSetType _expandCleanupFileSet;
  
  private DeployTagItem _deployItem;
  private DeployListener _deployListener;
  
  private DependencyContainer _depend = new DependencyContainer();
  private long _dependencyCheckInterval = _depend.getCheckInterval();
  
  private Dependency _versionDependency;

  private Object _applicationExtractLock = new Object();

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

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

  protected ExpandDeployController(String id,
                                   ClassLoader loader,
                                   Path rootDirectory,
                                   DeployContainerApi<?> container)
  {
    super(id, loader);

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

    _rootDirectory = rootDirectory;
    _container = container;
    
    _autoDeployStage = "server-" + ResinSystem.getCurrentId();
  }

  /**
   * 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;
  }
  
  public void setAllowRepository(boolean isAllowRepository)
  {
    _isAllowRepository = isAllowRepository;
  }
  
  public boolean isAllowRepository()
  {
    return _isAllowRepository;
  }

  /**
   * Returns the repository
   */
  public Repository getRepository()
  {
    return _repository;
  }

  /**
   * 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<Object,Object> entry : attr.entrySet()) {
        map.put(String.valueOf(entry.getKey()),
                String.valueOf(entry.getValue()));
      }
    }

    return map;
  }

  /**
   * Sets the archive auto-remove file set.
   */
  public void addExpandCleanupFileSet(FileSetType fileSet)
  {
    if (_expandCleanupFileSet == null)
      _expandCleanupFileSet = fileSet;
    else
      _expandCleanupFileSet.add(fileSet);
  }

  /**
   * Sets the archive auto-remove file set.
   */
  public void addExpandPreserveFileset(FileSetType fileSet)
  {
    if (_expandCleanupFileSet == null)
      _expandCleanupFileSet = new FileSetType();
    
    _expandCleanupFileSet.addInverse(fileSet);
  }

  public String getAutoDeployStage()
  {
    return _autoDeployStage;
  }
  
  public void setDependencyCheckInterval(long period)
  {
    _dependencyCheckInterval = period;
    _depend.setCheckInterval(period);
  }
  
  /**
   * Final calls for init.
   */
  @Override
  protected void initEnd()
  {
    super.initEnd();
    
    if (isAllowRepository()) {
      RepositorySystem repositoryService = RepositorySystem.getCurrent();
      _repository = repositoryService.getRepository();
      _repository.addListener(getId(), this);
      _repositorySpi = repositoryService.getRepositorySpi();
    }
      
    DeployControllerService deployService = DeployControllerService.getCurrent();

    deployService.addTag(getId());
    _deployItem = deployService.getTagItem(getId());
    
    if (_container != null) {
      _deployListener = new DeployListener(_container, getId());
    
      _deployItem.addNotificationListener(_deployListener);
    }
    
    _rootHash = readRootHash();
  }

  /**
   * Merges with the new controller.
   */
  @Override
  public void merge(DeployControllerApi<I> newControllerV)
  {
    super.merge(newControllerV);

    ExpandDeployController<I> newController;
    newController = (ExpandDeployController<I>) newControllerV;

    if (newController._expandCleanupFileSet != null)
      _expandCleanupFileSet = newController._expandCleanupFileSet;

    if (newController.getArchivePath() != null)
      setArchivePath(newController.getArchivePath());
  }

  /**
   * Deploys the controller
   */
  public void deploy()
  {
    deployImpl();
  }
  
  /**
   * Deploys the controller
   */
  protected void deployImpl()
  {
    if (log.isLoggable(Level.FINER))
      log.finer(this + " deploying");
    
    try {
      extractApplication();
    } catch (Exception e) {
      // XXX: better exception
      throw new RuntimeException(e);
    }
  }

  @Override
  protected void preConfigureInstance(I deployInstance)
    throws Exception
  {
    extractApplication();
    
    addManifestClassPath();
    
    super.preConfigureInstance(deployInstance);
  }
  
  @Override
  protected void configureInstance(I deployInstance)
    throws Exception
  {
    super.configureInstance(deployInstance);
  }

  @Override
  protected void postConfigureInstance(I deployInstance)
    throws Exception
  {
    super.postConfigureInstance(deployInstance);
    
    addDependencies();
  }
  
  @Override
  public void onTagChange(String tag)
  {
    alarm();
  }

  /**
   * Extract an application from the repository.
   */
  private void extractApplication()
    throws IOException
  {
    // adds any .war file to the server-specific repository
    for (int i = 0; ! commitArchiveIgnoreException() && i < 3; i++) {
      try {
        Thread.sleep(2000);
      } catch (InterruptedException e) {
      }
    }
    
    commitArchive();
    
    synchronized (_applicationExtractLock) {
      boolean isExtract = extractFromRepository();

      postExtract(isExtract);
    }
  }
  
  /**
   * Called after the application is extracted from the repository.
   */
  protected void postExtract(boolean isExtract)
    throws IOException
  {
    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("{0} Manifest file cannot be read for '{1}'.\n  {2}",
                        this, 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);
  }

  public String getAutoDeployTag()
  {
    return (getAutoDeployStage() + "/" + getIdType() + "/" + getIdKey());
  }
  
  private boolean commitArchiveIgnoreException()
  {
    try {
      return commitArchive();
    } catch (Exception e) {
      log.log(Level.FINE, e.toString(), e);
      
      return false;
    }
  }

  /**
   * Adds any updated .war file to the server-specific repository. The 
   * application will be extracted as part of the usual repository system.
   *
   * The commitArchive() can return false if the war update fails, for example
   * if the war is in the process of updating.
   */
  private boolean commitArchive()
  throws IOException
{
    Path archivePath = getArchivePath();

    if (archivePath == null)
      return true;

    if (! archivePath.canRead())
      return true;
    
    String hash = Long.toHexString(archivePath.getCrc64());

    CommitBuilder commit = new CommitBuilder();
    commit.stage(getAutoDeployStage());
    commit.type(getIdType());
    commit.tagKey(getIdKey());

    String commitId = commit.getId();

    RepositoryTagEntry tagEntry = _repositorySpi.getTagMap().get(commitId);

    if (tagEntry != null 
        && hash.equals(tagEntry.getAttributeMap().get("archive-digest"))) {
      return true;
    }

    commit.attribute("archive-digest", hash);
    commit.message(".war added to repository from "
                   + archivePath.getNativePath());

    if (log.isLoggable(Level.FINE))
      log.fine(this + " adding archive to repository from " + archivePath);

    _repository.commitArchive(commit, archivePath);

    return true;
  }

  /**
   * Extract the contents from the repository into the root directory.
   */
  private boolean extractFromRepository()
    throws IOException
  {
    try {
      if (_repositorySpi == null)
        return false;
      
      String tag = getId();
      String treeHash = _repositorySpi.getTagContentHash(tag);
      
      Path archivePath = getArchivePath();

      if (treeHash != null && archivePath != null && archivePath.canRead()) {
        throw new ConfigException(L.l("{0} cannot be deployed from both an archive {1} and cluster deployment.",
                                      this, archivePath.getNativePath()));
      }

      if (treeHash == null) {
        tag = getAutoDeployTag();

        treeHash = _repositorySpi.getTagContentHash(tag);
      }
      
      if (treeHash == null)
        return false;
      
      if (treeHash.equals(_rootHash))
        return false;
      
      Path pwd = getRootDirectory();

      pwd.mkdirs();

      removeExpandDirectory(pwd);

      if (log.isLoggable(Level.FINE)) {
        log.fine(this + " extract from repository tag=" + tag
                 + "\n  root=" + getRootDirectory()
                 + "\n  contentHash=" + treeHash);
      }

      _repositorySpi.expandToPath(treeHash, pwd);
      
      writeRootHash(treeHash);
      
      _rootHash = treeHash;

      return true;
    } catch (ConfigException e) {
      throw e;
    } catch (IOException e) {
      log.log(Level.FINE, e.toString(), e);

      return false;
    } catch (Exception e) {
      log.log(Level.FINE, e.toString(), e);

      return false;
    }
  }

  /**
   * Reads the saved application root hash which is stored in META-INF, so
   * the application is not extracted twice.
   */
  private String readRootHash()
  {
    Path path = _rootDirectory.lookup(APPLICATION_HASH_PATH);
    
    ReadStream is = null;
    try {
      is = path.openRead();
      
      String rootHash = is.readLine();
      
      return rootHash;
    } catch (FileNotFoundException e) {
      log.log(Level.ALL, e.toString(), e);
      
      return null;
    } catch (IOException e) {
      log.log(Level.FINER, e.toString(), e);
      
      return null;
    } finally {
      IoUtil.close(is);
    }
  }
  

  /**
   * Saves the saved application root hash which is stored in META-INF, so
   * the application is not extracted twice.
   */
  private void writeRootHash(String hash)
    throws IOException
  {
    Path path = _rootDirectory.lookup(APPLICATION_HASH_PATH);
    
    WriteStream os = null;
    
    try {
      path.getParent().mkdirs();
      
      os = path.openWrite();
      
      os.println(hash);
    } finally {
      IoUtil.close(os);
    }
  }
  
  @Override
  protected boolean isControllerModified()
  {
    return _depend.isModified();
  }
  
  @Override
  protected boolean isControllerModifiedNow()
  {
    return _depend.isModified();
  }
  
  @Override
  protected boolean controllerLogModified(Logger log)
  {
    return _depend.logModified(log);
  }

  protected void addDependencies()
  {
    _depend = new DependencyContainer();
    _depend.setCheckInterval(_dependencyCheckInterval);
    
    if (getArchivePath() != null)
      _depend.add(new Depend(getArchivePath()));

    String value = _repositorySpi.getTagContentHash(getId());
    _depend.add(new RepositoryDependency(getId(), value));
    
    value = _repositorySpi.getTagContentHash(getAutoDeployTag());
    _depend.add(new RepositoryDependency(getAutoDeployTag(), value));
  }
  
  public Dependency getVersionDependency()
  {
    return _versionDependency;
  }
  
  public void setVersionDependency(Dependency versionDependency)
  {
    _versionDependency = versionDependency;
  }

  /**
   * 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);
  }
  
  public Throwable getConfigException()
  {
    return null;
  }
 
  /**
   * 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();
    }
  }
  
  //
  // state callbacks
  //
  
  @Override
  protected void onActive()
  {
    super.onActive();
    
    if (_deployItem != null && ! "error".equals(_deployItem.getState()))
      _deployItem.onStart();
  }
  
  @Override
  protected void onError(Throwable e)
  {
    super.onError(e);
    
    if (_deployItem != null)
      _deployItem.toError(e);
  }
  
  @Override
  protected void onStop()
  {
    super.onStop();
    
    if (_deployItem != null)
      _deployItem.toStop();
  }
  
  @Override
  protected void onDestroy()
  {
    super.onDestroy();
    
    if (_deployItem != null)
      _deployItem.removeNotificationListener(_deployListener);
  }

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

  /**
   * Returns equality.
   */
  @Override
  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());
  }
  
  static class DeployListener implements DeployNotificationListener
  {
    private WeakReference<DeployContainerApi<?>> _container;
    private String _tag;
    
    DeployListener(DeployContainerApi<?> container, String tag)
    {
      _container = new WeakReference<DeployContainerApi<?>>(container);
      _tag = tag;
      
      // 
    }

    @Override
    public void onStart()
    {
      DeployContainerApi<?> container = _container.get();
      
      if (container != null) {
        DeployControllerApi<?> controller = container.findControllerById(_tag);
        
        if (controller != null)
          controller.start();
      }
    }

    @Override
    public void onStop()
    {
      DeployContainerApi<?> container = _container.get();
      
      if (container != null) {
        DeployControllerApi<?> controller = container.findControllerById(_tag);
        
        if (controller != null)
          controller.stop();
      }
    }
  }
}
