• 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

60.75
/exist-core/src/main/java/org/exist/backup/SystemExport.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.backup;
50

51
import com.evolvedbinary.j8fu.function.FunctionE;
52
import org.apache.logging.log4j.LogManager;
53
import org.apache.logging.log4j.Logger;
54
import org.exist.Namespaces;
55
import org.exist.collections.Collection;
56
import org.exist.collections.MutableCollection;
57
import org.exist.dom.QName;
58
import org.exist.dom.persistent.*;
59
import org.exist.management.Agent;
60
import org.exist.management.AgentFactory;
61
import org.exist.numbering.NodeId;
62
import org.exist.security.ACLPermission;
63
import org.exist.security.Permission;
64
import org.exist.security.PermissionDeniedException;
65
import org.exist.security.internal.AccountImpl;
66
import org.exist.stax.ExtendedXMLStreamReader;
67
import org.exist.storage.DBBroker;
68
import org.exist.storage.DataBackup;
69
import org.exist.storage.NativeBroker;
70
import org.exist.storage.ProcessMonitor;
71
import org.exist.storage.btree.BTreeCallback;
72
import org.exist.storage.btree.Value;
73
import org.exist.storage.index.CollectionStore;
74
import org.exist.storage.io.VariableByteInput;
75
import org.exist.storage.serializers.ChainOfReceiversFactory;
76
import org.exist.storage.serializers.EXistOutputKeys;
77
import org.exist.storage.txn.Txn;
78
import org.exist.util.FileUtils;
79
import org.exist.util.LockException;
80
import org.exist.util.UTF8;
81
import org.exist.util.serializer.AttrList;
82
import org.exist.util.serializer.Receiver;
83
import org.exist.util.serializer.SAXSerializer;
84
import org.exist.util.serializer.SerializerPool;
85
import org.exist.xmldb.XmldbURI;
86
import org.exist.xquery.Expression;
87
import org.exist.xquery.TerminatedException;
88
import org.exist.xquery.XPathException;
89
import org.exist.xquery.util.URIUtils;
90
import org.exist.xquery.value.DateTimeValue;
91
import org.w3c.dom.DocumentType;
92
import org.w3c.dom.Node;
93
import org.w3c.dom.NodeList;
94
import org.xml.sax.Attributes;
95
import org.xml.sax.SAXException;
96
import org.xml.sax.helpers.AttributesImpl;
97
import org.xml.sax.helpers.DefaultHandler;
98
import org.xml.sax.helpers.NamespaceSupport;
99

100
import javax.xml.stream.XMLStreamException;
101
import javax.xml.stream.XMLStreamReader;
102
import javax.xml.transform.OutputKeys;
103
import java.io.*;
104
import java.nio.file.Files;
105
import java.nio.file.Path;
106
import java.nio.file.Paths;
107
import java.text.SimpleDateFormat;
108
import java.util.*;
109

110
import static java.nio.charset.StandardCharsets.UTF_8;
111

112

113
/**
114
 * Embedded database export tool class. Tries to export as much data as possible, even if parts of the collection hierarchy are corrupted or documents
115
 * are no longer readable. Features:
116
 *
117
 * <ul>
118
 * <li>Descendant collections will be exported properly even if their ancestor collection is corrupted.</li>
119
 * <li>Documents which are intact but belong to a destroyed collection will be stored into a special collection /db/__lost_and_found__.</li>
120
 * <li>Damaged documents are detected by ConsistencyCheck and are removed from the backup.</li>
121
 * <li>The format of the exported data is compatible with backups generated via the standard backup tool (Java admin client).</li>
122
 * </ul>
123
 *
124
 * The class should be used in combination with {@link ConsistencyCheck}. The error lists returned by ConsistencyCheck can be passed to {@link
125
 * #export(org.exist.collections.Collection, BackupWriter, java.util.Date, BackupDescriptor, java.util.List, org.exist.dom.persistent.MutableDocumentSet)}.
126
 */
127
public class SystemExport {
128
    public final static Logger LOG = LogManager.getLogger(SystemExport.class);
1✔
129

130
    private static final XmldbURI TEMP_COLLECTION = XmldbURI.createInternal(XmldbURI.TEMP_COLLECTION);
1✔
131
    private static final XmldbURI CONTENTS_URI = XmldbURI.createInternal("__contents__.xml");
1✔
132
    private static final XmldbURI LOST_URI = XmldbURI.createInternal("__lost_and_found__");
1✔
133

134
    public final static String CONFIGURATION_ELEMENT = "backup-filter";
135
    public final static String CONFIG_FILTERS = "backup.serialization.filters";
136

137
    private static final int currVersion = 1;
1✔
138

139
    private final SimpleDateFormat creationDateFormat = new SimpleDateFormat(DataBackup.DATE_FORMAT_PICTURE);
1✔
140

141
    private int collectionCount = -1;
1✔
142

143
    private final Properties defaultOutputProperties = new Properties();
1✔
144
    private final Properties contentsOutputProps = new Properties();
1✔
145

146
    private final DBBroker broker;
147
    private final Txn transaction;
148
    private StatusCallback callback = null;
1✔
149
    private boolean directAccess = false;
1✔
150
    private ProcessMonitor.Monitor monitor = null;
1✔
151
    private ChainOfReceiversFactory chainFactory;
152

153
    public SystemExport(final DBBroker broker, final Txn transaction, final StatusCallback callback, final ProcessMonitor.Monitor monitor,
1✔
154
            final boolean direct, final ChainOfReceiversFactory chainFactory) {
155
        this.broker = broker;
1✔
156
        this.transaction = transaction;
1✔
157
        this.callback = callback;
1✔
158
        this.monitor = monitor;
1✔
159
        this.directAccess = direct;
1✔
160
        this.chainFactory = chainFactory;
1✔
161

162
        defaultOutputProperties.setProperty(OutputKeys.INDENT, "no");
1✔
163
        defaultOutputProperties.setProperty(OutputKeys.ENCODING, UTF_8.name());
1✔
164
        defaultOutputProperties.setProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
1✔
165
        defaultOutputProperties.setProperty(EXistOutputKeys.EXPAND_XINCLUDES, "no");
1✔
166
        defaultOutputProperties.setProperty(EXistOutputKeys.PROCESS_XSL_PI, "no");
1✔
167

168
        contentsOutputProps.setProperty(OutputKeys.INDENT, "yes");
1✔
169
    }
1✔
170

171
    @SuppressWarnings("unchecked")
172
    public SystemExport(final DBBroker broker, final Txn transaction, final StatusCallback callback,
173
            final ProcessMonitor.Monitor monitor, final boolean direct) {
174
        this(broker, transaction, callback, monitor, direct, null);
1✔
175

176
        final List<String> list = (List<String>) broker.getConfiguration().getProperty(CONFIG_FILTERS);
1✔
177
        if (list != null) {
1✔
178
            chainFactory = new ChainOfReceiversFactory(list);
1✔
179
        }
180
    }
1✔
181

182
    public Path export(final String targetDir, final boolean incremental, final boolean zip, final List<ErrorReport> errorList) {
183
        return (export(targetDir, incremental, -1, zip, errorList));
1✔
184
    }
185

186

187
    /**
188
     * Export the contents of the database, trying to preserve as much data as possible. To be effective, this method should be used in combination
189
     * with class {@link ConsistencyCheck}.
190
     *
191
     * @param targetDir   the output directory or file to which data will be written. Output will be written to a zip file if target ends with
192
     *                    .zip.
193
     * @param incremental DOCUMENT ME!
194
     * @param maxInc      DOCUMENT ME!
195
     * @param zip         DOCUMENT ME!
196
     * @param errorList   a list of {@link ErrorReport} objects as returned by methods in {@link ConsistencyCheck}.
197
     * @return DOCUMENT ME!
198
     */
199
    public Path export(final String targetDir, boolean incremental, final int maxInc, final boolean zip, final List<ErrorReport> errorList) {
200
        Path backupFile = null;
1✔
201

202
        try {
203
            final BackupDirectory directory = new BackupDirectory(targetDir);
1✔
204
            BackupDescriptor prevBackup = null;
1✔
205

206
            if (incremental) {
1!
207
                prevBackup = directory.lastBackupFile();
×
208
                LOG.info("Creating incremental backup. Prev backup: {}", (prevBackup == null) ? "none" : prevBackup.getSymbolicPath());
×
209
            }
210

211
            final Properties properties = new Properties();
1✔
212
            int seqNr = 1;
1✔
213

214
            if (incremental) {
1!
215
                properties.setProperty(BackupDescriptor.PREVIOUS_PROP_NAME, (prevBackup == null) ? "" : prevBackup.getName());
×
216

217
                if (prevBackup != null) {
×
218
                    final Properties prevProp = prevBackup.getProperties();
×
219

220
                    if (prevProp != null) {
×
221
                        final String seqNrStr = prevProp.getProperty(BackupDescriptor.NUMBER_IN_SEQUENCE_PROP_NAME, "1");
×
222

223
                        try {
224
                            seqNr = Integer.parseInt(seqNrStr);
×
225

226
                            if (seqNr == maxInc) {
×
227
                                seqNr = 1;
×
228
                                incremental = false;
×
229
                                prevBackup = null;
×
230
                            } else {
×
231
                                ++seqNr;
×
232
                            }
233
                        } catch (final NumberFormatException e) {
×
234
                            LOG.warn("Bad sequence number in backup descriptor: {}", prevBackup.getName());
×
235
                        }
236
                    }
237
                }
238
            }
239
            properties.setProperty(BackupDescriptor.NUMBER_IN_SEQUENCE_PROP_NAME, Integer.toString(seqNr));
1✔
240
            properties.setProperty(BackupDescriptor.INCREMENTAL_PROP_NAME, incremental ? "yes" : "no");
1!
241

242
            try {
243
                properties.setProperty(BackupDescriptor.DATE_PROP_NAME, new DateTimeValue(new Date()).getStringValue());
1✔
244
            } catch (final XPathException e) {
1✔
245
            }
246

247
            backupFile = directory.createBackup(incremental && (prevBackup != null), zip);
1!
248

249
            final FunctionE<Path, BackupWriter, IOException> fWriter;
250
            if (zip) {
1✔
251
                fWriter = p -> new ZipWriter(p, XmldbURI.ROOT_COLLECTION);
1✔
252
            } else {
1✔
253
                fWriter = FileSystemWriter::new;
1✔
254
            }
255

256
            try (final BackupWriter output = fWriter.apply(backupFile)) {
1✔
257
                output.setProperties(properties);
1✔
258

259
//            File repoBackup = RepoBackup.backup(broker);
260
//            output.addToRoot(RepoBackup.REPO_ARCHIVE, repoBackup);
261
//            FileUtils.forceDelete(repoBackup);
262

263
                final Date date = (prevBackup == null) ? null : prevBackup.getDate();
1!
264
                final CollectionCallback cb = new CollectionCallback(output, date, prevBackup, errorList, true);
1✔
265
                broker.getCollectionsFailsafe(transaction, cb);
1✔
266

267
                exportOrphans(output, cb.getDocs(), errorList);
1✔
268
            }
269

270
            return backupFile;
1✔
271

272
        } catch (final IOException e) {
×
273
            reportError("A write error occurred while exporting data: '" + e.getMessage() + "'. Aborting export.", e);
×
274
            return null;
×
275
        } catch (final TerminatedException e) {
×
276
            if (backupFile != null) {
×
277
                FileUtils.deleteQuietly(backupFile);
×
278
            }
279
            return null;
×
280
        }
281
    }
282

283

284
    private void reportError(final String message, final Throwable e) {
285
        if (callback != null) {
×
286
            callback.error("EXPORT: " + message, e);
×
287
        }
288

289
        LOG.error("EXPORT: {}", message, e);
×
290
    }
×
291

292

293
    private static boolean isDamaged(final DocumentImpl doc, final List<ErrorReport> errorList) {
294
        if (errorList == null) {
1!
295
            return (false);
1✔
296
        }
297

298
        for (final org.exist.backup.ErrorReport report : errorList) {
×
299

300
            if ((report.getErrcode() == org.exist.backup.ErrorReport.RESOURCE_ACCESS_FAILED) && (((ErrorReport.ResourceError) report).getDocumentId() == doc.getDocId())) {
×
301
                return (true);
×
302
            }
303
        }
304
        return (false);
×
305
    }
306

307

308
    @SuppressWarnings("unused")
309
    private static boolean isDamaged(final Collection collection, final List<ErrorReport> errorList) {
310
        if (errorList == null) {
×
311
            return (false);
×
312
        }
313

314
        for (final ErrorReport report : errorList) {
×
315

316
            if ((report.getErrcode() == org.exist.backup.ErrorReport.CHILD_COLLECTION) && (((ErrorReport.CollectionError) report).getCollectionId() == collection.getId())) {
×
317
                return (true);
×
318
            }
319
        }
320
        return (false);
×
321
    }
322

323

324
    private static boolean isDamagedChild(final XmldbURI uri, final List<ErrorReport> errorList) {
325
        if (errorList == null) {
1!
326
            return (false);
1✔
327
        }
328

329
        for (final org.exist.backup.ErrorReport report : errorList) {
×
330

331
            if ((report.getErrcode() == org.exist.backup.ErrorReport.CHILD_COLLECTION) && ((org.exist.backup.ErrorReport.CollectionError) report).getCollectionURI().equalsInternal(uri)) {
×
332
                return (true);
×
333
            }
334
        }
335
        return (false);
×
336
    }
337

338

339
    /**
340
     * Scan all document records in collections.dbx and try to find orphaned documents whose parent collection got destroyed or is damaged.
341
     *
342
     * @param output    the backup writer
343
     * @param docs      a document set containing all the documents which were exported regularily. the method will ignore those.
344
     * @param errorList a list of {@link org.exist.backup.ErrorReport} objects as returned by methods in {@link ConsistencyCheck}
345
     */
346
    private void exportOrphans(final BackupWriter output, final DocumentSet docs, final List<ErrorReport> errorList) throws IOException {
347
        output.newCollection("/db/__lost_and_found__");
1✔
348

349
        final SAXSerializer serializer = (SAXSerializer) SerializerPool.getInstance().borrowObject(SAXSerializer.class);
1✔
350
        try(final Writer contents = output.newContents()) {
1✔
351

352
            // serializer writes to __contents__.xml
353
            serializer.setOutput(contents, contentsOutputProps);
1✔
354

355
            serializer.startDocument();
1✔
356
            serializer.startPrefixMapping("", Namespaces.EXIST_NS);
1✔
357
            final AttributesImpl attr = new AttributesImpl();
1✔
358
            attr.addAttribute(Namespaces.EXIST_NS, "name", "name", "CDATA", "/db/__lost_and_found__");
1✔
359
            attr.addAttribute(Namespaces.EXIST_NS, "version", "version", "CDATA", String.valueOf(currVersion));
1✔
360
            attr.addAttribute(Namespaces.EXIST_NS, "owner", "owner", "CDATA", org.exist.security.SecurityManager.DBA_USER);
1✔
361
            attr.addAttribute(Namespaces.EXIST_NS, "group", "group", "CDATA", org.exist.security.SecurityManager.DBA_GROUP);
1✔
362
            attr.addAttribute(Namespaces.EXIST_NS, "mode", "mode", "CDATA", "0771");
1✔
363
            serializer.startElement(Namespaces.EXIST_NS, "collection", "collection", attr);
1✔
364

365
            final DocumentCallback docCb = new DocumentCallback(output, serializer, null, null, docs, true);
1✔
366
            broker.getResourcesFailsafe(transaction, docCb, directAccess);
1✔
367

368
            serializer.endElement(Namespaces.EXIST_NS, "collection", "collection");
1✔
369
            serializer.endPrefixMapping("");
1✔
370
            serializer.endDocument();
1✔
371
        } catch (final Exception e) {
×
372
            e.printStackTrace();
×
373

374
            if (callback != null) {
×
375
                callback.error(e.getMessage(), e);
×
376
            }
377
        } finally {
378
            SerializerPool.getInstance().returnObject(serializer);
1✔
379
            output.closeCollection();
1✔
380
        }
381
    }
1✔
382

383

384
    /**
385
     * Export a collection. Write out the collection metadata and save the resources stored in the collection.
386
     *
387
     * @param current    the collection
388
     * @param output     the output writer
389
     * @param date
390
     * @param prevBackup DOCUMENT ME!
391
     * @param errorList  a list of {@link org.exist.backup.ErrorReport} objects as returned by methods in {@link org.exist.backup.ConsistencyCheck}
392
     * @param docs       a document set to keep track of all written documents.
393
     * @throws IOException
394
     * @throws SAXException
395
     * @throws TerminatedException DOCUMENT ME!
396
     */
397
    private void export(final Collection current, final BackupWriter output, final Date date, final BackupDescriptor prevBackup, final List<ErrorReport> errorList, final MutableDocumentSet docs) throws IOException, SAXException, TerminatedException, PermissionDeniedException {
398
//        if( callback != null ) {
399
//            callback.startCollection( current.getURI().toString() );
400
//        }
401

402
        if ((monitor != null) && !monitor.proceed()) {
1!
403
            throw (new TerminatedException((Expression) null, "system export terminated by db"));
×
404
        }
405

406
//        if( !current.getURI().equalsInternal( XmldbURI.ROOT_COLLECTION_URI ) ) {
407
        output.newCollection(Backup.encode(URIUtils.urlDecodeUtf8(current.getURI())));
1✔
408
//        }
409

410
        final SAXSerializer serializer = (SAXSerializer) SerializerPool.getInstance().borrowObject(SAXSerializer.class);
1✔
411
        try {
412
            final Writer contents = output.newContents();
1✔
413

414
            // serializer writes to __contents__.xml
415
            serializer.setOutput(contents, contentsOutputProps);
1✔
416

417
            final Permission perm = current.getPermissionsNoLock();
1✔
418

419
            serializer.startDocument();
1✔
420
            serializer.startPrefixMapping("", Namespaces.EXIST_NS);
1✔
421
            final XmldbURI uri = current.getURI();
1✔
422
            final AttributesImpl attr = new AttributesImpl();
1✔
423
            attr.addAttribute(Namespaces.EXIST_NS, "name", "name", "CDATA", uri.toString());
1✔
424
            attr.addAttribute(Namespaces.EXIST_NS, "version", "version", "CDATA", String.valueOf(currVersion));
1✔
425
            Backup.writeUnixStylePermissionAttributes(attr, perm);
1✔
426
            try {
427
                attr.addAttribute(Namespaces.EXIST_NS, "created", "created", "CDATA", new DateTimeValue(new Date(current.getCreated())).getStringValue());
1✔
428
            } catch (final XPathException e) {
1✔
429
                e.printStackTrace();
×
430
            }
431

432
            serializer.startElement(Namespaces.EXIST_NS, "collection", "collection", attr);
1✔
433

434
            if (perm instanceof ACLPermission) {
1!
435
                Backup.writeACLPermission(serializer, (ACLPermission) perm);
1✔
436
            }
437

438
            final int docsCount = current.getDocumentCountNoLock(broker);
1✔
439
            int count = 0;
1✔
440

441
            for (final Iterator<DocumentImpl> i = current.iteratorNoLock(broker); i.hasNext(); count++) {
1✔
442
                final DocumentImpl doc = i.next();
1✔
443

444
                if (isDamaged(doc, errorList)) {
1!
445
                    reportError("Skipping damaged document " + doc.getFileURI(), null);
×
446
                    continue;
×
447
                }
448

449
                if (doc.getFileURI().equalsInternal(CONTENTS_URI) || doc.getFileURI().equalsInternal(LOST_URI)) {
1!
450
                    continue; // skip __contents__.xml documents
×
451
                }
452
                exportDocument(output, date, prevBackup, serializer, docsCount, count, doc);
1✔
453
                docs.add(doc, false);
1✔
454
            }
455

456
            for (final Iterator<XmldbURI> i = current.collectionIteratorNoLock(broker); i.hasNext(); ) {
1✔
457
                final XmldbURI childUri = i.next();
1✔
458

459
                if (childUri.equalsInternal(TEMP_COLLECTION)) {
1!
460
                    continue;
×
461
                }
462

463
                if (isDamagedChild(childUri, errorList)) {
1!
464
                    reportError("Skipping damaged child collection " + childUri, null);
×
465
                    continue;
×
466
                }
467
                attr.clear();
1✔
468
                attr.addAttribute(Namespaces.EXIST_NS, "name", "name", "CDATA", childUri.toString());
1✔
469
                attr.addAttribute(Namespaces.EXIST_NS, "filename", "filename", "CDATA", Backup.encode(URIUtils.urlDecodeUtf8(childUri.toString())));
1✔
470
                serializer.startElement(Namespaces.EXIST_NS, "subcollection", "subcollection", attr);
1✔
471
                serializer.endElement(Namespaces.EXIST_NS, "subcollection", "subcollection");
1✔
472
            }
473

474
            if (prevBackup != null) {
1!
475

476
                // Check which collections and resources have been deleted since
477
                // the
478
                // last backup
479
                final CheckDeletedHandler check = new CheckDeletedHandler(current, serializer);
×
480

481
                try {
482
                    prevBackup.parse(broker.getBrokerPool().getParserPool(), check);
×
483
                } catch (final Exception e) {
×
484
                    LOG.error("Caught exception while trying to parse previous backup descriptor: {}", prevBackup.getSymbolicPath(), e);
×
485
                }
486
            }
487

488
            // close <collection>
489
            serializer.endElement(Namespaces.EXIST_NS, "collection", "collection");
1✔
490
            serializer.endPrefixMapping("");
1✔
491
            serializer.endDocument();
1✔
492
            output.closeContents();
1✔
493
        } finally {
1✔
494
            SerializerPool.getInstance().returnObject(serializer);
1✔
495
//            if( !current.getURI().equalsInternal( XmldbURI.ROOT_COLLECTION_URI ) ) {
496
            output.closeCollection();
1✔
497
//            }
498
        }
499
    }
1✔
500

501

502
    private void exportDocument(final BackupWriter output, final Date date, final BackupDescriptor prevBackup, final SAXSerializer serializer, final int docsCount, final int count, final DocumentImpl doc) throws IOException, SAXException, TerminatedException {
503
        if (callback != null) {
1!
504
            callback.startDocument(doc.getFileURI().toString(), count, docsCount);
×
505
        }
506

507
        if ((monitor != null) && !monitor.proceed()) {
1!
508
            throw new TerminatedException((Expression) null, "system export terminated by db");
×
509
        }
510
        final boolean needsBackup = (prevBackup == null) || (date.getTime() < doc.getLastModified());
1!
511

512
        if (needsBackup) {
1!
513
            // Note: do not auto-close the output stream or the zip will be closed!
514
            try {
515
                final OutputStream os = output.newEntry(Backup.encode(URIUtils.urlDecodeUtf8(doc.getFileURI())));
1✔
516
                if (doc.getResourceType() == DocumentImpl.BINARY_FILE) {
1✔
517
                    broker.readBinaryResource((BinaryDocument) doc, os);
1✔
518
                } else {
1✔
519
                    final SAXSerializer contentSerializer = (SAXSerializer) SerializerPool.getInstance().borrowObject(SAXSerializer.class);
1✔
520
                    final Writer writer = new BufferedWriter(new OutputStreamWriter(os, UTF_8));
1✔
521
                    try {
522

523
                        // write resource to contentSerializer
524
                        contentSerializer.setOutput(writer, defaultOutputProperties);
1✔
525

526
                        final Receiver receiver;
527
                        if (chainFactory != null) {
1✔
528
                            chainFactory.getLast().setNextInChain(contentSerializer);
1✔
529
                            receiver = chainFactory.getFirst();
1✔
530
                        } else {
1✔
531
                            receiver = contentSerializer;
1✔
532
                        }
533

534
                        writeXML(doc, receiver);
1✔
535
                    } finally {
1✔
536
                        SerializerPool.getInstance().returnObject(contentSerializer);
1✔
537
                        writer.flush();
1✔
538
                    }
539
                }
540
            } catch (final Exception e) {
×
541
                reportError("A write error occurred while exporting document: '" + doc.getFileURI() + "'. Continuing with next document.", e);
×
542
                return;
×
543
            } finally {
544
                output.closeEntry();
1✔
545
            }
546
        }
547

548
        final Permission perms = doc.getPermissions();
1✔
549

550
        // store permissions
551
        final AttributesImpl attr = new AttributesImpl();
1✔
552
        attr.addAttribute(Namespaces.EXIST_NS, "type", "type", "CDATA", (doc.getResourceType() == DocumentImpl.BINARY_FILE) ? "BinaryResource" : "XMLResource");
1✔
553
        attr.addAttribute(Namespaces.EXIST_NS, "name", "name", "CDATA", doc.getFileURI().toString());
1✔
554
        attr.addAttribute(Namespaces.EXIST_NS, "skip", "skip", "CDATA", (needsBackup ? "no" : "yes"));
1!
555
        Backup.writeUnixStylePermissionAttributes(attr, perms);
1✔
556

557
        // be careful when accessing document metadata: it is stored in a
558
        // different place than the
559
        // main document info and could thus be damaged
560

561
        try {
562
            final String created = new DateTimeValue(new Date(doc.getCreated())).getStringValue();
1✔
563
            final String modified = new DateTimeValue(new Date(doc.getLastModified())).getStringValue();
1✔
564
            attr.addAttribute(Namespaces.EXIST_NS, "created", "created", "CDATA", created);
1✔
565
            attr.addAttribute(Namespaces.EXIST_NS, "modified", "modified", "CDATA", modified);
1✔
566
        } catch (final XPathException e) {
1✔
567
            LOG.warn(e.getMessage(), e);
×
568
        }
569

570
        attr.addAttribute(Namespaces.EXIST_NS, "filename", "filename", "CDATA", Backup.encode(URIUtils.urlDecodeUtf8(doc.getFileURI())));
1✔
571
        String mimeType = "application/xml";
1✔
572

573
        if (doc.getMimeType() != null) {
1!
574
            mimeType = Backup.encode(doc.getMimeType());
1✔
575
        }
576
        attr.addAttribute(Namespaces.EXIST_NS, "mimetype", "mimetype", "CDATA", mimeType);
1✔
577

578
//output by serializer
579
//        if( ( doc.getResourceType() == DocumentImpl.XML_FILE ) && ( metadata != null ) && ( doc.getDoctype() != null ) ) {
580
//
581
//            if( doc.getDoctype().getName() != null ) {
582
//                attr.addAttribute( Namespaces.EXIST_NS, "namedoctype", "namedoctype", "CDATA", doc.getDoctype().getName() );
583
//            }
584
//
585
//            if( doc.getDoctype().getPublicId() != null ) {
586
//                attr.addAttribute( Namespaces.EXIST_NS, "publicid", "publicid", "CDATA", doc.getDoctype().getPublicId() );
587
//            }
588
//
589
//            if( doc.getDoctype().getSystemId() != null ) {
590
//                attr.addAttribute( Namespaces.EXIST_NS, "systemid", "systemid", "CDATA", doc.getDoctype().getSystemId() );
591
//            }
592
//        }
593

594
        serializer.startElement(Namespaces.EXIST_NS, "resource", "resource", attr);
1✔
595
        if (perms instanceof ACLPermission) {
1!
596
            Backup.writeACLPermission(serializer, (ACLPermission) perms);
1✔
597
        }
598

599
        serializer.endElement(Namespaces.EXIST_NS, "resource", "resource");
1✔
600
    }
1✔
601

602

603
    /**
604
     * Serialize a document to XML, based on {@link XMLStreamReader}.
605
     *
606
     * @param doc      the document to serialize
607
     * @param receiver the output handler
608
     */
609
    private void writeXML(final DocumentImpl doc, final Receiver receiver) {
610
        try {
611
            char[] ch;
612
            int nsdecls;
613
            final NamespaceSupport nsSupport = new NamespaceSupport();
1✔
614
            final NodeList children = doc.getChildNodes();
1✔
615

616
            final DocumentType docType = doc.getDoctype();
1✔
617
            if (docType != null) {
1✔
618
                receiver.documentType(docType.getName(), docType.getPublicId(), docType.getSystemId());
1✔
619
            }
620

621
            for (int i = 0; i < children.getLength(); i++) {
1✔
622
                final StoredNode child = (StoredNode) children.item(i);
1✔
623

624
                final int thisLevel = child.getNodeId().getTreeLevel();
1✔
625
                final int childLevel = child.getNodeType() == Node.ELEMENT_NODE ? thisLevel + 1 : thisLevel;
1✔
626

627
                final XMLStreamReader reader = broker.getXMLStreamReader(child, false);
1✔
628

629
                while (reader.hasNext()) {
1✔
630
                    final int status = reader.next();
1✔
631

632
                    switch (status) {
1!
633

634
                        case XMLStreamReader.START_DOCUMENT:
635
                        case XMLStreamReader.END_DOCUMENT:
636
                            break;
×
637

638
                        case XMLStreamReader.START_ELEMENT:
639
                            nsdecls = reader.getNamespaceCount();
1✔
640
                            for (int ni = 0; ni < nsdecls; ni++) {
1✔
641
                                receiver.startPrefixMapping(reader.getNamespacePrefix(ni), reader.getNamespaceURI(ni));
1✔
642
                            }
643

644
                            final int attrCount = reader.getAttributeCount();
1✔
645
                            final AttrList attribs = new AttrList(attrCount);
1✔
646
                            for (int j = 0; j < attrCount; j++) {
1✔
647
                                final QName qn = new QName(reader.getAttributeLocalName(j), reader.getAttributeNamespace(j), reader.getAttributePrefix(j));
1✔
648
                                attribs.addAttribute(qn, reader.getAttributeValue(j));
1✔
649
                            }
650
                            receiver.startElement(new QName(reader.getLocalName(), reader.getNamespaceURI(), reader.getPrefix()), attribs);
1✔
651
                            break;
1✔
652

653
                        case XMLStreamReader.END_ELEMENT:
654
                            receiver.endElement(new QName(reader.getLocalName(), reader.getNamespaceURI(), reader.getPrefix()));
1✔
655
                            nsdecls = reader.getNamespaceCount();
1✔
656
                            for (int ni = 0; ni < nsdecls; ni++) {
1✔
657
                                receiver.endPrefixMapping(reader.getNamespacePrefix(ni));
1✔
658
                            }
659

660
                            final NodeId otherId = (NodeId) reader.getProperty(ExtendedXMLStreamReader.PROPERTY_NODE_ID);
1✔
661
                            final int otherLevel = otherId.getTreeLevel();
1✔
662
                            if (childLevel != thisLevel && otherLevel == thisLevel) {
1!
663
                                // finished `this` element...
664
                                break;  // exit-while
1✔
665
                            }
666

667
                            break;
668

669
                        case XMLStreamReader.CHARACTERS:
670
                            receiver.characters(reader.getText());
1✔
671
                            break;
1✔
672

673
                        case XMLStreamReader.CDATA:
674
                            ch = reader.getTextCharacters();
×
675
                            receiver.cdataSection(ch, 0, ch.length);
×
676
                            break;
×
677

678
                        case XMLStreamReader.COMMENT:
679
                            ch = reader.getTextCharacters();
1✔
680
                            receiver.comment(ch, 0, ch.length);
1✔
681
                            break;
1✔
682

683
                        case XMLStreamReader.PROCESSING_INSTRUCTION:
684
                            receiver.processingInstruction(reader.getPITarget(), reader.getPIData());
×
685
                            break;
686
                    }
687

688
                    if (child.getNodeType() == Node.COMMENT_NODE || child.getNodeType() == Node.PROCESSING_INSTRUCTION_NODE) {
1!
689
                        break;
×
690
                    }
691
                }
692
                nsSupport.reset();
1✔
693
            }
694
        } catch (final IOException | SAXException | XMLStreamException e) {
1✔
695
            e.printStackTrace();
×
696
        }
697
    }
1✔
698

699
    public static Path getUniqueFile(final String base, final String extension, final String dir) {
700
        final SimpleDateFormat creationDateFormat = new SimpleDateFormat(DataBackup.DATE_FORMAT_PICTURE);
1✔
701
        final String filename = base + '-' + creationDateFormat.format(Calendar.getInstance().getTime());
1✔
702
        Path file = Paths.get(dir, filename + extension);
1✔
703
        int version = 0;
1✔
704

705
        while (Files.exists(file)) {
1!
706
            file = Paths.get(dir, filename + '_' + version++ + extension);
×
707
        }
708
        return file;
1✔
709
    }
710

711

712
    public int getCollectionCount() throws TerminatedException {
713
        if (collectionCount == -1) {
1✔
714
            AccountImpl.getSecurityProperties().enableCheckPasswords(false);
1✔
715

716
            try {
717
                final CollectionCallback cb = new CollectionCallback(null, null, null, null, false);
1✔
718
                broker.getCollectionsFailsafe(transaction, cb);
1✔
719
                collectionCount = cb.collectionCount;
1✔
720
            } finally {
1✔
721
                AccountImpl.getSecurityProperties().enableCheckPasswords(true);
1✔
722
            }
723
        }
724
        return (collectionCount);
1✔
725
    }
726

727
    public interface StatusCallback {
728
        void startCollection(String path) throws TerminatedException;
729

730

731
        void startDocument(String name, int current, int count) throws TerminatedException;
732

733

734
        void error(String message, Throwable exception);
735
    }
736

737
    private class CollectionCallback implements BTreeCallback {
738
        private final BackupWriter writer;
739
        private final BackupDescriptor prevBackup;
740
        private final Date date;
741
        private final List<ErrorReport> errors;
742
        private final MutableDocumentSet docs = new DefaultDocumentSet();
1✔
743
        private int collectionCount = 0;
1✔
744
        private final boolean exportCollection;
745
        private int lastPercentage = -1;
1✔
746
        private final Agent jmxAgent = AgentFactory.getInstance();
1✔
747

748
        private CollectionCallback(final BackupWriter writer, final Date date, final BackupDescriptor prevBackup, final List<ErrorReport> errorList, final boolean exportCollection) {
1✔
749
            this.writer = writer;
1✔
750
            this.errors = errorList;
1✔
751
            this.date = date;
1✔
752
            this.prevBackup = prevBackup;
1✔
753
            this.exportCollection = exportCollection;
1✔
754
        }
1✔
755

756
        public boolean indexInfo(final Value value, final long pointer) throws TerminatedException {
757
            String uri = null;
1✔
758

759
            try {
760
                collectionCount++;
1✔
761

762
                if (exportCollection) {
1✔
763
                    final CollectionStore store = (CollectionStore) ((NativeBroker) broker).getStorage(NativeBroker.COLLECTIONS_DBX_ID);
1✔
764
                    uri = UTF8.decode(value.data(), value.start() + CollectionStore.CollectionKey.OFFSET_VALUE, value.getLength() - CollectionStore.CollectionKey.OFFSET_VALUE).toString();
1✔
765

766
                    if (CollectionStore.NEXT_COLLECTION_ID_KEY.equals(uri) || CollectionStore.NEXT_DOC_ID_KEY.equals(uri) || CollectionStore.FREE_COLLECTION_ID_KEY.equals(uri) || CollectionStore.FREE_DOC_ID_KEY.equals(uri)) {
1!
767
                        return (true);
1✔
768
                    }
769

770
                    if (callback != null) {
1!
771
                        callback.startCollection(uri);
×
772
                    }
773

774
                    final VariableByteInput istream = store.getAsStream(pointer);
1✔
775
                    final Collection collection = MutableCollection.load(broker, XmldbURI.createInternal(uri), istream);
1✔
776

777
                    BackupDescriptor bd = null;
1✔
778

779
                    if (prevBackup != null) {
1!
780
                        bd = prevBackup.getBackupDescriptor(uri);
×
781
                    }
782
                    final int percentage = 100 * (collectionCount + 1) / (getCollectionCount() + 1);
1✔
783

784
                    if ((jmxAgent != null) && (percentage != lastPercentage)) {
1!
785
                        lastPercentage = percentage;
1✔
786
                        jmxAgent.updateStatus(broker.getBrokerPool(), percentage);
1✔
787
                    }
788
                    export(collection, writer, date, bd, errors, docs);
1✔
789
                }
790
            } catch (final TerminatedException e) {
1✔
791
                reportError("Terminating system export upon request", e);
×
792

793
                // rethrow
794
                throw (e);
×
795
            } catch (final Exception e) {
×
796
                reportError("Caught exception while scanning collections: " + uri, e);
×
797
            }
798
            return (true);
1✔
799
        }
800

801

802
        public DocumentSet getDocs() {
803
            return (docs);
1✔
804
        }
805
    }
806

807

808
    private class DocumentCallback implements BTreeCallback {
809
        private final DocumentSet exportedDocs;
810
        private Set<String> writtenDocs = null;
1✔
811
        private final SAXSerializer serializer;
812
        private final BackupWriter output;
813
        private final Date date;
814
        private final BackupDescriptor prevBackup;
815

816
        private DocumentCallback(final BackupWriter output, final SAXSerializer serializer, final Date date, final BackupDescriptor prevBackup, final DocumentSet exportedDocs, final boolean checkNames) {
1✔
817
            this.exportedDocs = exportedDocs;
1✔
818
            this.serializer = serializer;
1✔
819
            this.output = output;
1✔
820
            this.date = date;
1✔
821
            this.prevBackup = prevBackup;
1✔
822

823
            if (checkNames) {
1!
824
                writtenDocs = new TreeSet<>();
1✔
825
            }
826
        }
1✔
827

828
        public boolean indexInfo(final Value key, final long pointer) throws TerminatedException {
829
            final CollectionStore store = (CollectionStore) ((NativeBroker) broker).getStorage(NativeBroker.COLLECTIONS_DBX_ID);
1✔
830
            final int docId = CollectionStore.DocumentKey.getDocumentId(key);
1✔
831

832
            if (!exportedDocs.contains(docId)) {
1!
833

834
                try {
835
                    final byte type = key.data()[key.start() + Collection.LENGTH_COLLECTION_ID + DocumentImpl.LENGTH_DOCUMENT_TYPE];
×
836
                    final VariableByteInput istream = store.getAsStream(pointer);
×
837
                    DocumentImpl doc = null;
×
838

839
                    if (type == DocumentImpl.BINARY_FILE) {
×
840
                        doc = BinaryDocument.read(broker.getBrokerPool(), istream);
×
841
                    } else {
×
842
                        doc = DocumentImpl.read(broker.getBrokerPool(), istream);
×
843
                    }
844
                    reportError("Found an orphaned document: " + doc.getFileURI().toString(), null);
×
845

846
                    if (writtenDocs != null) {
×
847
                        int count = 1;
×
848
                        String fileURI = doc.getFileURI().toString();
×
849
                        final String origURI = fileURI;
×
850

851
                        while (writtenDocs.contains(fileURI)) {
×
852
                            fileURI = origURI + "." + count++;
×
853
                        }
854
                        doc.setFileURI(XmldbURI.createInternal(fileURI));
×
855
                        writtenDocs.add(fileURI);
×
856
                    }
857
                    exportDocument(output, date, prevBackup, serializer, 0, 0, doc);
×
858
                } catch (final Exception e) {
×
859
                    reportError("Caught an exception while scanning documents: " + e.getMessage(), e);
×
860
                }
861
            }
862
            return (true);
1✔
863
        }
864
    }
865

866

867
    private class CheckDeletedHandler extends DefaultHandler {
868
        private final Collection collection;
869
        private final SAXSerializer serializer;
870

871
        private CheckDeletedHandler(final Collection collection, final SAXSerializer serializer) {
×
872
            this.collection = collection;
×
873
            this.serializer = serializer;
×
874
        }
×
875

876
        @Override
877
        public void startElement(final String uri, final String localName, final String qName, final Attributes attributes) throws SAXException {
878
            if (uri.equals(Namespaces.EXIST_NS)) {
×
879

880
                try {
881
                    if ("subcollection".equals(localName)) {
×
882
                        String name = attributes.getValue("filename");
×
883

884
                        if (name == null) {
×
885
                            name = attributes.getValue("name");
×
886
                        }
887

888
                        if (!collection.hasChildCollection(broker, XmldbURI.create(name))) {
×
889
                            final AttributesImpl attr = new AttributesImpl();
×
890
                            attr.addAttribute(Namespaces.EXIST_NS, "name", "name", "CDATA", name);
×
891
                            attr.addAttribute(Namespaces.EXIST_NS, "type", "type", "CDATA", "collection");
×
892
                            serializer.startElement(Namespaces.EXIST_NS, "deleted", "deleted", attr);
×
893
                            serializer.endElement(Namespaces.EXIST_NS, "deleted", "deleted");
×
894
                        }
895
                    } else if ("resource".equals(localName)) {
×
896
                        final String name = attributes.getValue("name");
×
897

898
                        if (!collection.hasDocument(broker, XmldbURI.create(name))) {
×
899
                            final AttributesImpl attr = new AttributesImpl();
×
900
                            attr.addAttribute(Namespaces.EXIST_NS, "name", "name", "CDATA", name);
×
901
                            attr.addAttribute(Namespaces.EXIST_NS, "type", "type", "CDATA", "resource");
×
902
                            serializer.startElement(Namespaces.EXIST_NS, "deleted", "deleted", attr);
×
903
                            serializer.endElement(Namespaces.EXIST_NS, "deleted", "deleted");
×
904
                        }
905
                    }
906
                } catch (final LockException | PermissionDeniedException e) {
×
907
                    throw new SAXException("Unable to process :" + qName + ": " + e.getMessage(), e);
×
908
                }
909
            }
910
        }
×
911
    }
912
}
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