• 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

0.0
/exist-core/src/main/java/org/exist/backup/Main.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 org.exist.client.ClientFrame;
52
import org.exist.start.CompatibleJavaVersionCheck;
53
import org.exist.start.StartException;
54
import org.exist.util.ConfigurationHelper;
55
import org.exist.util.NamedThreadFactory;
56
import org.exist.util.OSUtil;
57
import org.exist.util.SystemExitCodes;
58
import org.exist.xmldb.*;
59
import org.xmldb.api.DatabaseManager;
60
import org.xmldb.api.base.Collection;
61
import org.xmldb.api.base.Database;
62
import org.xmldb.api.base.XMLDBException;
63
import se.softhouse.jargo.*;
64

65
import javax.swing.*;
66
import java.io.File;
67
import java.io.IOException;
68
import java.nio.file.Files;
69
import java.nio.file.Path;
70
import java.nio.file.Paths;
71
import java.util.Map;
72
import java.util.Optional;
73
import java.util.Properties;
74
import java.util.concurrent.*;
75
import java.util.prefs.Preferences;
76

77
import static org.exist.util.ArgumentUtil.getBool;
78
import static org.exist.util.ArgumentUtil.getOpt;
79
import static se.softhouse.jargo.Arguments.*;
80

81
/**
82
 * Main.java
83
 *
84
 * @author Wolfgang Meier
85
 */
86
public class Main {
×
87

88
    private static final String USER_PROP = "user";
89
    private static final String PASSWORD_PROP = "password";
90
    private static final String URI_PROP = "uri";
91
    private static final String CONFIGURATION_PROP = "configuration";
92
    private static final String DRIVER_PROP = "driver";
93
    private static final String CREATE_DATABASE_PROP = "create-database";
94
    private static final String BACKUP_DIR_PROP = "backup-dir";
95

96
    public static final String SSL_ENABLE = "ssl-enable";
97

98
    private static final String DEFAULT_USER = "admin";
99
    private static final String DEFAULT_PASSWORD = "";
100
    private static final String DEFAULT_URI = "xmldb:exist://";
101
    private static final String DEFAULT_DRIVER = "org.exist.xmldb.DatabaseImpl";
102
    private static final String DEFAULT_BACKUP_DIR = "backup";
103

104
    /* general arguments */
105
    private static final Argument<?> helpArg = helpArgument("-h", "--help");
×
106
    private static final Argument<Boolean> guiArg = optionArgument("-U", "--gui")
×
107
            .description("Start in GUI mode.")
×
108
            .defaultValue(false)
×
109
            .build();
×
110
    private static final Argument<Boolean> quietArg = optionArgument("-q", "--quiet")
×
111
            .description("Be quiet. Just print errors.")
×
112
            .defaultValue(false)
×
113
            .build();
×
114
    private static final Argument<Map<String, String>> optionArg = stringArgument("-o", "--option")
×
115
            .description("Specify extra options: property=value. For available properties see client.properties.")
×
116
            .asKeyValuesWithKeyParser(StringParsers.stringParser())
×
117
            .build();
×
118

119

120
    /* database connection arguments */
121
    private static final Argument<String> userArg = stringArgument("-u", "--user")
×
122
            .description("Set user.")
×
123
            .defaultValue(DEFAULT_USER)
×
124
            .build();
×
125
    private static final Argument<String> passwordArg = stringArgument("-p", "--password")
×
126
            .description("Set the password for connecting to the database.")
×
127
            .build();
×
128
    private static final Argument<String> dbaPasswordArg = stringArgument("-P", "--dba-password")
×
129
            .description("If the backup specifies a different password for the admin user, " +
×
130
                    "use this option to specify the new password. Otherwise you will get a permission denied.")
131
            .build();
×
132
    private static final Argument<Boolean> useSslArg = optionArgument("-S", "--use-ssl")
×
133
            .description("Use SSL by default for remote connections.")
×
134
            .defaultValue(false)
×
135
            .build();
×
136

137
    /* backup arguments */
138
    private static final Argument<String> backupCollectionArg = stringArgument("-b", "--backup")
×
139
            .description("Backup the specified collection.")
×
140
            .build();
×
141
    private static final Argument<File> backupOutputDirArg = fileArgument("-d", "--dir")
×
142
            .description("Specify the directory to use for backups.")
×
143
            .build();
×
144
    private static final Argument<Boolean> backupDeduplicateBlobs = booleanArgument("--deduplicate-blobs")
×
145
            .description("Deduplicate BLOBS in the backup.")
×
146
            .build();
×
147

148

149
    /* restore arguments */
150
    private static final Argument<File> restoreArg = fileArgument("-r", "--restore")
×
151
            .description("Restore from the specified 'full' backup file in ZIP format, or " +
×
152
                    "read the specified __contents__.xml file and restore the resources described in there.")
153
            .build();
×
154
    private static final Argument<Boolean> rebuildExpathRepoArg = optionArgument("-R", "--rebuild")
×
155
            .description("Rebuild the EXpath app repository after restore.")
×
156
            .defaultValue(false)
×
157
            .build();
×
158
    private static final Argument<Boolean> overwriteAppsArg = optionArgument("-a", "--overwrite-apps")
×
159
            .description("Overwrite newer applications installed in the database.")
×
160
            .defaultValue(false)
×
161
            .build();
×
162

163
    private static Properties loadProperties() {
164
        try {
165
            final Properties properties = ConfigurationHelper.loadProperties("backup.properties", Main.class);
×
166
            if (properties != null) {
×
167
                return properties;
×
168
            }
169

170
            System.err.println("WARN - Unable to find backup.properties");
×
171

172
        } catch (final IOException e) {
×
173
            System.err.println("WARN - Unable to load backup.properties: " + e.getMessage());
×
174
        }
175

176
        // return new empty properties
177
        return new Properties();
×
178
    }
179

180
    /**
181
     * Constructor for Main.
182
     *
183
     * @param arguments parsed command line arguments
184
     */
185
    public static void process(final ParsedArguments arguments) {
186
        final Properties properties = loadProperties();
×
187
        final Preferences preferences = Preferences.userNodeForPackage(Main.class);
×
188

189
        final boolean guiMode = getBool(arguments, guiArg);
×
190
        final boolean quiet = getBool(arguments, quietArg);
×
191
        Optional.ofNullable(arguments.get(optionArg)).ifPresent(options -> options.forEach(properties::setProperty));
×
192

193
        properties.setProperty(USER_PROP, arguments.get(userArg));
×
194
        final String optionPass = arguments.get(passwordArg);
×
195
        properties.setProperty(PASSWORD_PROP, optionPass);
×
196
        final Optional<String> optionDbaPass = getOpt(arguments, dbaPasswordArg);
×
197
        final boolean useSsl = getBool(arguments, useSslArg);
×
198
        if (useSsl) {
×
199
            properties.setProperty(SSL_ENABLE, "TRUE");
×
200
        }
201

202
        final Optional<String> backupCollection = getOpt(arguments, backupCollectionArg);
×
203
        getOpt(arguments, backupOutputDirArg).ifPresent(backupOutputDir -> properties.setProperty(BACKUP_DIR_PROP, backupOutputDir.getAbsolutePath()));
×
204

205
        final Optional<Path> restorePath = getOpt(arguments, restoreArg).map(File::toPath);
×
206
        final boolean rebuildRepo = getBool(arguments, rebuildExpathRepoArg);
×
207
        final boolean overwriteApps = getBool(arguments, overwriteAppsArg);
×
208

209
        boolean deduplicateBlobs = getBool(arguments, backupDeduplicateBlobs);
×
210

211
        // initialize driver
212
        final Database database;
213

214
        try {
215
            final Class<?> cl = Class.forName(properties.getProperty(DRIVER_PROP, DEFAULT_DRIVER));
×
216
            database = (Database) cl.newInstance();
×
217
            database.setProperty(CREATE_DATABASE_PROP, "true");
×
218
            database.setProperty(SSL_ENABLE, properties.getProperty(SSL_ENABLE, "FALSE"));
×
219
            
220
            if (properties.containsKey(CONFIGURATION_PROP)) {
×
221
                database.setProperty(CONFIGURATION_PROP, properties.getProperty(CONFIGURATION_PROP));
×
222
            }
223
            DatabaseManager.registerDatabase(database);
×
224
        } catch (final ClassNotFoundException | InstantiationException | XMLDBException | IllegalAccessException e) {
×
225
            reportError(e);
×
226
            return;
×
227
        }
228

229
        // process
230
        if (backupCollection.isPresent()) {
×
231
            String collection = backupCollection.get();
×
232
            if (collection.isEmpty()) {
×
233
                if (guiMode) {
×
234
                    final CreateBackupDialog dialog = new CreateBackupDialog(properties.getProperty(URI_PROP, DEFAULT_URI), properties.getProperty(USER_PROP, DEFAULT_USER), properties.getProperty(PASSWORD_PROP, DEFAULT_PASSWORD), Paths.get(preferences.get("directory.backup", System.getProperty("user.dir"))));
×
235

236
                    if (JOptionPane.showOptionDialog(null, dialog, "Create Backup", JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, null, null, null) == JOptionPane.YES_OPTION) {
×
237
                        collection = dialog.getCollection();
×
238
                        deduplicateBlobs = dialog.getDeduplicateBlobs();
×
239
                        properties.setProperty(BACKUP_DIR_PROP, dialog.getBackupTarget());
×
240
                    }
241
                } else {
×
242
                    collection = XmldbURI.ROOT_COLLECTION;
×
243
                }
244
            }
245

246
            if (!collection.isEmpty()) {
×
247
                try {
248
                    final Backup backup = new Backup(
×
249
                            properties.getProperty(USER_PROP, DEFAULT_USER),
×
250
                            properties.getProperty(PASSWORD_PROP, DEFAULT_PASSWORD),
×
251
                            Paths.get(properties.getProperty(BACKUP_DIR_PROP, DEFAULT_BACKUP_DIR)),
×
252
                            XmldbURI.xmldbUriFor(properties.getProperty(URI_PROP, DEFAULT_URI) + collection),
×
253
                            properties,
×
254
                            deduplicateBlobs
×
255
                    );
256
                    backup.backup(guiMode, null);
×
257
                } catch (final Exception e) {
×
258
                    reportError(e);
×
259
                }
260
            }
261
        }
262

263
        if (restorePath.isPresent()) {
×
264
            Path path = restorePath.get();
×
265
            if (!Files.exists(path) && guiMode) {
×
266
                final JFileChooser chooser = new JFileChooser();
×
267
                chooser.setMultiSelectionEnabled(false);
×
268
                chooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
×
269

270
                if (chooser.showDialog(null, "Select backup file for restore") == JFileChooser.APPROVE_OPTION) {
×
271
                    path = chooser.getSelectedFile().toPath();
×
272
                }
273
            }
274

275
            if (Files.exists(path)) {
×
276
                final String username = properties.getProperty(USER_PROP, DEFAULT_USER);
×
277
                final String uri = properties.getProperty(URI_PROP, DEFAULT_URI);
×
278

279
                try {
280
                    final XmldbURI dbUri;
281
                    if(!uri.endsWith(XmldbURI.ROOT_COLLECTION)) {
×
282
                        dbUri = XmldbURI.xmldbUriFor(uri + XmldbURI.ROOT_COLLECTION);
×
283
                    } else {
×
284
                        dbUri = XmldbURI.xmldbUriFor(uri);
×
285
                    }
286

287
                    if (guiMode) {
×
288
                        restoreWithGui(username, optionPass, optionDbaPass, path, dbUri, overwriteApps);
×
289
                    } else {
×
290
                        restoreWithoutGui(username, optionPass, optionDbaPass, path, dbUri,
×
291
                                rebuildRepo, quiet, overwriteApps);
×
292
                    }
293
                } catch (final Exception e) {
×
294
                    reportError(e);
×
295
                }
296
            }
297
        }
298

299
        try {
300
            String uri = properties.getProperty(URI_PROP, XmldbURI.EMBEDDED_SERVER_URI_PREFIX);
×
301
            if (!(uri.contains(XmldbURI.ROOT_COLLECTION) || uri.endsWith(XmldbURI.ROOT_COLLECTION))) {
×
302
                uri += XmldbURI.ROOT_COLLECTION;
×
303
            }
304

305
            final Collection root = DatabaseManager.getCollection(uri, properties.getProperty(USER_PROP, DEFAULT_USER), optionDbaPass.orElse(optionPass));
×
306
            shutdown(root);
×
307
        } catch (final Exception e) {
×
308
            reportError(e);
×
309
        }
310
        System.exit(SystemExitCodes.OK_EXIT_CODE);
×
311
    }
×
312

313
    private static void restoreWithoutGui(final String username, final String password,
314
            final Optional<String> dbaPassword, final Path f, final XmldbURI uri, final boolean rebuildRepo,
315
            final boolean quiet, final boolean overwriteApps) {
316
        final AggregatingConsoleRestoreServiceTaskListener listener = new AggregatingConsoleRestoreServiceTaskListener(quiet);
×
317
        try {
318
            final Collection collection = DatabaseManager.getCollection(uri.toString(), username, password);
×
319
            final EXistRestoreService service = collection.getService(EXistRestoreService.class);
×
320
            service.restore(f.toAbsolutePath().toString(), dbaPassword.orElse(null), listener, overwriteApps);
×
321

322
        } catch (final XMLDBException e) {
×
323
            listener.error(e.getMessage());
×
324
        }
325

326
        if (listener.hasProblems()) {
×
327
            System.err.println(listener.getAllProblems());
×
328
        }
329

330
        if (rebuildRepo) {
×
331
            System.out.println("Rebuilding application repository ...");
×
332
            System.out.println("URI: " + uri);
×
333
            try {
334
                final Collection root = DatabaseManager.getCollection(uri.toString(), username, dbaPassword.orElse(password));
×
335
                if (root != null) {
×
336
                    ClientFrame.repairRepository(root);
×
337
                    System.out.println("Application repository rebuilt successfully.");
×
338
                } else {
×
339
                    System.err.println("Failed to retrieve root collection: " + uri);
×
340
                }
341
            } catch (final XMLDBException e) {
×
342
                reportError(e);
×
343
                System.err.println("Rebuilding application repository failed!");
×
344
            }
345
        } else {
×
346
            System.out.println("\nIf you restored collections inside /db/apps, you may want\n" +
×
347
                    "to rebuild the application repository. To do so, run the following query\n" +
348
                    "as admin:\n\n" +
349
                    "import module namespace repair=\"http://exist-db.org/xquery/repo/repair\"\n" +
350
                    "at \"resource:org/exist/xquery/modules/expathrepo/repair.xql\";\n" +
351
                    "repair:clean-all(),\n" +
352
                    "repair:repair()\n");
353
        }
354
    }
×
355

356
    private static class AggregatingConsoleRestoreServiceTaskListener extends ConsoleRestoreServiceTaskListener {
357
        private StringBuilder allProblems = null;
×
358

359
        public AggregatingConsoleRestoreServiceTaskListener(final boolean quiet) {
360
            super(quiet);
×
361
        }
×
362

363
        @Override
364
        public void warn(final String message) {
365
            super.warn(message);
×
366
            addProblem(true, message);
×
367
        }
×
368

369
        @Override
370
        public void error(final String message) {
371
            super.error(message);
×
372
            addProblem(false, message);
×
373
        }
×
374

375
        public boolean hasProblems() {
376
            return allProblems != null && allProblems.length() > 0;
×
377
        }
378

379
        public String getAllProblems() {
380
            return allProblems.toString();
×
381
        }
382

383
        private void addProblem(final boolean warning, final String message) {
384
            final String sep = System.getProperty("line.separator");
×
385
            if (allProblems == null) {
×
386
                allProblems = new StringBuilder();
×
387
                allProblems.append("------------------------------------").append(sep);
×
388
                allProblems.append("Problems occurred found during restore:").append(sep);
×
389
            }
390

391
            if (warning) {
×
392
                allProblems.append("WARN: ");
×
393
            } else {
×
394
                allProblems.append("ERROR: ");
×
395
            }
396
            allProblems.append(message);
×
397
            allProblems.append(sep);
×
398
        }
×
399
    }
400

401
    private static void restoreWithGui(final String username, final String password, final Optional<String> dbaPassword,
402
                                       final Path f, final XmldbURI uri, boolean overwriteApps) {
403

404
        final GuiRestoreServiceTaskListener listener = new GuiRestoreServiceTaskListener();
×
405

406
        listener.info("Connecting ...");
×
407

408
        final Callable<Void> callable = () -> {
×
409

410
            try {
411
                final Collection collection = DatabaseManager.getCollection(uri.toString(), username, password);
×
412
                final EXistRestoreService service = collection.getService(EXistRestoreService.class);
×
413
                service.restore(f.toAbsolutePath().toString(), dbaPassword.orElse(null), listener, overwriteApps);
×
414

415
                listener.enableDismissDialogButton();
×
416

417
                if (JOptionPane.showConfirmDialog(null, "Would you like to rebuild the application repository?\nThis is only necessary if application packages were restored.", "Rebuild App Repository?",
×
418
                        JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION) {
×
419
                    System.out.println("Rebuilding application repository ...");
×
420
                    try {
421
                        final Collection root = DatabaseManager.getCollection(uri.toString(), username, dbaPassword.orElse(password));
×
422
                        ClientFrame.repairRepository(root);
×
423
                        listener.info("Application repository rebuilt successfully.");
×
424
                    } catch (final XMLDBException e) {
×
425
                        reportError(e);
×
426
                        listener.info("Rebuilding application repository failed!");
×
427
                    }
428
                }
429
            } catch (final Exception e) {
×
430
                ClientFrame.showErrorMessage(e.getMessage(), null); //$NON-NLS-1$
×
431
            } finally {
432
                if (listener.hasProblems()) {
×
433
                    ClientFrame.showErrorMessage(listener.getAllProblems(), null);
×
434
                }
435
            }
436

437
            return null;
×
438
        };
439

440
        final ExecutorService executor = Executors.newSingleThreadExecutor(new NamedThreadFactory(null, null, "backup.restore-with-gui"));
×
441
        final Future<Void> future = executor.submit(callable);
×
442

443
        while (!future.isDone() && !future.isCancelled()) {
×
444
            try {
445
                future.get(100, TimeUnit.MILLISECONDS);
×
446
            } catch (final InterruptedException | TimeoutException ie) {
×
447

448
            } catch (final ExecutionException ee) {
×
449
                break;
×
450
            }
451
        }
452
    }
×
453

454

455
    private static void reportError(final Throwable e) {
456
        e.printStackTrace();
×
457

458
        if (e.getCause() != null) {
×
459
            System.err.println("caused by ");
×
460
            e.getCause().printStackTrace();
×
461
        }
462

463
        System.exit(SystemExitCodes.CATCH_ALL_GENERAL_ERROR_EXIT_CODE);
×
464
    }
×
465

466
    private static void shutdown(final Collection root) {
467
        try {
468
            final DatabaseInstanceManager mgr = root.getService(DatabaseInstanceManager.class);
×
469

470
            if (mgr == null) {
×
471
                System.err.println("service is not available");
×
472
            } else if (mgr.isLocalInstance()) {
×
473
                System.out.println("shutting down database...");
×
474
                mgr.shutdown();
×
475
            }
476
        } catch (final XMLDBException e) {
×
477
            System.err.println("database shutdown failed: ");
×
478
            e.printStackTrace();
×
479
        }
480
    }
×
481

482

483
    public static void main(final String[] args) {
484
        try {
485
            CompatibleJavaVersionCheck.checkForCompatibleJavaVersion();
×
486

487
            final ParsedArguments arguments = CommandLineParser
×
488
                    .withArguments(userArg, passwordArg, dbaPasswordArg, useSslArg)
×
489
                    .andArguments(backupCollectionArg, backupOutputDirArg, backupDeduplicateBlobs)
×
490
                    .andArguments(restoreArg, rebuildExpathRepoArg, overwriteAppsArg)
×
491
                    .andArguments(helpArg, guiArg, quietArg, optionArg)
×
492
                    .programName("backup" + (OSUtil.IS_WINDOWS ? ".bat" : ".sh"))
×
493
                    .parse(args);
×
494

495
            process(arguments);
×
496
        } catch (final StartException e) {
×
497
            if (e.getMessage() != null && !e.getMessage().isEmpty()) {
×
498
                System.err.println(e.getMessage());
×
499
            }
500
            System.exit(e.getErrorCode());
×
501
        } catch(final ArgumentException e) {
×
502
            System.out.println(e.getMessageAndUsage());
×
503
            System.exit(SystemExitCodes.INVALID_ARGUMENT_EXIT_CODE);
×
504

505
        } catch (final Throwable e) {
×
506
            e.printStackTrace();
×
507
            System.exit(SystemExitCodes.CATCH_ALL_GENERAL_ERROR_EXIT_CODE);
×
508
        }
509
    }
×
510
}
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