• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

evolvedbinary / elemental / 982

29 Apr 2025 08:34PM UTC coverage: 56.409% (+0.007%) from 56.402%
982

push

circleci

adamretter
[feature] Improve README.md badges

28451 of 55847 branches covered (50.94%)

Branch coverage included in aggregate %.

77468 of 131924 relevant lines covered (58.72%)

0.59 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

37.98
/exist-core/src/main/java/org/exist/repo/Deployment.java
1
/*
2
 * Elemental
3
 * Copyright (C) 2024, Evolved Binary Ltd
4
 *
5
 * admin@evolvedbinary.com
6
 * https://www.evolvedbinary.com | https://www.elemental.xyz
7
 *
8
 * Use of this software is governed by the Business Source License 1.1
9
 * included in the LICENSE file and at www.mariadb.com/bsl11.
10
 *
11
 * Change Date: 2028-04-27
12
 *
13
 * On the date above, in accordance with the Business Source License, use
14
 * of this software will be governed by the Apache License, Version 2.0.
15
 *
16
 * Additional Use Grant: Production use of the Licensed Work for a permitted
17
 * purpose. A Permitted Purpose is any purpose other than a Competing Use.
18
 * A Competing Use means making the Software available to others in a commercial
19
 * product or service that: substitutes for the Software; substitutes for any
20
 * other product or service we offer using the Software that exists as of the
21
 * date we make the Software available; or offers the same or substantially
22
 * similar functionality as the Software.
23
 *
24
 * NOTE: Parts of this file contain code from 'The eXist-db Authors'.
25
 *       The original license header is included below.
26
 *
27
 * =====================================================================
28
 *
29
 * eXist-db Open Source Native XML Database
30
 * Copyright (C) 2001 The eXist-db Authors
31
 *
32
 * info@exist-db.org
33
 * http://www.exist-db.org
34
 *
35
 * This library is free software; you can redistribute it and/or
36
 * modify it under the terms of the GNU Lesser General Public
37
 * License as published by the Free Software Foundation; either
38
 * version 2.1 of the License, or (at your option) any later version.
39
 *
40
 * This library is distributed in the hope that it will be useful,
41
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
42
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
43
 * Lesser General Public License for more details.
44
 *
45
 * You should have received a copy of the GNU Lesser General Public
46
 * License along with this library; if not, write to the Free Software
47
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
48
 */
49
package org.exist.repo;
50

51
import com.evolvedbinary.j8fu.Either;
52
import org.apache.logging.log4j.LogManager;
53
import org.apache.logging.log4j.Logger;
54
import org.exist.EXistException;
55
import org.exist.SystemProperties;
56
import org.exist.collections.Collection;
57
import org.exist.collections.triggers.TriggerException;
58
import org.exist.dom.QName;
59
import org.exist.dom.memtree.*;
60
import org.exist.security.*;
61
import org.exist.security.internal.aider.GroupAider;
62
import org.exist.security.internal.aider.UserAider;
63
import org.exist.source.FileSource;
64
import org.exist.storage.DBBroker;
65
import org.exist.storage.txn.Txn;
66
import org.exist.util.*;
67
import org.exist.util.serializer.AttrList;
68
import org.exist.xmldb.XmldbURI;
69
import org.exist.xquery.*;
70
import org.exist.xquery.util.DocUtils;
71
import org.exist.xquery.value.DateTimeValue;
72
import org.exist.xquery.value.Sequence;
73
import org.exist.xquery.value.SequenceIterator;
74
import org.exist.xquery.value.Type;
75
import org.expath.pkg.repo.Package;
76
import org.expath.pkg.repo.*;
77
import org.expath.pkg.repo.deps.DependencyVersion;
78
import org.expath.pkg.repo.tui.BatchUserInteraction;
79
import org.w3c.dom.Element;
80
import org.xml.sax.Attributes;
81
import org.xml.sax.InputSource;
82
import org.xml.sax.SAXException;
83

84
import javax.annotation.Nullable;
85
import java.io.BufferedInputStream;
86
import java.io.IOException;
87
import java.io.InputStream;
88
import java.nio.file.DirectoryStream;
89
import java.nio.file.Files;
90
import java.nio.file.Path;
91
import java.util.*;
92
import java.util.jar.JarEntry;
93
import java.util.jar.JarInputStream;
94
import java.util.stream.Stream;
95

96
import static org.exist.util.StringUtil.notNullOrEmptyOptional;
97
import static org.exist.util.StringUtil.nullIfEmpty;
98

99
/**
100
 * Deploy a .xar package into the database using the information provided
101
 * in expath-pkg.xml and repo.xml.
102
 */
103
public class Deployment {
1✔
104

105
    public final static String PROPERTY_APP_ROOT = "repo.root-collection";
106

107
    private final static Logger LOG = LogManager.getLogger(Deployment.class);
1✔
108

109
    public final static String PROCESSOR_NAME = "http://elemental.xyz";
110
    public final static String EXIST_PROCESSOR_NAME = "http://exist-db.org";
111

112
    private final static String REPO_NAMESPACE = "http://exist-db.org/xquery/repo";
113
    private final static String PKG_NAMESPACE = "http://expath.org/ns/pkg";
114

115
    private final static QName SETUP_ELEMENT = new QName("setup", REPO_NAMESPACE);
1✔
116
    private static final QName PRE_SETUP_ELEMENT = new QName("prepare", REPO_NAMESPACE);
1✔
117
    private static final QName POST_SETUP_ELEMENT = new QName("finish", REPO_NAMESPACE);
1✔
118
    private static final QName TARGET_COLL_ELEMENT = new QName("target", REPO_NAMESPACE);
1✔
119
    private static final QName PERMISSIONS_ELEMENT = new QName("permissions", REPO_NAMESPACE);
1✔
120
    private static final QName CLEANUP_ELEMENT = new QName("cleanup", REPO_NAMESPACE);
1✔
121
    private static final QName DEPLOYED_ELEMENT = new QName("deployed", REPO_NAMESPACE);
1✔
122
    private static final QName DEPENDENCY_ELEMENT = new QName("dependency", PKG_NAMESPACE);
1✔
123
    private static final QName RESOURCES_ELEMENT = new QName("resources", REPO_NAMESPACE);
1✔
124
    private static final String RESOURCES_PATH_ATTRIBUTE = "path";
1✔
125

126
    private static class RequestedPerms {
127
        final String user;
128
        final String password;
129
        final Optional<String> group;
130
        final Either<Integer, String> permissions;
131

132
        private RequestedPerms(final String user, final String password, final Optional<String> group, final Either<Integer, String> permissions) {
×
133
            this.user = user;
×
134
            this.password = password;
×
135
            this.group = group;
×
136
            this.permissions = permissions;
×
137
        }
×
138
    }
139

140
//    private Optional<RequestedPerms> requestedPerms = Optional.empty();
141

142
    protected Optional<Path> getPackageDir(final String pkgName, final Optional<ExistRepository> repo) throws PackageException {
143
        Optional<Path> packageDir = Optional.empty();
1✔
144

145
        if (repo.isPresent()) {
1!
146
            for (final Packages pp : repo.get().getParentRepo().listPackages()) {
1✔
147
                final org.expath.pkg.repo.Package pkg = pp.latest();
1✔
148
                if (pkg.getName().equals(pkgName)) {
1!
149
                    packageDir = Optional.of(getPackageDir(pkg));
1✔
150
                }
151
            }
152
        }
153
        return packageDir;
1✔
154
    }
155

156
    protected Path getPackageDir(final Package pkg) {
157
        final FileSystemStorage.FileSystemResolver resolver = (FileSystemStorage.FileSystemResolver) pkg.getResolver();
1✔
158
        return resolver.resolveResourceAsFile("");
1✔
159
    }
160

161
    protected Optional<org.expath.pkg.repo.Package> getPackage(final String pkgName, final Optional<ExistRepository> repo) throws PackageException {
162
        if (repo.isPresent()) {
1!
163
            for (final Packages pp : repo.get().getParentRepo().listPackages()) {
1!
164
                final org.expath.pkg.repo.Package pkg = pp.latest();
1✔
165
                if (pkg.getName().equals(pkgName)) {
1!
166
                    return Optional.ofNullable(pkg);
1✔
167
                }
168
            }
169
        }
170
        return Optional.empty();
×
171
    }
172

173
    protected DocumentImpl getRepoXML(final DBBroker broker, final Path packageDir) throws PackageException {
174
        // find and parse the repo.xml descriptor
175
        final Path repoFile = packageDir.resolve("repo.xml");
1✔
176
        if (!Files.isReadable(repoFile)) {
1!
177
            return null;
×
178
        }
179
        try(final InputStream is = new BufferedInputStream(Files.newInputStream(repoFile))) {
1✔
180
            return DocUtils.parse(broker.getBrokerPool(), null, is, null);
1✔
181
        } catch (final XPathException | IOException e) {
×
182
            throw new PackageException("Failed to parse repo.xml: " + e.getMessage(), e);
×
183
        }
184
    }
185

186
    public Optional<String> installAndDeploy(final DBBroker broker, final Txn transaction, final XarSource xar, final PackageLoader loader) throws PackageException, IOException {
187
        return installAndDeploy(broker, transaction, xar, loader, true);
1✔
188
    }
189

190
    /**
191
     * Install and deploy a give xar archive. Dependencies are installed from
192
     * the PackageLoader.
193
     *
194
     * @param broker the broker to use
195
     * @param transaction the transaction for this deployment task
196
     * @param xar the .xar file to install
197
     * @param loader package loader to use
198
     * @param enforceDeps when set to true, the method will throw an exception if a dependency could not be resolved
199
     *                    or an older version of the required dependency is installed and needs to be replaced.
200
     * @return the collection path to which the package was deployed or Optional.empty if not deployed
201
     * @throws PackageException if package installation failed
202
     * @throws IOException in case of an IO error
203
     */
204
    public Optional<String> installAndDeploy(final DBBroker broker, final Txn transaction, final XarSource xar, final PackageLoader loader, boolean enforceDeps) throws PackageException, IOException {
205
        final Optional<DocumentImpl> descriptor = getDescriptor(broker, xar);
1✔
206
        if(!descriptor.isPresent()) {
1!
207
            throw new PackageException("Missing descriptor from package: " + xar.getURI());
×
208
        }
209
        final DocumentImpl document = descriptor.get();
1✔
210

211
        final ElementImpl root = (ElementImpl) document.getDocumentElement();
1✔
212
        final String name = root.getAttribute("name");
1✔
213
        @Nullable final String pkgVersion = nullIfEmpty(root.getAttribute("version"));
1✔
214

215
        final Optional<ExistRepository> repo = broker.getBrokerPool().getExpathRepo();
1✔
216
            if (repo.isPresent()) {
1!
217
            final Packages packages = repo.get().getParentRepo().getPackages(name);
1✔
218

219
            if (packages != null && (!enforceDeps || (pkgVersion != null && pkgVersion.equals(packages.latest().getVersion())))) {
1!
220
                LOG.info("Application package {} already installed. Skipping.", name);
×
221
                final Package pkg = packages.latest();
×
222
                return Optional.of(getTargetCollection(broker, pkg, getPackageDir(pkg)));
×
223
            }
224

225
            InMemoryNodeSet deps;
226
            try {
227
                deps = findElements(root, DEPENDENCY_ELEMENT);
1✔
228
                for (final SequenceIterator i = deps.iterate(); i.hasNext(); ) {
1✔
229
                    final Element dependency = (Element) i.nextItem();
1✔
230
                    final String pkgName = dependency.getAttribute("package");
1✔
231
                    final String processor = dependency.getAttribute("processor");
1✔
232
                    @Nullable final String versionStr = nullIfEmpty(dependency.getAttribute("version"));
1✔
233
                    @Nullable final String semVer = nullIfEmpty(dependency.getAttribute("semver"));
1✔
234
                    @Nullable final String semVerMin = nullIfEmpty(dependency.getAttribute("semver-min"));
1✔
235
                    @Nullable final String semVerMax = nullIfEmpty(dependency.getAttribute("semver-max"));
1✔
236
                    PackageLoader.Version version = null;
1✔
237
                    if (semVer != null) {
1!
238
                        version = new PackageLoader.Version(semVer, true);
×
239
                    } else if (semVerMin != null || semVerMax != null) {
1!
240
                        version = new PackageLoader.Version(semVerMin, semVerMax);
1✔
241
                    } else if (pkgVersion != null) {
1!
242
                        version = new PackageLoader.Version(versionStr, false);
×
243
                    }
244

245
                    if (processor.equals(PROCESSOR_NAME) && version != null) {
1!
246
                        checkProcessorVersion(version);
×
247
                    } else if (processor.equals(EXIST_PROCESSOR_NAME) && version != null) {
1!
248
                        checkExistDbProcessorVersion(version);
1✔
249
                    } else if (!pkgName.isEmpty()) {
1!
250
                        LOG.info("Package {} depends on {}", name, pkgName);
×
251
                        boolean isInstalled = false;
×
252
                        if (repo.get().getParentRepo().getPackages(pkgName) != null) {
×
253
                            LOG.debug("Package {} already installed", pkgName);
×
254
                            Packages pkgs = repo.get().getParentRepo().getPackages(pkgName);
×
255
                            // check if installed package matches required version
256
                            if (pkgs != null) {
×
257
                                if (version != null) {
×
258
                                    Package latest = pkgs.latest();
×
259
                                    DependencyVersion depVersion = version.getDependencyVersion();
×
260
                                    if (depVersion.isCompatible(latest.getVersion())) {
×
261
                                        isInstalled = true;
×
262
                                    } else {
×
263
                                        LOG.debug("Package {} needs to be upgraded", pkgName);
×
264
                                        if (enforceDeps) {
×
265
                                            throw new PackageException("Package requires version " + version.toString() +
×
266
                                                " of package " + pkgName +
×
267
                                                ". Installed version is " + latest.getVersion() + ". Please upgrade!");
×
268
                                        }
269
                                    }
270
                                } else {
271
                                    isInstalled = true;
×
272
                                }
273
                                if (isInstalled) {
×
274
                                    LOG.debug("Package {} already installed", pkgName);
×
275
                                }
276
                            }
277
                        }
278
                        if (!isInstalled && loader != null) {
×
279
                            final XarSource depFile = loader.load(pkgName, version);
×
280
                            if (depFile != null) {
×
281
                                installAndDeploy(broker, transaction, depFile, loader);
×
282
                            } else {
×
283
                                if (enforceDeps) {
×
284
                                    LOG.warn("Missing dependency: package {} could not be resolved. This error is not fatal, but the package may not work as expected", pkgName);
×
285
                                } else {
×
286
                                    throw new PackageException("Missing dependency: package " + pkgName + " could not be resolved.");
×
287
                                }
288
                            }
289
                        }
290
                    }
291
                }
292
            } catch (final XPathException e) {
1✔
293
                throw new PackageException("Invalid descriptor found in " + xar.getURI());
×
294
            }
295

296
            // installing the xar into the expath repo
297
            LOG.info("Installing package {}", xar.getURI());
1✔
298
            final UserInteractionStrategy interact = new BatchUserInteraction();
1✔
299
            final org.expath.pkg.repo.Package pkg = repo.get().getParentRepo().installPackage(xar, true, interact);
1✔
300
            final ExistPkgInfo info = (ExistPkgInfo) pkg.getInfo("exist");
1✔
301
            if (info != null && !info.getJars().isEmpty()) {
1!
302
                ClasspathHelper.updateClasspath(broker.getBrokerPool(), pkg);
×
303
            }
304
            broker.getBrokerPool().getXQueryPool().clear();
1✔
305
            final String pkgName = pkg.getName();
1✔
306
            // signal status
307
            broker.getBrokerPool().reportStatus("Installing app: " + pkg.getAbbrev());
1✔
308
            repo.get().reportAction(ExistRepository.Action.INSTALL, pkg.getName());
1✔
309

310
            LOG.info("Deploying package {}", pkgName);
1✔
311
            return deploy(broker, transaction, pkgName, repo, null);
1✔
312
        }
313

314
            // Totally unnecessary to do the above if repo is unavailable.
315
            return Optional.empty();
×
316
    }
317

318
    private void checkProcessorVersion(final PackageLoader.Version version) throws PackageException {
319
        final String procVersion = SystemProperties.getInstance().getSystemProperty("product-version", "1.0.0");
×
320

321
        final DependencyVersion depVersion = version.getDependencyVersion();
×
322
        if (!depVersion.isCompatible(procVersion)) {
×
323
            throw new PackageException("Package requires Elemental version: " + version + ". " +
×
324
                "Installed version is " + procVersion);
×
325
        }
326
    }
×
327

328
    private void checkExistDbProcessorVersion(final PackageLoader.Version version) throws PackageException {
329
        final String procVersion = SystemProperties.getInstance().getSystemProperty("product-version", "1.0.0");
1✔
330
        final String eXistCompatibleProcVersion = SystemProperties.getInstance().getSystemProperty("exist-db-expath-pkg-compatible-version", "6.3.0");
1✔
331

332
        final DependencyVersion depVersion = version.getDependencyVersion();
1✔
333
        if (!depVersion.isCompatible(eXistCompatibleProcVersion)) {
1!
334
            throw new PackageException("Package requires eXist-db version: " + version + ". " +
×
335
                    "Installed version is Elemental: " + procVersion + ", with compatibility for eXist-db version: " + eXistCompatibleProcVersion);
×
336
        }
337
    }
1✔
338

339
    public Optional<String> undeploy(final DBBroker broker, final Txn transaction, final String pkgName, final Optional<ExistRepository> repo) throws PackageException {
340
        final Optional<Path> maybePackageDir = getPackageDir(pkgName, repo);
×
341
        if (!maybePackageDir.isPresent()) {
×
342
            // fails silently if package dir is not found?
343
            return Optional.empty();
×
344
        }
345

346
        final Path packageDir = maybePackageDir.get();
×
347
        final Optional<Package> pkg = getPackage(pkgName, repo);
×
348
        final DocumentImpl repoXML;
349
        try {
350
            repoXML = getRepoXML(broker, packageDir);
×
351
        } catch (PackageException e) {
×
352
            if (pkg.isPresent()) {
×
353
                uninstall(broker, transaction, pkg.get(), Optional.empty());
×
354
            }
355
            throw new PackageException("Failed to remove package from database " +
×
356
                    "due to error in repo.xml: " + e.getMessage(), e);
×
357
        }
358
        if (repoXML != null) {
×
359
            try {
360
                final Optional<ElementImpl> cleanup = findElement(repoXML, CLEANUP_ELEMENT);
×
361
                if(cleanup.isPresent()) {
×
362
                    runQuery(broker, null, packageDir, cleanup.get().getStringValue(), pkgName, QueryPurpose.UNDEPLOY);
×
363
                }
364

365
                final Optional<ElementImpl> target = findElement(repoXML, TARGET_COLL_ELEMENT);
×
366
                if (pkg.isPresent()) {
×
367
                    uninstall(broker, transaction, pkg.get(), target);
×
368
                }
369

370
                return target.map(e -> Optional.ofNullable(e.getStringValue())).orElseGet(() -> Optional.of(getTargetFallback(pkg.get()).getCollectionPath()));
×
371
            } catch (final XPathException | IOException e) {
×
372
                throw new PackageException("Error found while processing repo.xml: " + e.getMessage(), e);
×
373
            }
374
        } else {
375
            // we still may need to remove the copy of the package from /db/system/repo
376
            if (pkg.isPresent()) {
×
377
                        uninstall(broker, transaction, pkg.get(), Optional.empty());
×
378
                }
379
        }
380
        return Optional.empty();
×
381
    }
382

383
    public Optional<String> deploy(final DBBroker broker, final Txn transaction, final String pkgName, final Optional<ExistRepository> repo, final String userTarget) throws PackageException, IOException {
384
        final Optional<Path> maybePackageDir = getPackageDir(pkgName, repo);
1✔
385
        if (!maybePackageDir.isPresent()) {
1!
386
            throw new PackageException("Package not found: " + pkgName);
×
387
        }
388

389
        final Path packageDir = maybePackageDir.get();
1✔
390

391
        final DocumentImpl repoXML = getRepoXML(broker, packageDir);
1✔
392
        if (repoXML == null) {
1!
393
            return Optional.empty();
×
394
        }
395
        try {
396
            // if there's a <setup> element, run the query it points to
397
            final Optional<ElementImpl> setup = findElement(repoXML, SETUP_ELEMENT);
1✔
398
            final Optional<String> setupPath = setup.map(ElementImpl::getStringValue).filter(s -> !s.isEmpty());
1!
399

400
            if (setupPath.isPresent()) {
1!
401
                runQuery(broker, null, packageDir, setupPath.get(), pkgName, QueryPurpose.SETUP);
×
402
                return Optional.empty();
×
403
            } else {
404
                // otherwise create the target collection
405
                XmldbURI targetCollection = null;
1✔
406
                if (userTarget != null) {
1!
407
                    try {
408
                        targetCollection = XmldbURI.create(userTarget);
×
409
                    } catch (final IllegalArgumentException e) {
×
410
                        throw new PackageException("Bad collection URI: " + userTarget, e);
×
411
                    }
412
                } else {
413
                    final Optional<ElementImpl> target = findElement(repoXML, TARGET_COLL_ELEMENT);
1✔
414
                    final Optional<String> targetPath = target.map(ElementImpl::getStringValue).filter(s -> !s.isEmpty());
1!
415

416
                    if (targetPath.isPresent()) {
1✔
417
                        // determine target collection
418
                        try {
419
                            targetCollection = XmldbURI.create(getTargetCollection(broker, targetPath.get()));
1✔
420
                        } catch (final IllegalArgumentException e) {
1✔
421
                            throw new PackageException("Bad collection URI for <target> element: " + targetPath.get(), e);
×
422
                        }
423
                    } else {
424
                        LOG.warn("EXPath Package '{}' does not contain a <target> in its repo.xml, no files will be deployed to /apps", pkgName);
1✔
425
                    }
426
                }
427
                if (targetCollection == null) {
1✔
428
                    // no target means: package does not need to be deployed into database
429
                    // however, we need to preserve a copy for backup purposes
430
                    final Optional<Package> pkg = getPackage(pkgName, repo);
1✔
431
                            pkg.orElseThrow(() -> new XPathException((Expression) null, "expath repository is not available so the package was not stored."));
1✔
432
                    final String pkgColl = pkg.get().getAbbrev() + "-" + pkg.get().getVersion();
1✔
433
                    targetCollection = XmldbURI.SYSTEM.append("repo/" + pkgColl);
1✔
434
                }
435

436
                // extract the permissions (if any)
437
                final Optional<ElementImpl> permissions = findElement(repoXML, PERMISSIONS_ELEMENT);
1✔
438
                final Optional<RequestedPerms> requestedPerms = permissions.flatMap(elem -> {
1✔
439
                    final Optional<Either<Integer, String>> perms = notNullOrEmptyOptional(elem.getAttribute("mode")).flatMap(mode -> {
×
440
                        try {
441
                            return Optional.of(Either.Left(Integer.parseInt(mode, 8)));
×
442
                        } catch(final NumberFormatException e) {
×
443
                            if(mode.matches("^[rwx-]{9}")) {
×
444
                                return Optional.of(Either.Right(mode));
×
445
                            } else {
446
                                return Optional.empty();
×
447
                            }
448
                        }
449
                    });
450

451
                    return perms.map(p -> new RequestedPerms(
×
452
                        elem.getAttribute("user"),
×
453
                        elem.getAttribute("password"),
×
454
                        notNullOrEmptyOptional(elem.getAttribute("group")),
×
455
                        p
×
456
                    ));
×
457
                });
458

459
                //check that if there were permissions then we were able to parse them, a failure would be related to the mode string
460
                if(permissions.isPresent() && !requestedPerms.isPresent()) {
1!
461
                    final String mode = permissions.map(elem -> elem.getAttribute("mode")).orElse(null);
×
462
                    throw new PackageException("Bad format for mode attribute in <permissions>: " + mode);
×
463
                }
464

465
                // run the pre-setup query if present
466
                final Optional<ElementImpl> preSetup = findElement(repoXML, PRE_SETUP_ELEMENT);
1✔
467
                final Optional<String> preSetupPath = preSetup.map(ElementImpl::getStringValue).filter(s -> !s.isEmpty());
1!
468

469
                if(preSetupPath.isPresent()) {
1!
470
                    runQuery(broker, targetCollection, packageDir, preSetupPath.get(), pkgName, QueryPurpose.PREINSTALL);
×
471
                }
472

473
                // create the group specified in the permissions element if needed
474
                // create the user specified in the permissions element if needed; assign it to the specified group
475
                // TODO: if the user already exists, check and ensure the user is assigned to the specified group
476
                if(requestedPerms.isPresent()) {
1!
477
                    checkUserSettings(broker, requestedPerms.get());
×
478
                }
479

480
                final InMemoryNodeSet resources = findElements(repoXML,RESOURCES_ELEMENT);
1✔
481

482
                // store all package contents into database, using the user/group/mode in the permissions element. however:
483
                // 1. repo.xml is excluded for now, since it may contain the default user's password in the clear
484
                // 2. contents of directories identified in the path attribute of any <resource path=""/> element are stored as binary
485
                final List<String> errors = scanDirectory(broker, transaction, packageDir, targetCollection, resources, true, false,
1✔
486
                        requestedPerms);
1✔
487
                
488
                // store repo.xml, filtering out the default user's password
489
                storeRepoXML(broker, transaction, repoXML, targetCollection, requestedPerms);
1✔
490

491
                // run the post-setup query if present
492
                final Optional<ElementImpl> postSetup = findElement(repoXML, POST_SETUP_ELEMENT);
1✔
493
                final Optional<String> postSetupPath = postSetup.map(ElementImpl::getStringValue).filter(s -> !s.isEmpty());
1!
494

495
                if(postSetupPath.isPresent()) {
1!
496
                    runQuery(broker, targetCollection, packageDir, postSetupPath.get(), pkgName, QueryPurpose.POSTINSTALL);
×
497
                }
498

499
                // TODO: it should be safe to clean up the file system after a package
500
                // has been deployed. Might be enabled after 2.0
501
                //cleanup(pkgName, repo);
502

503
                if (!errors.isEmpty()) {
1!
504
                    throw new PackageException("Deployment incomplete, " + errors.size() + " issues found: " +
×
505
                            String.join("; ", errors));
×
506
                }
507
                return Optional.ofNullable(targetCollection.getCollectionPath());
1✔
508
            }
509
        } catch (final XPathException e) {
×
510
            throw new PackageException("Error found while processing repo.xml: " + e.getMessage(), e);
×
511
        }
512
    }
513

514
    /**
515
     * After deployment, clean up the package directory and remove all files which have been
516
     * stored into the db. They are not needed anymore. Only preserve the descriptors and the
517
     * contents directory.
518
     *
519
     * @param pkgName
520
     * @param repo
521
     * @throws PackageException
522
     */
523
    private void cleanup(final String pkgName, final Optional<ExistRepository> repo) throws PackageException {
524
        if (repo.isPresent()) {
×
525
            final Optional<Package> pkg = getPackage(pkgName, repo);
×
526
            final Optional<Path> maybePackageDir = pkg.map(this::getPackageDir);
×
527
            if (!maybePackageDir.isPresent()) {
×
528
                throw new PackageException("Cleanup: package dir for package " + pkgName + " not found");
×
529
            }
530

531
            final Path packageDir = maybePackageDir.get();
×
532
            final String abbrev = pkg.get().getAbbrev();
×
533

534
            try(final Stream<Path> filesToDelete = Files.find(packageDir, 1, (path, attrs) -> {
×
535
                    if(path.equals(packageDir)) {
×
536
                        return false;
×
537
                    }
538
                    final String name = FileUtils.fileName(path);
×
539
                    if (attrs.isDirectory()) {
×
540
                        return !(name.equals(abbrev) || name.equals("content"));
×
541
                    } else {
542
                        return !(name.equals("expath-pkg.xml") || name.equals("repo.xml") ||
×
543
                                "exist.xml".equals(name) || name.startsWith("icon"));
×
544
                    }
545
            })) {
546

547
                filesToDelete.forEach(path -> {
×
548
                    try {
549
                        Files.deleteIfExists(path);
×
550
                    } catch(final IOException ioe) {
×
551
                        LOG.warn("Cleanup: failed to delete file {} in package {}", path.toAbsolutePath().toString(), pkgName);
×
552
                    }
553
                });
×
554
            } catch (final IOException ioe) {
×
555
                LOG.warn("Cleanup: failed to delete files", ioe);
×
556
            }
557
        }
558
    }
×
559

560
    /**
561
     * Get the target collection for the given package, which resides in pkgDir.
562
     * Returns path to cached .xar for library packages.
563
     *
564
     * @param broker
565
     * @param pkg
566
     * @param pkgDir
567
     * @return
568
     * @throws PackageException
569
     */
570
    private String getTargetCollection(final DBBroker broker, final Package pkg, final Path pkgDir) throws PackageException {
571
        final DocumentImpl repoXML = getRepoXML(broker, pkgDir);
×
572
        if (repoXML != null) {
×
573
            try {
574
                final Optional<ElementImpl> target = findElement(repoXML, TARGET_COLL_ELEMENT);
×
575
                return target.map(ElementImpl::getStringValue).map(s -> getTargetCollection(broker, s)).map(XmldbURI::create).map(XmldbURI::getCollectionPath)
×
576
                        .orElseGet(() -> getTargetFallback(pkg).getCollectionPath());
×
577
            } catch (XPathException e) {
×
578
                throw new PackageException("Failed to determine target collection");
×
579
            }
580
        } else {
581
            return getTargetFallback(pkg).getCollectionPath();
×
582
        }
583
    }
584

585
    private XmldbURI getTargetFallback(final Package pkg) {
586
        final String pkgColl = pkg.getAbbrev() + "-" + pkg.getVersion();
×
587
        return XmldbURI.SYSTEM.append("repo/" + pkgColl);
×
588
    }
589

590
    private String getTargetCollection(final DBBroker broker, String targetFromRepo) {
591
        final String appRoot = (String) broker.getConfiguration().getProperty(PROPERTY_APP_ROOT);
1✔
592
        if (appRoot != null) {
1!
593
            if (targetFromRepo.startsWith("/db/")) {
1!
594
                targetFromRepo = targetFromRepo.substring(4);
×
595
            }
596
            return appRoot + targetFromRepo;
1✔
597
        }
598
        if (targetFromRepo.startsWith("/db")) {
×
599
            return targetFromRepo;
×
600
        } else {
601
            return "/db/" + targetFromRepo;
×
602
        }
603
    }
604

605
    /**
606
     * Delete the target collection of the package. If there's no repo.xml descriptor,
607
     * target will be null.
608
     *
609
     * @param pkg
610
     * @param target
611
     * @throws PackageException
612
     */
613
    private void uninstall(final DBBroker broker, final Txn transaction, final Package pkg, final Optional<ElementImpl> target)
614
            throws PackageException {
615
        // determine target collection
616
        final Optional<String> targetPath = target.map(ElementImpl::getStringValue).filter(s -> !s.isEmpty());
×
617
        final XmldbURI targetCollection = targetPath.map(s -> XmldbURI.create(getTargetCollection(broker, s)))
×
618
                .orElseGet(() -> getTargetFallback(pkg));
×
619

620
        try {
621
            Collection collection = broker.getOrCreateCollection(transaction, targetCollection);
×
622
            if (collection != null) {
×
623
                broker.removeCollection(transaction, collection);
×
624
            }
625
            if (target != null) {
×
626
                final XmldbURI configCollection = XmldbURI.CONFIG_COLLECTION_URI.append(targetCollection);
×
627
                collection = broker.getOrCreateCollection(transaction, configCollection);
×
628
                if (collection != null) {
×
629
                    broker.removeCollection(transaction, collection);
×
630
                }
631
            }
632
        } catch (final PermissionDeniedException | IOException | TriggerException e) {
×
633
            LOG.error("Exception occurred while removing package.", e);
×
634
        }
635
    }
×
636

637
    /**
638
     * Store repo.xml into the db. Adds the time of deployment to the descriptor.
639
     *
640
     * @param repoXML
641
     * @param targetCollection
642
     * @throws XPathException
643
     */
644
    private void storeRepoXML(final DBBroker broker, final Txn transaction, final DocumentImpl repoXML, final XmldbURI targetCollection, final Optional<RequestedPerms> requestedPerms)
645
            throws PackageException, XPathException {
646
        // Store repo.xml
647
        final DateTimeValue time = new DateTimeValue(new Date());
1✔
648
        final MemTreeBuilder builder = new MemTreeBuilder((Expression) null);
1✔
649
        builder.startDocument();
1✔
650
        final UpdatingDocumentReceiver receiver = new UpdatingDocumentReceiver(null, builder, time.getStringValue());
1✔
651
        try {
652
            repoXML.copyTo(broker, receiver);
1✔
653
        } catch (final SAXException e) {
1✔
654
            throw new PackageException("Error while updating repo.xml in-memory: " + e.getMessage(), e);
×
655
        }
656
        builder.endDocument();
1✔
657
        final DocumentImpl updatedXML = builder.getDocument();
1✔
658

659
        try {
660
            final Collection collection = broker.getOrCreateCollection(transaction, targetCollection);
1✔
661
            final XmldbURI name = XmldbURI.createInternal("repo.xml");
1✔
662

663
            final Permission permission = PermissionFactory.getDefaultResourcePermission(broker.getBrokerPool().getSecurityManager());
1✔
664
            setPermissions(broker, requestedPerms, false, MimeType.XML_TYPE, permission);
1✔
665

666
            collection.storeDocument(transaction, broker, name, updatedXML, MimeType.XML_TYPE, null, null, permission, null, null);
1✔
667

668
        } catch (final PermissionDeniedException | IOException | SAXException | LockException | EXistException e) {
1✔
669
            throw new PackageException("Error while storing updated repo.xml: " + e.getMessage(), e);
×
670
        }
671
    }
1✔
672

673
    private void checkUserSettings(final DBBroker broker, final RequestedPerms requestedPerms) throws PackageException {
674
        final org.exist.security.SecurityManager secman = broker.getBrokerPool().getSecurityManager();
×
675
        try {
676
            if (requestedPerms.group.filter(g -> !secman.hasGroup(g)).isPresent()) {
×
677
                secman.addGroup(broker, new GroupAider(requestedPerms.group.get()));
×
678
            }
679

680
            if (!secman.hasAccount(requestedPerms.user)) {
×
681
                final UserAider aider = new UserAider(requestedPerms.user);
×
682
                aider.setPassword(requestedPerms.password);
×
683
                requestedPerms.group.ifPresent(aider::addGroup);
×
684
                secman.addAccount(broker, aider);
×
685
            }
686
        } catch (final PermissionDeniedException | EXistException e) {
×
687
            throw new PackageException("Failed to create user: " + requestedPerms.user, e);
×
688
        }
689
    }
×
690

691
    private enum QueryPurpose {
×
692
        SETUP("<setup> element"),
×
693
        PREINSTALL("<prepare> element"),
×
694
        POSTINSTALL("<finish> element"),
×
695
        UNDEPLOY("undeploy");
×
696

697
        private final String purpose;
698

699
        QueryPurpose(final String purpose) {
×
700
            this.purpose = purpose;
×
701
        }
×
702

703
        public String getPurposeString() {
704
            return purpose;
×
705
        }
706
    }
707

708
    private Sequence runQuery(final DBBroker broker, final XmldbURI targetCollection, final Path tempDir,
709
            final String fileName, final String pkgName, final QueryPurpose purpose)
710
            throws PackageException, IOException, XPathException {
711
        final Path xquery = tempDir.resolve(fileName);
×
712
        if (!Files.isReadable(xquery)) {
×
713
            LOG.warn("The XQuery resource specified in the {} was not found for EXPath Package: '{}'", purpose.getPurposeString(), pkgName);
×
714
            return Sequence.EMPTY_SEQUENCE;
×
715
        }
716
        final XQuery xqs = broker.getBrokerPool().getXQueryService();
×
717
        final XQueryContext ctx = new XQueryContext(broker.getBrokerPool());
×
718
        ctx.declareVariable("dir", tempDir.toAbsolutePath().toString());
×
719
        final Optional<Path> home = broker.getConfiguration().getExistHome();
×
720
        if(home.isPresent()) {
×
721
            ctx.declareVariable("home", home.get().toAbsolutePath().toString());
×
722
        }
723

724
        if (targetCollection != null) {
×
725
            ctx.declareVariable("target", targetCollection.toString());
×
726
            ctx.setModuleLoadPath(XmldbURI.EMBEDDED_SERVER_URI + targetCollection.toString());
×
727
        } else
×
728
            {ctx.declareVariable("target", Sequence.EMPTY_SEQUENCE);}
×
729
        if (QueryPurpose.PREINSTALL == purpose) {
×
730
            // when running pre-setup scripts, base path should point to directory
731
            // because the target collection does not yet exist
732
            ctx.setModuleLoadPath(tempDir.toAbsolutePath().toString());
×
733
        }
734

735
        CompiledXQuery compiled;
736
        try {
737
            compiled = xqs.compile(ctx, new FileSource(xquery, false));
×
738
            return xqs.execute(broker, compiled, null);
×
739
        } catch (final PermissionDeniedException e) {
×
740
            throw new PackageException(e.getMessage(), e);
×
741
        } finally {
742
            ctx.runCleanupTasks();
×
743
        }
744
    }
745

746
    /**
747
     * Scan a directory and import all files and sub directories into the target
748
     * collection.
749
     *
750
     * @param broker
751
     * @param transaction
752
     * @param directory
753
     * @param target
754
     */
755
    private List<String> scanDirectory(final DBBroker broker, final Txn transaction, final Path directory, final XmldbURI target, final InMemoryNodeSet resources,
756
                               final boolean inRootDir, final boolean isResourcesDir, final Optional<RequestedPerms> requestedPerms) {
757
        return scanDirectory(broker, transaction, directory, target, resources, inRootDir, isResourcesDir, requestedPerms, new ArrayList<>());
1✔
758
    }
759

760
    private List<String> scanDirectory(final DBBroker broker, final Txn transaction, final Path directory, final XmldbURI target, final InMemoryNodeSet resources,
761
                                       final boolean inRootDir, final boolean isResourcesDir, final
762
                                       Optional<RequestedPerms> requestedPerms, final List<String> errors) {
763
        Collection collection = null;
1✔
764
        try {
765
            collection = broker.getOrCreateCollection(transaction, target);
1✔
766
            setPermissions(broker, requestedPerms, true, null, collection.getPermissionsNoLock());
1✔
767
            broker.saveCollection(transaction, collection);
1✔
768
        } catch (final PermissionDeniedException | TriggerException | IOException e) {
1✔
769
            LOG.warn(e);
×
770
            errors.add(e.getMessage());
×
771
        }
772

773
        final boolean isResources = isResourcesDir || isResourceDir(target, resources);
1!
774

775
        // the root dir is not allowed to be a resources directory
776
        if (!inRootDir && isResources) {
1!
777
            try {
778
                storeBinaryResources(broker, transaction, directory, collection, requestedPerms, errors);
×
779
            } catch (Exception e) {
×
780
                LOG.error(e.getMessage(), e); 
×
781
            }
782
        } else {
×
783
            storeFiles(broker, transaction, directory, collection, inRootDir, requestedPerms, errors);
1✔
784
        }
785

786
        // scan sub directories
787
        try(final Stream<Path> subDirs = Files.find(directory, 1, (path, attrs) -> (!path.equals(directory)) && attrs.isDirectory())) {
1✔
788
            subDirs.forEach(path -> scanDirectory(broker, transaction, path, target.append(FileUtils.fileName(path)), resources, false,
1✔
789
                    isResources, requestedPerms, errors));
1✔
790
        } catch(final IOException ioe) {
×
791
            LOG.warn("Unable to scan sub-directories", ioe);
×
792
        }
793
        return errors;
1✔
794
    }
795

796
    private boolean isResourceDir(final XmldbURI target, final InMemoryNodeSet resources) {
797
        // iterate here or pass into scandirectory directly or even save as class property???
798
        for (final SequenceIterator i = resources.iterate(); i.hasNext(); ) {
1!
799
            final ElementImpl child = (ElementImpl) i.nextItem();
×
800
            final String resourcePath = child.getAttribute(RESOURCES_PATH_ATTRIBUTE);
×
801
            if (target.toString().endsWith(resourcePath)) {
×
802
                return true;
×
803
            }
804
        }
805
        return false;
1✔
806
    }
807

808
    /**
809
     * Import all files in the given directory into the target collection
810
     *
811
     * @param broker
812
     * @param transaction
813
     * @param directory
814
     * @param targetCollection
815
     */
816
    private void storeFiles(final DBBroker broker, final Txn transaction, final Path directory, final Collection targetCollection, final boolean inRootDir,
817
            final Optional<RequestedPerms> requestedPerms, final List<String> errors) {
818
        List<Path> files;
819
        try {
820
            files = FileUtils.list(directory);
1✔
821
        } catch(final IOException ioe) {
1✔
822
            LOG.error(ioe);
×
823
            errors.add(FileUtils.fileName(directory) + ": " + ioe.getMessage());
×
824
            files = Collections.EMPTY_LIST;
×
825
        }
826

827
        final MimeTable mimeTab = MimeTable.getInstance();
1✔
828

829
        for (final Path file : files) {
1✔
830
            if (inRootDir && FileUtils.fileName(file).equals("repo.xml")) {
1✔
831
                continue;
1✔
832
            }
833
            if (!Files.isDirectory(file)) {
1✔
834
                MimeType mime = mimeTab.getContentTypeFor(FileUtils.fileName(file));
1✔
835
                if (mime == null) {
1!
836
                    mime = MimeType.BINARY_TYPE;
×
837
                }
838
                final XmldbURI name = XmldbURI.create(FileUtils.fileName(file));
1✔
839

840
                try {
841
                    final Permission permission = PermissionFactory.getDefaultResourcePermission(broker.getBrokerPool().getSecurityManager());
1✔
842
                    setPermissions(broker, requestedPerms, false, mime, permission);
1✔
843

844
                    try (final FileInputSource is = new FileInputSource(file)) {
1✔
845

846
                        broker.storeDocument(transaction, name, is, mime, null, null, permission, null, null, targetCollection);
1✔
847

848
                    } catch (final EXistException | PermissionDeniedException | LockException | SAXException | IOException e) {
×
849
                        //check for .html ending
850
                        if (mime.getName().equals(MimeType.HTML_TYPE.getName())) {
×
851
                            //store it as binary resource
852
                            storeBinary(broker, transaction, targetCollection, file, mime, name, permission);
×
853
                        } else {
×
854
                            // could neither store as xml nor binary: give up and report failure in outer catch
855
                            throw new EXistException(FileUtils.fileName(file) + " cannot be stored");
×
856
                        }
857
                    }
858
                } catch (final SAXException | EXistException | PermissionDeniedException | LockException | IOException e) {
×
859
                    LOG.error(e.getMessage(), e);
×
860
                    errors.add(FileUtils.fileName(file) + ": " + e.getMessage());
×
861
                }
862
            }
863
        }
864
    }
1✔
865

866
    private void storeBinary(final DBBroker broker, final Txn transaction, final Collection targetCollection, final Path file, final MimeType mime, final XmldbURI name, @Nullable final Permission permission) throws
867
            IOException, EXistException, PermissionDeniedException, LockException, SAXException {
868

869
        final InputSource is = new FileInputSource(file);
×
870
        broker.storeDocument(transaction, name, is, new MimeType(mime.getName(), MimeType.BINARY), null, null, permission, null, null, targetCollection);
×
871
    }
×
872

873
    private void storeBinaryResources(final DBBroker broker, final Txn transaction, final Path directory, final Collection targetCollection,
874
            final Optional<RequestedPerms> requestedPerms, final List<String> errors) throws IOException, EXistException,
875
            PermissionDeniedException, LockException, TriggerException {
876
        try (final DirectoryStream<Path> stream = Files.newDirectoryStream(directory)) {
×
877
            for (final Path entry: stream) {
×
878
                if (!Files.isDirectory(entry)) {
×
879
                    final XmldbURI name = XmldbURI.create(FileUtils.fileName(entry));
×
880
                    try {
881
                        final Permission permission = PermissionFactory.getDefaultResourcePermission(broker.getBrokerPool().getSecurityManager());
×
882
                        setPermissions(broker, requestedPerms, false, MimeType.BINARY_TYPE, permission);
×
883

884
                        storeBinary(broker, transaction, targetCollection, entry, MimeType.BINARY_TYPE, name, permission);
×
885
                    } catch (final Exception e) {
×
886
                        LOG.error(e.getMessage(), e);
×
887
                        errors.add(e.getMessage());
×
888
                    }
889
                }  
890
            }
891
        }
892
    }
×
893

894
    /**
895
     * Set owner, group and permissions. For XQuery resources, always set the executable flag.
896
     * @param mime
897
     * @param permission
898
     */
899
    private void setPermissions(final DBBroker broker, final Optional<RequestedPerms> requestedPerms, final boolean isCollection, final MimeType mime, final Permission permission) throws PermissionDeniedException {
900
        int mode = permission.getMode();
1✔
901
        if (requestedPerms.isPresent()) {
1!
902
            final RequestedPerms perms = requestedPerms.get();
×
903

904
            PermissionFactory.chown(broker, permission, Optional.of(perms.user), perms.group);
×
905

906
            mode = perms.permissions.map(permStr -> {
×
907
                try {
908
                    final UnixStylePermission other = new UnixStylePermission(broker.getBrokerPool().getSecurityManager());
×
909
                    other.setMode(permStr);
×
910
                    return other.getMode();
×
911
                } catch (final PermissionDeniedException | SyntaxException e) {
×
912
                    LOG.warn("Unable to set permissions string: {}. Falling back to default.", permStr);
×
913
                    return permission.getMode();
×
914
                }
915
            }).fold(l -> l, r -> r);
×
916
        }
917

918
        if (isCollection || (mime != null && mime.getName().equals(MimeType.XQUERY_TYPE.getName()))) {
1!
919
            mode = AbstractUnixStylePermission.safeSetExecutable(mode);
1✔
920
        }
921

922
        PermissionFactory.chmod(broker, permission, Optional.of(mode), Optional.empty());
1✔
923
    }
1✔
924

925
    private Optional<ElementImpl> findElement(final NodeImpl root, final QName qname) throws XPathException {
926
        final InMemoryNodeSet setupNodes = new InMemoryNodeSet();
1✔
927
        root.selectDescendants(false, new NameTest(Type.ELEMENT, qname), setupNodes);
1✔
928
        if (setupNodes.getItemCount() == 0) {
1✔
929
            return Optional.empty();
1✔
930
        }
931
        return Optional.of((ElementImpl) setupNodes.itemAt(0));
1✔
932
    }
933

934
    private InMemoryNodeSet findElements(final NodeImpl root, final QName qname) throws XPathException {
935
        final InMemoryNodeSet setupNodes = new InMemoryNodeSet();
1✔
936
        root.selectDescendants(false, new NameTest(Type.ELEMENT, qname), setupNodes);
1✔
937
        return setupNodes;
1✔
938
    }
939

940
    public Optional<String> getNameFromDescriptor(final DBBroker broker, final XarSource xar) throws IOException, PackageException {
941
        final Optional<DocumentImpl> doc = getDescriptor(broker, xar);
1✔
942
        return doc.map(DocumentImpl::getDocumentElement).map(root -> root.getAttribute("name"));
1✔
943
    }
944

945
    public Optional<DocumentImpl> getDescriptor(final DBBroker broker, final XarSource xar) throws IOException, PackageException {
946
        try(final JarInputStream jis = new JarInputStream(xar.newInputStream())) {
1✔
947
            JarEntry entry;
948
            while ((entry = jis.getNextJarEntry()) != null) {
1!
949
                if (!entry.isDirectory() && "expath-pkg.xml".equals(entry.getName())) {
1!
950
                    try {
951
                        return Optional.of(DocUtils.parse(broker.getBrokerPool(), null, jis, null));
1✔
952
                    } catch (final XPathException e) {
×
953
                        throw new PackageException("Error while parsing expath-pkg.xml: " + e.getMessage(), e);
×
954
                    }
955
                }
956
            }
957
        }
958
        return Optional.empty();
×
959
    }
960

961
    /**
962
     * Update repo.xml while copying it. For security reasons, make sure
963
     * any default password is removed before uploading.
964
     */
965
    private static class UpdatingDocumentReceiver extends DocumentBuilderReceiver {
966
        private final String time;
967
        private final Deque<String> stack = new ArrayDeque<>();
1✔
968

969
        public UpdatingDocumentReceiver(final MemTreeBuilder builder, final String time) {
970
            this(null, builder, time);
×
971
        }
×
972

973
        public UpdatingDocumentReceiver(final Expression expression, final MemTreeBuilder builder, final String time) {
974
            super(expression, builder, false);
1✔
975
            this.time = time;
1✔
976
        }
1✔
977

978
        @Override
979
        public void startElement(final QName qname, final AttrList attribs) {
980
            stack.push(qname.getLocalPart());
1✔
981
            AttrList newAttrs = attribs;
1✔
982
            if (attribs != null && "permissions".equals(qname.getLocalPart())) {
1!
983
                newAttrs = new AttrList();
×
984
                for (int i = 0; i < attribs.getLength(); i++) {
×
985
                    if (!"password". equals(attribs.getQName(i).getLocalPart())) {
×
986
                        newAttrs.addAttribute(attribs.getQName(i), attribs.getValue(i), attribs.getType(i));
×
987
                    }
988
                }
989
            }
990

991
            if (!"deployed".equals(qname.getLocalPart())) {
1!
992
                super.startElement(qname, newAttrs);
1✔
993
            }
994
        }
1✔
995

996
        @Override
997
        public void startElement(final String namespaceURI, final String localName,
998
                                 final String qName, final Attributes attrs) throws SAXException {
999
            stack.push(localName);
×
1000
            if (!"deployed".equals(localName)) {
×
1001
                super.startElement(namespaceURI, localName, qName, attrs);
×
1002
            }
1003
        }
×
1004

1005
        @Override
1006
        public void endElement(final QName qname) throws SAXException {
1007
            stack.pop();
1✔
1008
            if ("meta".equals(qname.getLocalPart())) {
1✔
1009
                addDeployTime();
1✔
1010
            }
1011
            if (!"deployed".equals(qname.getLocalPart())) {
1!
1012
                super.endElement(qname);
1✔
1013
            }
1014
        }
1✔
1015

1016
        @Override
1017
        public void endElement(final String uri, final String localName, final String qName) throws SAXException {
1018
            stack.pop();
×
1019
            if ("meta".equals(localName)) {
×
1020
                addDeployTime();
×
1021
            }
1022
            if (!"deployed".equals(localName)) {
×
1023
                super.endElement(uri, localName, qName);
×
1024
            }
1025
        }
×
1026

1027
        @Override
1028
        public void attribute(final QName qname, final String value) throws SAXException {
1029
            final String current = stack.peek();
×
1030
            if (!("permissions".equals(current) && "password".equals(qname.getLocalPart()))) {
×
1031
                super.attribute(qname, value);
×
1032
            }
1033
        }
×
1034

1035
        @Override
1036
        public void characters(final char[] ch, final int start, final int len) throws SAXException {
1037
            final String current = stack.peek();
1✔
1038
            if (!"deployed".equals(current)) {
1!
1039
                super.characters(ch, start, len);
1✔
1040
            }
1041
        }
1✔
1042

1043
        @Override
1044
        public void characters(final CharSequence seq) throws SAXException {
1045
            final String current = stack.peek();
×
1046
            if (!"deployed".equals(current)) {
×
1047
                super.characters(seq);
×
1048
            }
1049
        }
×
1050

1051
        private void addDeployTime() throws SAXException {
1052
            super.startElement(DEPLOYED_ELEMENT, null);
1✔
1053
            super.characters(time);
1✔
1054
            super.endElement(DEPLOYED_ELEMENT);
1✔
1055
        }
1✔
1056
    }
1057
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc