/*
* 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.RepositoryService;
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()) {
RepositoryService repositoryService = RepositoryService.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();
}
}
}
}