/*
* eXist Open Source Native XML Database
* Copyright (C) 2010-2015 The eXist-db Project
* http://exist-db.org
*
* This program 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
* of the License, or (at your option) any later version.
*
* This program 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 program; if not, write to the Free Software Foundation
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
*/
package org.exist.xquery.modules.expathrepo;
import java.io.IOException;
import java.io.InputStream;
import java.net.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Optional;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.exist.SystemProperties;
import org.exist.dom.persistent.DocumentImpl;
import org.exist.dom.QName;
import org.exist.dom.memtree.MemTreeBuilder;
import org.exist.repo.Deployment;
import org.exist.repo.PackageLoader;
import org.exist.security.PermissionDeniedException;
import org.exist.storage.NativeBroker;
import org.exist.storage.lock.Lock.LockMode;
import org.exist.xmldb.XmldbURI;
import org.exist.xquery.*;
import org.exist.xquery.value.*;
import org.expath.pkg.repo.PackageException;
import org.xml.sax.helpers.AttributesImpl;
public class Deploy extends BasicFunction {
protected static final Logger logger = LogManager.getLogger(Deploy.class);
public final static FunctionSignature signatures[] = {
new FunctionSignature(
new QName("deploy", ExpathPackageModule.NAMESPACE_URI, ExpathPackageModule.PREFIX),
"Deploy an application package. Installs package contents to the specified target collection, using the permissions " +
"defined by the <permissions> element in repo.xml. Pre- and post-install XQuery scripts can be specified " +
"via the <prepare> and <finish> elements.",
new SequenceType[] { new FunctionParameterSequenceType("pkgName", Type.STRING, Cardinality.EXACTLY_ONE, "package name")},
new FunctionReturnSequenceType(Type.ELEMENT, Cardinality.EXACTLY_ONE,
"<status result=\"ok\"/> if deployment was ok. Throws an error otherwise.")),
new FunctionSignature(
new QName("deploy", ExpathPackageModule.NAMESPACE_URI, ExpathPackageModule.PREFIX),
"Deploy an application package. Installs package contents to the specified target collection, using the permissions " +
"defined by the <permissions> element in repo.xml. Pre- and post-install XQuery scripts can be specified " +
"via the <prepare> and <finish> elements.",
new SequenceType[] {
new FunctionParameterSequenceType("pkgName", Type.STRING, Cardinality.EXACTLY_ONE, "package name"),
new FunctionParameterSequenceType("targetCollection", Type.STRING, Cardinality.EXACTLY_ONE, "the target " +
"collection into which the package will be stored")
},
new FunctionReturnSequenceType(Type.ELEMENT, Cardinality.EXACTLY_ONE,
"<status result=\"ok\"/> if deployment was ok. Throws an error otherwise.")),
new FunctionSignature(
new QName("install-and-deploy", ExpathPackageModule.NAMESPACE_URI, ExpathPackageModule.PREFIX),
"Downloads, installs and deploys a package from the public repository at $publicRepoURL. Dependencies are resolved " +
"automatically. For downloading the package, the package name is appended to the repository URL as " +
"parameter 'name'.",
new SequenceType[] {
new FunctionParameterSequenceType("pkgName", Type.STRING, Cardinality.EXACTLY_ONE,
"Unique name of the package to install."),
new FunctionParameterSequenceType("publicRepoURL", Type.STRING, Cardinality.EXACTLY_ONE,
"The URL of the public repo.")
},
new FunctionReturnSequenceType(Type.ELEMENT, Cardinality.EXACTLY_ONE,
"<status result=\"ok\"/> if deployment was ok. Throws an error otherwise.")),
new FunctionSignature(
new QName("install-and-deploy", ExpathPackageModule.NAMESPACE_URI, ExpathPackageModule.PREFIX),
"Downloads, installs and deploys a package from the public repository at $publicRepoURL. Dependencies are resolved " +
"automatically. For downloading the package, the package name and version are appended to the repository URL as " +
"parameters 'name' and 'version'.",
new SequenceType[] {
new FunctionParameterSequenceType("pkgName", Type.STRING, Cardinality.EXACTLY_ONE,
"Unique name of the package to install."),
new FunctionParameterSequenceType("version", Type.STRING, Cardinality.ZERO_OR_ONE,
"Version to install."),
new FunctionParameterSequenceType("publicRepoURL", Type.STRING, Cardinality.EXACTLY_ONE,
"The URL of the public repo.")
},
new FunctionReturnSequenceType(Type.ELEMENT, Cardinality.EXACTLY_ONE,
"<status result=\"ok\"/> if deployment was ok. Throws an error otherwise.")),
new FunctionSignature(
new QName("install-and-deploy-from-db", ExpathPackageModule.NAMESPACE_URI, ExpathPackageModule.PREFIX),
"Installs and deploys a package from a .xar archive file stored in the database. Dependencies are not " +
"resolved and will just be ignored.",
new SequenceType[] {
new FunctionParameterSequenceType("path", Type.STRING, Cardinality.EXACTLY_ONE,
"Database path to the package archive (.xar file)")
},
new FunctionReturnSequenceType(Type.ELEMENT, Cardinality.EXACTLY_ONE,
"<status result=\"ok\"/> if deployment was ok. Throws an error otherwise.")),
new FunctionSignature(
new QName("install-and-deploy-from-db", ExpathPackageModule.NAMESPACE_URI, ExpathPackageModule.PREFIX),
"Installs and deploys a package from a .xar archive file stored in the database. Dependencies will be downloaded " +
"from the public repo and installed automatically.",
new SequenceType[] {
new FunctionParameterSequenceType("path", Type.STRING, Cardinality.EXACTLY_ONE,
"Database path to the package archive (.xar file)"),
new FunctionParameterSequenceType("publicRepoURL", Type.STRING, Cardinality.EXACTLY_ONE,
"The URL of the public repo.")
},
new FunctionReturnSequenceType(Type.ELEMENT, Cardinality.EXACTLY_ONE,
"<status result=\"ok\"/> if deployment was ok. Throws an error otherwise.")),
new FunctionSignature(
new QName("undeploy", ExpathPackageModule.NAMESPACE_URI, ExpathPackageModule.PREFIX),
"Uninstall the resources belonging to a package from the db. Calls cleanup scripts if defined.",
new SequenceType[] { new FunctionParameterSequenceType("pkgName", Type.STRING, Cardinality.EXACTLY_ONE, "package name")},
new FunctionReturnSequenceType(Type.ELEMENT, Cardinality.EXACTLY_ONE,
"<status result=\"ok\"/> if deployment was ok. Throws an error otherwise."))
};
private static final QName STATUS_ELEMENT = new QName("status", ExpathPackageModule.NAMESPACE_URI);
public Deploy(final XQueryContext context, final FunctionSignature signature) {
super(context, signature);
}
@Override
public Sequence eval(final Sequence[] args, final Sequence contextSequence)
throws XPathException {
if (!context.getSubject().hasDbaRole())
throw new XPathException(this, EXPathErrorCode.EXPDY003, "Permission denied. You need to be a member " +
"of the dba group to use repo:deploy/undeploy");
final String pkgName = args[0].getStringValue();
try {
Deployment deployment = new Deployment(context.getBroker());
final Optional<String> target;
if (isCalledAs("deploy")) {
String userTarget = null;
if (getArgumentCount() == 2) {
userTarget = args[1].getStringValue();
}
target = deployment.deploy(pkgName, context.getRepository(), userTarget);
} else if (isCalledAs("install-and-deploy")) {
String version = null;
final String repoURI;
if (getArgumentCount() == 3) {
version = args[1].getStringValue();
repoURI = args[2].getStringValue();
} else {
repoURI = args[1].getStringValue();
}
target = installAndDeploy(pkgName, version, repoURI);
} else if (isCalledAs("install-and-deploy-from-db")) {
String repoURI = null;
if (getArgumentCount() == 2) {
repoURI = args[1].getStringValue();
}
target = installAndDeployFromDb(pkgName, repoURI);
} else {
target = deployment.undeploy(pkgName, context.getRepository());
}
target.orElseThrow(() -> new XPathException("expath repository is not available."));
return statusReport(target);
} catch (PackageException e) {
throw new XPathException(this, EXPathErrorCode.EXPDY001, e.getMessage());
} catch (IOException e) {
throw new XPathException(this, ErrorCodes.FOER0000, "Caught IO error while deploying expath archive");
}
}
private Optional<String> installAndDeploy(final String pkgName, final String version, final String repoURI) throws XPathException {
try {
final RepoPackageLoader loader = new RepoPackageLoader(repoURI);
final Deployment deployment = new Deployment(context.getBroker());
final Path xar = loader.load(pkgName, new PackageLoader.Version(version, false));
if (xar != null) {
return deployment.installAndDeploy(xar, loader);
}
return Optional.empty();
} catch (final MalformedURLException e) {
throw new XPathException(this, EXPathErrorCode.EXPDY005, "Malformed URL: " + repoURI);
} catch (final PackageException | IOException e) {
LOG.error(e.getMessage(), e);
throw new XPathException(this, EXPathErrorCode.EXPDY007, e.getMessage());
}
}
private Optional<String> installAndDeployFromDb(final String path, final String repoURI) throws XPathException {
final XmldbURI docPath = XmldbURI.createInternal(path);
DocumentImpl doc = null;
try {
doc = context.getBroker().getXMLResource(docPath, LockMode.READ_LOCK);
if (doc.getResourceType() != DocumentImpl.BINARY_FILE)
throw new XPathException(this, EXPathErrorCode.EXPDY001, path + " is not a valid .xar", new StringValue(path));
final Path file = ((NativeBroker)context.getBroker()).getCollectionBinaryFileFsPath(doc.getURI());
RepoPackageLoader loader = null;
if (repoURI != null) {
loader = new RepoPackageLoader(repoURI);
}
final Deployment deployment = new Deployment(context.getBroker());
return deployment.installAndDeploy(file, loader);
} catch (PackageException | IOException | PermissionDeniedException e) {
LOG.error(e.getMessage(), e);
throw new XPathException(this, EXPathErrorCode.EXPDY007, "Package installation failed: " + e.getMessage(), new StringValue(e.getMessage()));
} finally {
if (doc != null)
doc.getUpdateLock().release(LockMode.READ_LOCK);
}
}
private Sequence statusReport(final Optional<String> target) {
context.pushDocumentContext();
try {
final MemTreeBuilder builder = context.getDocumentBuilder();
final AttributesImpl attrs = new AttributesImpl();
if (target.isPresent()) {
attrs.addAttribute("", "result", "result", "CDATA", "ok");
attrs.addAttribute("", "target", "target", "CDATA", target.get());
} else {
attrs.addAttribute("", "result", "result", "CDATA", "fail");
}
builder.startElement(STATUS_ELEMENT, attrs);
builder.endElement();
return builder.getDocument().getNode(1);
} finally {
context.popDocumentContext();
}
}
@Override
public void resetState(final boolean postOptimization) {
super.resetState(postOptimization);
}
private static class RepoPackageLoader implements PackageLoader {
private final String repoURL;
public RepoPackageLoader(final String repoURL) {
this.repoURL = repoURL;
}
public Path load(final String name, final Version version) throws IOException {
String pkgURL = repoURL + "?name=" + URLEncoder.encode(name, "UTF-8") +
"&processor=" + SystemProperties.getInstance().getSystemProperty("product-version", "2.2.0");
if (version != null) {
if (version.getMin() != null) {
pkgURL += "&semver-min=" + version.getMin();
}
if (version.getMax() != null) {
pkgURL += "&semver-max=" + version.getMax();
}
if (version.getSemVer() != null) {
pkgURL += "&semver=" + version.getSemVer();
}
if (version.getVersion() != null) {
pkgURL += "&version=" + URLEncoder.encode(version.getVersion(), "UTF-8");
}
}
LOG.info("Retrieving package from " + pkgURL);
final HttpURLConnection connection = (HttpURLConnection) new URL(pkgURL).openConnection();
connection.setConnectTimeout(15 * 1000);
connection.setReadTimeout(15 * 1000);
connection.setRequestMethod("GET");
connection.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.1.2) " +
"Gecko/20090729 Firefox/3.5.2 (.NET CLR 3.5.30729)");
connection.connect();
try(final InputStream is = connection.getInputStream()) {
final Path outFile = Files.createTempFile("deploy", "xar");
Files.copy(is, outFile, StandardCopyOption.REPLACE_EXISTING);
return outFile;
} catch (IOException e) {
throw new IOException("Failed to install dependency from " + pkgURL + ": " + e.getMessage());
}
}
}
}