org.exist.repo.Deployment Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of exist-core Show documentation
Show all versions of exist-core Show documentation
eXist-db NoSQL Database Core
/*
* eXist Open Source Native XML Database
* Copyright (C) 2001-2015 The eXist 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 library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
package org.exist.repo;
import com.evolvedbinary.j8fu.Either;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.exist.EXistException;
import org.exist.SystemProperties;
import org.exist.collections.Collection;
import org.exist.collections.IndexInfo;
import org.exist.collections.triggers.TriggerException;
import org.exist.dom.QName;
import org.exist.dom.memtree.*;
import org.exist.dom.persistent.BinaryDocument;
import org.exist.security.Permission;
import org.exist.security.PermissionDeniedException;
import org.exist.security.PermissionFactory;
import org.exist.security.UnixStylePermission;
import org.exist.security.internal.aider.GroupAider;
import org.exist.security.internal.aider.UserAider;
import org.exist.source.FileSource;
import org.exist.storage.DBBroker;
import org.exist.storage.txn.Txn;
import org.exist.util.*;
import org.exist.util.serializer.AttrList;
import org.exist.xmldb.XmldbURI;
import org.exist.xquery.*;
import org.exist.xquery.util.DocUtils;
import org.exist.xquery.value.DateTimeValue;
import org.exist.xquery.value.Sequence;
import org.exist.xquery.value.SequenceIterator;
import org.exist.xquery.value.Type;
import org.expath.pkg.repo.*;
import org.expath.pkg.repo.Package;
import org.expath.pkg.repo.deps.DependencyVersion;
import org.expath.pkg.repo.tui.BatchUserInteraction;
import org.w3c.dom.Element;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Deploy a .xar package into the database using the information provided
* in expath-pkg.xml and repo.xml.
*/
public class Deployment {
public final static String PROPERTY_APP_ROOT = "repo.root-collection";
private final static Logger LOG = LogManager.getLogger(Deployment.class);
public final static String PROCESSOR_NAME = "http://exist-db.org";
private final static String REPO_NAMESPACE = "http://exist-db.org/xquery/repo";
private final static String PKG_NAMESPACE = "http://expath.org/ns/pkg";
private final static QName SETUP_ELEMENT = new QName("setup", REPO_NAMESPACE);
private static final QName PRE_SETUP_ELEMENT = new QName("prepare", REPO_NAMESPACE);
private static final QName POST_SETUP_ELEMENT = new QName("finish", REPO_NAMESPACE);
private static final QName TARGET_COLL_ELEMENT = new QName("target", REPO_NAMESPACE);
private static final QName PERMISSIONS_ELEMENT = new QName("permissions", REPO_NAMESPACE);
private static final QName CLEANUP_ELEMENT = new QName("cleanup", REPO_NAMESPACE);
private static final QName DEPLOYED_ELEMENT = new QName("deployed", REPO_NAMESPACE);
private static final QName DEPENDENCY_ELEMENT = new QName("dependency", PKG_NAMESPACE);
private static final QName RESOURCES_ELEMENT = new QName("resources", REPO_NAMESPACE);
private static final String RESOURCES_PATH_ATTRIBUTE = "path";
private static class RequestedPerms {
final String user;
final String password;
final Optional group;
final Either permissions;
private RequestedPerms(final String user, final String password, final Optional group, final Either permissions) {
this.user = user;
this.password = password;
this.group = group;
this.permissions = permissions;
}
}
// private Optional requestedPerms = Optional.empty();
protected Optional getPackageDir(final String pkgName, final Optional repo) throws PackageException {
Optional packageDir = Optional.empty();
if (repo.isPresent()) {
for (final Packages pp : repo.get().getParentRepo().listPackages()) {
final org.expath.pkg.repo.Package pkg = pp.latest();
if (pkg.getName().equals(pkgName)) {
packageDir = Optional.of(getPackageDir(pkg));
}
}
}
return packageDir;
}
protected Path getPackageDir(final Package pkg) {
final FileSystemStorage.FileSystemResolver resolver = (FileSystemStorage.FileSystemResolver) pkg.getResolver();
return resolver.resolveResourceAsFile("");
}
protected Optional getPackage(final String pkgName, final Optional repo) throws PackageException {
if (repo.isPresent()) {
for (final Packages pp : repo.get().getParentRepo().listPackages()) {
final org.expath.pkg.repo.Package pkg = pp.latest();
if (pkg.getName().equals(pkgName)) {
return Optional.ofNullable(pkg);
}
}
}
return Optional.empty();
}
protected DocumentImpl getRepoXML(final DBBroker broker, final Path packageDir) throws PackageException {
// find and parse the repo.xml descriptor
final Path repoFile = packageDir.resolve("repo.xml");
if (!Files.isReadable(repoFile)) {
return null;
}
try(final InputStream is = Files.newInputStream(repoFile)) {
return DocUtils.parse(broker.getBrokerPool(), null, is);
} catch (final XPathException | IOException e) {
throw new PackageException("Failed to parse repo.xml: " + e.getMessage(), e);
}
}
public Optional installAndDeploy(final DBBroker broker, final Txn transaction, final XarSource xar, final PackageLoader loader) throws PackageException, IOException {
return installAndDeploy(broker, transaction, xar, loader, true);
}
/**
* Install and deploy a give xar archive. Dependencies are installed from
* the PackageLoader.
*
* @param broker the broker to use
* @param transaction the transaction for this deployment task
* @param xar the .xar file to install
* @param loader package loader to use
* @param enforceDeps when set to true, the method will throw an exception if a dependency could not be resolved
* or an older version of the required dependency is installed and needs to be replaced.
* @return the collection path to which the package was deployed or Optional.empty if not deployed
* @throws PackageException if package installation failed
* @throws IOException in case of an IO error
*/
public Optional installAndDeploy(final DBBroker broker, final Txn transaction, final XarSource xar, final PackageLoader loader, boolean enforceDeps) throws PackageException, IOException {
final Optional descriptor = getDescriptor(broker, xar);
if(!descriptor.isPresent()) {
throw new PackageException("Missing descriptor from package: " + xar.getURI());
}
final DocumentImpl document = descriptor.get();
final ElementImpl root = (ElementImpl) document.getDocumentElement();
final String name = root.getAttribute("name");
final String pkgVersion = root.getAttribute("version");
final Optional repo = broker.getBrokerPool().getExpathRepo();
if (repo.isPresent()) {
final Packages packages = repo.get().getParentRepo().getPackages(name);
if (packages != null && (!enforceDeps || pkgVersion.equals(packages.latest().getVersion()))) {
LOG.info("Application package " + name + " already installed. Skipping.");
final Package pkg = packages.latest();
return Optional.of(getTargetCollection(broker, pkg, getPackageDir(pkg)));
}
InMemoryNodeSet deps;
try {
deps = findElements(root, DEPENDENCY_ELEMENT);
for (final SequenceIterator i = deps.iterate(); i.hasNext(); ) {
final Element dependency = (Element) i.nextItem();
final String pkgName = dependency.getAttribute("package");
final String processor = dependency.getAttribute("processor");
final String versionStr = dependency.getAttribute("version");
final String semVer = dependency.getAttribute("semver");
final String semVerMin = dependency.getAttribute("semver-min");
final String semVerMax = dependency.getAttribute("semver-max");
PackageLoader.Version version = null;
if (semVer != null) {
version = new PackageLoader.Version(semVer, true);
} else if (semVerMax != null || semVerMin != null) {
version = new PackageLoader.Version(semVerMin, semVerMax);
} else if (pkgVersion != null) {
version = new PackageLoader.Version(versionStr, false);
}
if (processor != null && processor.equals(PROCESSOR_NAME) && version != null) {
checkProcessorVersion(version);
} else if (pkgName != null) {
LOG.info("Package " + name + " depends on " + pkgName);
boolean isInstalled = false;
if (repo.get().getParentRepo().getPackages(pkgName) != null) {
LOG.debug("Package " + pkgName + " already installed");
Packages pkgs = repo.get().getParentRepo().getPackages(pkgName);
// check if installed package matches required version
if (pkgs != null) {
if (version != null) {
Package latest = pkgs.latest();
DependencyVersion depVersion = version.getDependencyVersion();
if (depVersion.isCompatible(latest.getVersion())) {
isInstalled = true;
} else {
LOG.debug("Package " + pkgName + " needs to be upgraded");
if (enforceDeps) {
throw new PackageException("Package requires version " + version.toString() +
" of package " + pkgName +
". Installed version is " + latest.getVersion() + ". Please upgrade!");
}
}
} else {
isInstalled = true;
}
if (isInstalled) {
LOG.debug("Package " + pkgName + " already installed");
}
}
}
if (!isInstalled && loader != null) {
final XarSource depFile = loader.load(pkgName, version);
if (depFile != null) {
installAndDeploy(broker, transaction, depFile, loader);
} else {
if (enforceDeps) {
LOG.warn("Missing dependency: package " + pkgName + " could not be resolved. This error " +
"is not fatal, but the package may not work as expected");
} else {
throw new PackageException("Missing dependency: package " + pkgName + " could not be resolved.");
}
}
}
}
}
} catch (final XPathException e) {
throw new PackageException("Invalid descriptor found in " + xar.getURI());
}
// installing the xar into the expath repo
LOG.info("Installing package " + xar.getURI());
final UserInteractionStrategy interact = new BatchUserInteraction();
final org.expath.pkg.repo.Package pkg = repo.get().getParentRepo().installPackage(xar, true, interact);
final ExistPkgInfo info = (ExistPkgInfo) pkg.getInfo("exist");
if (info != null && !info.getJars().isEmpty()) {
ClasspathHelper.updateClasspath(broker.getBrokerPool(), pkg);
}
broker.getBrokerPool().getXQueryPool().clear();
final String pkgName = pkg.getName();
// signal status
broker.getBrokerPool().reportStatus("Installing app: " + pkg.getAbbrev());
repo.get().reportAction(ExistRepository.Action.INSTALL, pkg.getName());
LOG.info("Deploying package " + pkgName);
return deploy(broker, transaction, pkgName, repo, null);
}
// Totally unnecessary to do the above if repo is unavailable.
return Optional.empty();
}
private void checkProcessorVersion(final PackageLoader.Version version) throws PackageException {
final String procVersion = SystemProperties.getInstance().getSystemProperty("product-version", "1.0");
final DependencyVersion depVersion = version.getDependencyVersion();
if (!depVersion.isCompatible(procVersion)) {
throw new PackageException("Package requires eXist-db version " + version.toString() + ". " +
"Installed version is " + procVersion);
}
}
public Optional undeploy(final DBBroker broker, final Txn transaction, final String pkgName, final Optional repo) throws PackageException {
final Optional maybePackageDir = getPackageDir(pkgName, repo);
if (!maybePackageDir.isPresent()) {
// fails silently if package dir is not found?
return Optional.empty();
}
final Path packageDir = maybePackageDir.get();
final Optional pkg = getPackage(pkgName, repo);
final DocumentImpl repoXML;
try {
repoXML = getRepoXML(broker, packageDir);
} catch (PackageException e) {
if (pkg.isPresent()) {
uninstall(broker, transaction, pkg.get(), Optional.empty());
}
throw new PackageException("Failed to remove package from database " +
"due to error in repo.xml: " + e.getMessage(), e);
}
if (repoXML != null) {
try {
final Optional cleanup = findElement(repoXML, CLEANUP_ELEMENT);
if(cleanup.isPresent()) {
runQuery(broker, null, packageDir, cleanup.get().getStringValue(), pkgName, QueryPurpose.UNDEPLOY);
}
final Optional target = findElement(repoXML, TARGET_COLL_ELEMENT);
if (pkg.isPresent()) {
uninstall(broker, transaction, pkg.get(), target);
}
return target.map(e -> Optional.ofNullable(e.getStringValue())).orElseGet(() -> Optional.of(getTargetFallback(pkg.get()).getCollectionPath()));
} catch (final XPathException e) {
throw new PackageException("Error found while processing repo.xml: " + e.getMessage(), e);
} catch (final IOException e) {
throw new PackageException("Error found while processing repo.xml: " + e.getMessage(), e);
}
} else {
// we still may need to remove the copy of the package from /db/system/repo
if (pkg.isPresent()) {
uninstall(broker, transaction, pkg.get(), Optional.empty());
}
}
return Optional.empty();
}
public Optional deploy(final DBBroker broker, final Txn transaction, final String pkgName, final Optional repo, final String userTarget) throws PackageException, IOException {
final Optional maybePackageDir = getPackageDir(pkgName, repo);
if (!maybePackageDir.isPresent()) {
throw new PackageException("Package not found: " + pkgName);
}
final Path packageDir = maybePackageDir.get();
final DocumentImpl repoXML = getRepoXML(broker, packageDir);
if (repoXML == null) {
return Optional.empty();
}
try {
// if there's a element, run the query it points to
final Optional setup = findElement(repoXML, SETUP_ELEMENT);
final Optional setupPath = setup.map(ElementImpl::getStringValue).filter(s -> !s.isEmpty());
if (setupPath.isPresent()) {
runQuery(broker, null, packageDir, setupPath.get(), pkgName, QueryPurpose.SETUP);
return Optional.empty();
} else {
// otherwise copy all child directories to the target collection
XmldbURI targetCollection = null;
if (userTarget != null) {
try {
targetCollection = XmldbURI.create(userTarget);
} catch (final IllegalArgumentException e) {
throw new PackageException("Bad collection URI: " + userTarget, e);
}
} else {
final Optional target = findElement(repoXML, TARGET_COLL_ELEMENT);
final Optional targetPath = target.map(ElementImpl::getStringValue).filter(s -> !s.isEmpty());
if (targetPath.isPresent()) {
// determine target collection
try {
targetCollection = XmldbURI.create(getTargetCollection(broker, targetPath.get()));
} catch (final IllegalArgumentException e) {
throw new PackageException("Bad collection URI for element: " + targetPath.get(), e);
}
} else {
LOG.warn("EXPath Package '" + pkgName + "' does not contain a in its repo.xml, no files will be deployed to /apps");
}
}
if (targetCollection == null) {
// no target means: package does not need to be deployed into database
// however, we need to preserve a copy for backup purposes
final Optional pkg = getPackage(pkgName, repo);
pkg.orElseThrow(() -> new XPathException("expath repository is not available so the package was not stored."));
final String pkgColl = pkg.get().getAbbrev() + "-" + pkg.get().getVersion();
targetCollection = XmldbURI.SYSTEM.append("repo/" + pkgColl);
}
// extract the permissions (if any)
final Optional permissions = findElement(repoXML, PERMISSIONS_ELEMENT);
final Optional requestedPerms = permissions.flatMap(elem -> {
final Optional> perms = Optional.ofNullable(elem.getAttribute("mode")).flatMap(mode -> {
try {
return Optional.of(Either.Left(Integer.parseInt(mode, 8)));
} catch(final NumberFormatException e) {
if(mode.matches("^[rwx-]{9}")) {
return Optional.of(Either.Right(mode));
} else {
return Optional.empty();
}
}
});
return perms.map(p -> new RequestedPerms(
elem.getAttribute("user"),
elem.getAttribute("password"),
Optional.ofNullable(elem.getAttribute("group")),
p
));
});
//check that if there were permissions then we were able to parse them, a failure would be related to the mode string
if(permissions.isPresent() && !requestedPerms.isPresent()) {
final String mode = permissions.map(elem -> elem.getAttribute("mode")).orElse(null);
throw new PackageException("Bad format for mode attribute in : " + mode);
}
// run the pre-setup query if present
final Optional preSetup = findElement(repoXML, PRE_SETUP_ELEMENT);
final Optional preSetupPath = preSetup.map(ElementImpl::getStringValue).filter(s -> !s.isEmpty());
if(preSetupPath.isPresent()) {
runQuery(broker, targetCollection, packageDir, preSetupPath.get(), pkgName, QueryPurpose.PREINSTALL);
}
// any required users and group should have been created by the pre-setup query.
// check for invalid users now.
if(requestedPerms.isPresent()) {
checkUserSettings(broker, requestedPerms.get());
}
final InMemoryNodeSet resources = findElements(repoXML,RESOURCES_ELEMENT);
// install
final List errors = scanDirectory(broker, transaction, packageDir, targetCollection, resources, true, false,
requestedPerms);
// run the post-setup query if present
final Optional postSetup = findElement(repoXML, POST_SETUP_ELEMENT);
final Optional postSetupPath = postSetup.map(ElementImpl::getStringValue).filter(s -> !s.isEmpty());
if(postSetupPath.isPresent()) {
runQuery(broker, targetCollection, packageDir, postSetupPath.get(), pkgName, QueryPurpose.POSTINSTALL);
}
storeRepoXML(broker, transaction, repoXML, targetCollection, requestedPerms);
// TODO: it should be safe to clean up the file system after a package
// has been deployed. Might be enabled after 2.0
//cleanup(pkgName, repo);
if (!errors.isEmpty()) {
throw new PackageException("Deployment incomplete, " + errors.size() + " issues found: " +
errors.stream().collect(Collectors.joining("; ")));
}
return Optional.ofNullable(targetCollection.getCollectionPath());
}
} catch (final XPathException e) {
throw new PackageException("Error found while processing repo.xml: " + e.getMessage(), e);
}
}
/**
* After deployment, clean up the package directory and remove all files which have been
* stored into the db. They are not needed anymore. Only preserve the descriptors and the
* contents directory.
*
* @param pkgName
* @param repo
* @throws PackageException
*/
private void cleanup(final String pkgName, final Optional repo) throws PackageException {
if (repo.isPresent()) {
final Optional pkg = getPackage(pkgName, repo);
final Optional maybePackageDir = pkg.map(this::getPackageDir);
if (!maybePackageDir.isPresent()) {
throw new PackageException("Cleanup: package dir for package " + pkgName + " not found");
}
final Path packageDir = maybePackageDir.get();
final String abbrev = pkg.get().getAbbrev();
try(final Stream filesToDelete = Files.find(packageDir, 1, (path, attrs) -> {
if(path.equals(packageDir)) {
return false;
}
final String name = FileUtils.fileName(path);
if (attrs.isDirectory()) {
return !(name.equals(abbrev) || name.equals("content"));
} else {
return !(name.equals("expath-pkg.xml") || name.equals("repo.xml") ||
"exist.xml".equals(name) || name.startsWith("icon"));
}
})) {
filesToDelete.forEach(path -> {
try {
Files.deleteIfExists(path);
} catch(final IOException ioe) {
LOG.warn("Cleanup: failed to delete file " + path.toAbsolutePath().toString() + " in package " + pkgName);
}
});
} catch (final IOException ioe) {
LOG.warn("Cleanup: failed to delete files", ioe);
}
}
}
/**
* Get the target collection for the given package, which resides in pkgDir.
* Returns path to cached .xar for library packages.
*
* @param broker
* @param pkg
* @param pkgDir
* @return
* @throws PackageException
*/
private String getTargetCollection(final DBBroker broker, final Package pkg, final Path pkgDir) throws PackageException {
final DocumentImpl repoXML = getRepoXML(broker, pkgDir);
if (repoXML != null) {
try {
final Optional target = findElement(repoXML, TARGET_COLL_ELEMENT);
return target.map(ElementImpl::getStringValue).map(s -> getTargetCollection(broker, s)).map(XmldbURI::create).map(XmldbURI::getCollectionPath)
.orElseGet(() -> getTargetFallback(pkg).getCollectionPath());
} catch (XPathException e) {
throw new PackageException("Failed to determine target collection");
}
} else {
return getTargetFallback(pkg).getCollectionPath();
}
}
private XmldbURI getTargetFallback(final Package pkg) {
final String pkgColl = pkg.getAbbrev() + "-" + pkg.getVersion();
return XmldbURI.SYSTEM.append("repo/" + pkgColl);
}
private String getTargetCollection(final DBBroker broker, String targetFromRepo) {
final String appRoot = (String) broker.getConfiguration().getProperty(PROPERTY_APP_ROOT);
if (appRoot != null) {
if (targetFromRepo.startsWith("/db/")) {
targetFromRepo = targetFromRepo.substring(4);
}
return appRoot + targetFromRepo;
}
if (targetFromRepo.startsWith("/db")) {
return targetFromRepo;
} else {
return "/db/" + targetFromRepo;
}
}
/**
* Delete the target collection of the package. If there's no repo.xml descriptor,
* target will be null.
*
* @param pkg
* @param target
* @throws PackageException
*/
private void uninstall(final DBBroker broker, final Txn transaction, final Package pkg, final Optional target)
throws PackageException {
// determine target collection
final Optional targetPath = target.map(ElementImpl::getStringValue).filter(s -> !s.isEmpty());
final XmldbURI targetCollection = targetPath.map(s -> XmldbURI.create(getTargetCollection(broker, s)))
.orElseGet(() -> getTargetFallback(pkg));
try {
Collection collection = broker.getOrCreateCollection(transaction, targetCollection);
if (collection != null) {
broker.removeCollection(transaction, collection);
}
if (target != null) {
final XmldbURI configCollection = XmldbURI.CONFIG_COLLECTION_URI.append(targetCollection);
collection = broker.getOrCreateCollection(transaction, configCollection);
if (collection != null) {
broker.removeCollection(transaction, collection);
}
}
} catch (final PermissionDeniedException | IOException | TriggerException e) {
LOG.error("Exception occurred while removing package.", e);
}
}
/**
* Store repo.xml into the db. Adds the time of deployment to the descriptor.
*
* @param repoXML
* @param targetCollection
* @throws XPathException
*/
private void storeRepoXML(final DBBroker broker, final Txn transaction, final DocumentImpl repoXML, final XmldbURI targetCollection, final Optional requestedPerms)
throws PackageException, XPathException {
// Store repo.xml
final DateTimeValue time = new DateTimeValue(new Date());
final MemTreeBuilder builder = new MemTreeBuilder();
builder.startDocument();
final UpdatingDocumentReceiver receiver = new UpdatingDocumentReceiver(builder, time.getStringValue());
try {
repoXML.copyTo(broker, receiver);
} catch (final SAXException e) {
throw new PackageException("Error while updating repo.xml in-memory: " + e.getMessage(), e);
}
builder.endDocument();
final DocumentImpl updatedXML = builder.getDocument();
try {
final Collection collection = broker.getOrCreateCollection(transaction, targetCollection);
final XmldbURI name = XmldbURI.createInternal("repo.xml");
final IndexInfo info = collection.validateXMLResource(transaction, broker, name, updatedXML);
final Permission permission = info.getDocument().getPermissions();
setPermissions(broker, requestedPerms, false, MimeType.XML_TYPE, permission);
collection.store(transaction, broker, info, updatedXML);
} catch (final PermissionDeniedException | IOException | SAXException | LockException | EXistException e) {
throw new PackageException("Error while storing updated repo.xml: " + e.getMessage(), e);
}
}
private void checkUserSettings(final DBBroker broker, final RequestedPerms requestedPerms) throws PackageException {
final org.exist.security.SecurityManager secman = broker.getBrokerPool().getSecurityManager();
try {
if (requestedPerms.group.filter(g -> !secman.hasGroup(g)).isPresent()) {
secman.addGroup(broker, new GroupAider(requestedPerms.group.get()));
}
if (!secman.hasAccount(requestedPerms.user)) {
final UserAider aider = new UserAider(requestedPerms.user);
aider.setPassword(requestedPerms.password);
requestedPerms.group.ifPresent(groupName -> aider.addGroup(groupName));
secman.addAccount(broker, aider);
}
} catch (final PermissionDeniedException | EXistException e) {
throw new PackageException("Failed to create user: " + requestedPerms.user, e);
}
}
private enum QueryPurpose {
SETUP(" element"),
PREINSTALL(" element"),
POSTINSTALL(" element"),
UNDEPLOY("undeploy");
private final String purpose;
QueryPurpose(final String purpose) {
this.purpose = purpose;
}
public String getPurposeString() {
return purpose;
}
}
private Sequence runQuery(final DBBroker broker, final XmldbURI targetCollection, final Path tempDir,
final String fileName, final String pkgName, final QueryPurpose purpose)
throws PackageException, IOException, XPathException {
final Path xquery = tempDir.resolve(fileName);
if (!Files.isReadable(xquery)) {
LOG.warn("The XQuery resource specified in the " + purpose.getPurposeString() + " was not found for EXPath Package: '" + pkgName + "'");
return Sequence.EMPTY_SEQUENCE;
}
final XQuery xqs = broker.getBrokerPool().getXQueryService();
final XQueryContext ctx = new XQueryContext(broker.getBrokerPool());
ctx.declareVariable("dir", tempDir.toAbsolutePath().toString());
final Optional home = broker.getConfiguration().getExistHome();
if(home.isPresent()) {
ctx.declareVariable("home", home.get().toAbsolutePath().toString());
}
if (targetCollection != null) {
ctx.declareVariable("target", targetCollection.toString());
ctx.setModuleLoadPath(XmldbURI.EMBEDDED_SERVER_URI + targetCollection.toString());
} else
{ctx.declareVariable("target", Sequence.EMPTY_SEQUENCE);}
if (QueryPurpose.PREINSTALL == purpose) {
// when running pre-setup scripts, base path should point to directory
// because the target collection does not yet exist
ctx.setModuleLoadPath(tempDir.toAbsolutePath().toString());
}
CompiledXQuery compiled;
try {
compiled = xqs.compile(broker, ctx, new FileSource(xquery, false));
return xqs.execute(broker, compiled, null);
} catch (final PermissionDeniedException e) {
throw new PackageException(e.getMessage(), e);
}
}
/**
* Scan a directory and import all files and sub directories into the target
* collection.
*
* @param broker
* @param transaction
* @param directory
* @param target
*/
private List scanDirectory(final DBBroker broker, final Txn transaction, final Path directory, final XmldbURI target, final InMemoryNodeSet resources,
final boolean inRootDir, final boolean isResourcesDir, final Optional requestedPerms) {
return scanDirectory(broker, transaction, directory, target, resources, inRootDir, isResourcesDir, requestedPerms, new ArrayList<>());
}
private List scanDirectory(final DBBroker broker, final Txn transaction, final Path directory, final XmldbURI target, final InMemoryNodeSet resources,
final boolean inRootDir, final boolean isResourcesDir, final
Optional requestedPerms, final List errors) {
Collection collection = null;
try {
collection = broker.getOrCreateCollection(transaction, target);
setPermissions(broker, requestedPerms, true, null, collection.getPermissionsNoLock());
broker.saveCollection(transaction, collection);
} catch (final PermissionDeniedException | TriggerException | IOException e) {
LOG.warn(e);
errors.add(e.getMessage());
}
final boolean isResources = isResourcesDir || isResourceDir(target, resources);
// the root dir is not allowed to be a resources directory
if (!inRootDir && isResources) {
try {
storeBinaryResources(broker, transaction, directory, collection, requestedPerms, errors);
} catch (Exception e) {
LOG.error(e.getMessage(), e);
}
} else {
storeFiles(broker, transaction, directory, collection, inRootDir, requestedPerms, errors);
}
// scan sub directories
try(final Stream subDirs = Files.find(directory, 1, (path, attrs) -> (!path.equals(directory)) && attrs.isDirectory())) {
subDirs.forEach(path -> scanDirectory(broker, transaction, path, target.append(FileUtils.fileName(path)), resources, false,
isResources, requestedPerms, errors));
} catch(final IOException ioe) {
LOG.warn("Unable to scan sub-directories", ioe);
}
return errors;
}
private boolean isResourceDir(final XmldbURI target, final InMemoryNodeSet resources) {
// iterate here or pass into scandirectory directly or even save as class property???
for (final SequenceIterator i = resources.iterate(); i.hasNext(); ) {
final ElementImpl child = (ElementImpl) i.nextItem();
final String resourcePath = child.getAttribute(RESOURCES_PATH_ATTRIBUTE);
if (target.toString().endsWith(resourcePath)) {
return true;
}
}
return false;
}
/**
* Import all files in the given directory into the target collection
*
* @param broker
* @param transaction
* @param directory
* @param targetCollection
*/
private void storeFiles(final DBBroker broker, final Txn transaction, final Path directory, final Collection targetCollection, final boolean inRootDir,
final Optional requestedPerms, final List errors) {
List files;
try {
files = FileUtils.list(directory);
} catch(final IOException ioe) {
LOG.error(ioe);
errors.add(FileUtils.fileName(directory) + ": " + ioe.getMessage());
files = Collections.EMPTY_LIST;
}
final MimeTable mimeTab = MimeTable.getInstance();
for (final Path file : files) {
if (inRootDir && FileUtils.fileName(file).equals("repo.xml")) {
continue;
}
if (!Files.isDirectory(file)) {
MimeType mime = mimeTab.getContentTypeFor(FileUtils.fileName(file));
if (mime == null) {
mime = MimeType.BINARY_TYPE;
}
final XmldbURI name = XmldbURI.create(FileUtils.fileName(file));
try {
if (mime.isXMLType()) {
final InputSource is = new FileInputSource(file);
IndexInfo info = null;
try {
info = targetCollection.validateXMLResource(transaction, broker, name, is);
} catch (EXistException | PermissionDeniedException | LockException | SAXException | IOException e) {
//check for .html ending
if(mime.getName().equals(MimeType.HTML_TYPE.getName())){
//store it as binary resource
storeBinary(broker, transaction, targetCollection, file, mime, name, requestedPerms);
} else {
// could neither store as xml nor binary: give up and report failure in outer catch
throw new EXistException(FileUtils.fileName(file) + " cannot be stored");
}
}
if (info != null) {
info.getDocument().getMetadata().setMimeType(mime.getName());
final Permission permission = info.getDocument().getPermissions();
setPermissions(broker, requestedPerms, false, mime, permission);
targetCollection.store(transaction, broker, info, is);
}
} else {
final long size = Files.size(file);
try(final InputStream is = Files.newInputStream(file)) {
final BinaryDocument doc =
targetCollection.addBinaryResource(transaction, broker, name, is, mime.getName(), size);
final Permission permission = doc.getPermissions();
setPermissions(broker, requestedPerms, false, mime, permission);
doc.getMetadata().setMimeType(mime.getName());
broker.storeXMLResource(transaction, doc);
}
}
} catch (final SAXException | EXistException | PermissionDeniedException | LockException | IOException e) {
LOG.error(e.getMessage(), e);
errors.add(FileUtils.fileName(file) + ": " + e.getMessage());
}
}
}
}
private void storeBinary(final DBBroker broker, final Txn transaction, final Collection targetCollection, final Path file, final MimeType mime, final XmldbURI name, final Optional requestedPerms) throws
IOException, EXistException, PermissionDeniedException, LockException, TriggerException {
final long size = Files.size(file);
try (final InputStream is = Files.newInputStream(file)) {
final BinaryDocument doc =
targetCollection.addBinaryResource(transaction, broker, name, is, mime.getName(), size);
final Permission permission = doc.getPermissions();
setPermissions(broker, requestedPerms, false, mime, permission);
doc.getMetadata().setMimeType(mime.getName());
broker.storeXMLResource(transaction, doc);
}
}
private void storeBinaryResources(final DBBroker broker, final Txn transaction, final Path directory, final Collection targetCollection,
final Optional requestedPerms, final List errors) throws IOException, EXistException,
PermissionDeniedException, LockException, TriggerException {
try(DirectoryStream stream = Files.newDirectoryStream(directory)) {
for (final Path entry: stream) {
if (!Files.isDirectory(entry)) {
final XmldbURI name = XmldbURI.create(FileUtils.fileName(entry));
try {
storeBinary(broker, transaction, targetCollection, entry, MimeType.BINARY_TYPE, name, requestedPerms);
} catch (final Exception e) {
LOG.error(e.getMessage(), e);
errors.add(e.getMessage());
}
}
}
}
}
/**
* Set owner, group and permissions. For XQuery resources, always set the executable flag.
* @param mime
* @param permission
*/
private void setPermissions(final DBBroker broker, final Optional requestedPerms, final boolean isCollection, final MimeType mime, final Permission permission) throws PermissionDeniedException {
int mode = permission.getMode();
if (requestedPerms.isPresent()) {
final RequestedPerms perms = requestedPerms.get();
PermissionFactory.chown(broker, permission, Optional.of(perms.user), perms.group);
mode = perms.permissions.map(permStr -> {
try {
final UnixStylePermission other = new UnixStylePermission(broker.getBrokerPool().getSecurityManager());
other.setMode(permStr);
return other.getMode();
} catch (final PermissionDeniedException | SyntaxException e) {
LOG.warn("Unable to set permissions string: " + permStr + ". Falling back to default.");
return permission.getMode();
}
}).fold(l -> l, r -> r);
}
if (isCollection || (mime != null && mime.getName().equals(MimeType.XQUERY_TYPE.getName()))) {
mode = mode | 0111; //TODO(AR) Whoever did this - this is a really bad idea. You are circumventing the security of the system
}
PermissionFactory.chmod(broker, permission, Optional.of(mode), Optional.empty());
}
private Optional findElement(final NodeImpl root, final QName qname) throws XPathException {
final InMemoryNodeSet setupNodes = new InMemoryNodeSet();
root.selectDescendants(false, new NameTest(Type.ELEMENT, qname), setupNodes);
if (setupNodes.getItemCount() == 0) {
return Optional.empty();
}
return Optional.of((ElementImpl) setupNodes.itemAt(0));
}
private InMemoryNodeSet findElements(final NodeImpl root, final QName qname) throws XPathException {
final InMemoryNodeSet setupNodes = new InMemoryNodeSet();
root.selectDescendants(false, new NameTest(Type.ELEMENT, qname), setupNodes);
return setupNodes;
}
public Optional getNameFromDescriptor(final DBBroker broker, final XarSource xar) throws IOException, PackageException {
final Optional doc = getDescriptor(broker, xar);
return doc.map(DocumentImpl::getDocumentElement).map(root -> root.getAttribute("name"));
}
public Optional getDescriptor(final DBBroker broker, final XarSource xar) throws IOException, PackageException {
try(final JarInputStream jis = new JarInputStream(xar.newInputStream())) {
JarEntry entry;
while ((entry = jis.getNextJarEntry()) != null) {
if (!entry.isDirectory() && "expath-pkg.xml".equals(entry.getName())) {
try {
return Optional.of(DocUtils.parse(broker.getBrokerPool(), null, jis));
} catch (final XPathException e) {
throw new PackageException("Error while parsing expath-pkg.xml: " + e.getMessage(), e);
}
}
}
}
return Optional.empty();
}
/**
* Update repo.xml while copying it. For security reasons, make sure
* any default password is removed before uploading.
*/
private static class UpdatingDocumentReceiver extends DocumentBuilderReceiver {
private final String time;
private final Deque stack = new ArrayDeque<>();
public UpdatingDocumentReceiver(final MemTreeBuilder builder, final String time) {
super(builder, false);
this.time = time;
}
@Override
public void startElement(final QName qname, final AttrList attribs) {
stack.push(qname.getLocalPart());
AttrList newAttrs = attribs;
if (attribs != null && "permissions".equals(qname.getLocalPart())) {
newAttrs = new AttrList();
for (int i = 0; i < attribs.getLength(); i++) {
if (!"password". equals(attribs.getQName(i).getLocalPart())) {
newAttrs.addAttribute(attribs.getQName(i), attribs.getValue(i), attribs.getType(i));
}
}
}
if (!"deployed".equals(qname.getLocalPart())) {
super.startElement(qname, newAttrs);
}
}
@Override
public void startElement(final String namespaceURI, final String localName,
final String qName, final Attributes attrs) throws SAXException {
stack.push(localName);
if (!"deployed".equals(localName)) {
super.startElement(namespaceURI, localName, qName, attrs);
}
}
@Override
public void endElement(final QName qname) throws SAXException {
stack.pop();
if ("meta".equals(qname.getLocalPart())) {
addDeployTime();
}
if (!"deployed".equals(qname.getLocalPart())) {
super.endElement(qname);
}
}
@Override
public void endElement(final String uri, final String localName, final String qName) throws SAXException {
stack.pop();
if ("meta".equals(localName)) {
addDeployTime();
}
if (!"deployed".equals(localName)) {
super.endElement(uri, localName, qName);
}
}
@Override
public void attribute(final QName qname, final String value) throws SAXException {
final String current = stack.peek();
if (!("permissions".equals(current) && "password".equals(qname.getLocalPart()))) {
super.attribute(qname, value);
}
}
@Override
public void characters(final char[] ch, final int start, final int len) throws SAXException {
final String current = stack.peek();
if (!"deployed".equals(current)) {
super.characters(ch, start, len);
}
}
@Override
public void characters(final CharSequence seq) throws SAXException {
final String current = stack.peek();
if (!"deployed".equals(current)) {
super.characters(seq);
}
}
private void addDeployTime() throws SAXException {
super.startElement(DEPLOYED_ELEMENT, null);
super.characters(time);
super.endElement(DEPLOYED_ELEMENT);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy