OpenConcerto

Dépôt officiel du code source de l'ERP OpenConcerto
sonarqube

svn://code.openconcerto.org/openconcerto

Rev

Rev 177 | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
18 ilm 1
/*
2
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
3
 *
182 ilm 4
 * Copyright 2011-2019 OpenConcerto, by ILM Informatique. All rights reserved.
18 ilm 5
 *
6
 * The contents of this file are subject to the terms of the GNU General Public License Version 3
7
 * only ("GPL"). You may not use this file except in compliance with the License. You can obtain a
8
 * copy of the License at http://www.gnu.org/licenses/gpl-3.0.html See the License for the specific
9
 * language governing permissions and limitations under the License.
10
 *
11
 * When distributing the software, include this License Header Notice in each file.
12
 */
13
 
14
 package org.openconcerto.erp.modules;
15
 
67 ilm 16
import org.openconcerto.erp.config.Log;
18 ilm 17
import org.openconcerto.erp.config.MainFrame;
73 ilm 18
import org.openconcerto.erp.config.MenuAndActions;
19
import org.openconcerto.erp.config.MenuManager;
80 ilm 20
import org.openconcerto.erp.modules.DepSolverResult.Factory;
21
import org.openconcerto.erp.modules.ModuleTableModel.ModuleRow;
18 ilm 22
import org.openconcerto.sql.Configuration;
19 ilm 23
import org.openconcerto.sql.element.SQLElement;
24
import org.openconcerto.sql.element.SQLElementDirectory;
156 ilm 25
import org.openconcerto.sql.element.SQLElementNamesFromXML;
80 ilm 26
import org.openconcerto.sql.model.AliasedTable;
25 ilm 27
import org.openconcerto.sql.model.ConnectionHandlerNoSetup;
18 ilm 28
import org.openconcerto.sql.model.DBFileCache;
29
import org.openconcerto.sql.model.DBItemFileCache;
30
import org.openconcerto.sql.model.DBRoot;
25 ilm 31
import org.openconcerto.sql.model.SQLDataSource;
19 ilm 32
import org.openconcerto.sql.model.SQLField;
33
import org.openconcerto.sql.model.SQLName;
80 ilm 34
import org.openconcerto.sql.model.SQLRow;
35
import org.openconcerto.sql.model.SQLRowListRSH;
36
import org.openconcerto.sql.model.SQLRowValues;
37
import org.openconcerto.sql.model.SQLSelect;
38
import org.openconcerto.sql.model.SQLSyntax;
25 ilm 39
import org.openconcerto.sql.model.SQLSystem;
18 ilm 40
import org.openconcerto.sql.model.SQLTable;
80 ilm 41
import org.openconcerto.sql.model.TableRef;
42
import org.openconcerto.sql.model.Where;
18 ilm 43
import org.openconcerto.sql.model.graph.DirectedEdge;
80 ilm 44
import org.openconcerto.sql.model.graph.Link.Rule;
27 ilm 45
import org.openconcerto.sql.preferences.SQLPreferences;
80 ilm 46
import org.openconcerto.sql.request.SQLFieldTranslator;
47
import org.openconcerto.sql.users.rights.UserRightsManager;
19 ilm 48
import org.openconcerto.sql.utils.AlterTable;
49
import org.openconcerto.sql.utils.ChangeTable;
80 ilm 50
import org.openconcerto.sql.utils.ChangeTable.ForeignColSpec;
19 ilm 51
import org.openconcerto.sql.utils.DropTable;
80 ilm 52
import org.openconcerto.sql.utils.SQLCreateTable;
18 ilm 53
import org.openconcerto.sql.utils.SQLUtils;
54
import org.openconcerto.sql.utils.SQLUtils.SQLFactory;
83 ilm 55
import org.openconcerto.sql.view.list.IListeAction;
73 ilm 56
import org.openconcerto.ui.SwingThreadUtils;
182 ilm 57
import org.openconcerto.utils.BaseDirs;
80 ilm 58
import org.openconcerto.utils.CollectionMap2.Mode;
19 ilm 59
import org.openconcerto.utils.CollectionUtils;
18 ilm 60
import org.openconcerto.utils.ExceptionHandler;
156 ilm 61
import org.openconcerto.utils.ExceptionUtils;
25 ilm 62
import org.openconcerto.utils.FileUtils;
80 ilm 63
import org.openconcerto.utils.SetMap;
19 ilm 64
import org.openconcerto.utils.StringUtils;
61 ilm 65
import org.openconcerto.utils.ThreadFactory;
18 ilm 66
import org.openconcerto.utils.Tuple2;
80 ilm 67
import org.openconcerto.utils.Tuple3;
18 ilm 68
import org.openconcerto.utils.cc.IClosure;
19 ilm 69
import org.openconcerto.utils.cc.IdentityHashSet;
25 ilm 70
import org.openconcerto.utils.cc.IdentitySet;
73 ilm 71
import org.openconcerto.utils.i18n.TranslationManager;
80 ilm 72
import org.openconcerto.xml.XMLCodecUtils;
18 ilm 73
 
80 ilm 74
import java.beans.XMLDecoder;
75
import java.beans.XMLEncoder;
18 ilm 76
import java.io.File;
77
import java.io.FileFilter;
80 ilm 78
import java.io.FileInputStream;
79
import java.io.FileOutputStream;
18 ilm 80
import java.io.IOException;
19 ilm 81
import java.io.InputStream;
156 ilm 82
import java.nio.charset.StandardCharsets;
83
import java.nio.file.Files;
84
import java.nio.file.Path;
85
import java.nio.file.StandardCopyOption;
18 ilm 86
import java.sql.SQLException;
87
import java.util.ArrayList;
88
import java.util.Arrays;
89
import java.util.Collection;
90
import java.util.Collections;
80 ilm 91
import java.util.Date;
18 ilm 92
import java.util.HashMap;
93
import java.util.HashSet;
80 ilm 94
import java.util.IdentityHashMap;
95
import java.util.Iterator;
18 ilm 96
import java.util.LinkedHashMap;
19 ilm 97
import java.util.LinkedHashSet;
80 ilm 98
import java.util.LinkedList;
18 ilm 99
import java.util.List;
80 ilm 100
import java.util.ListIterator;
101
import java.util.Locale;
18 ilm 102
import java.util.Map;
25 ilm 103
import java.util.Map.Entry;
80 ilm 104
import java.util.ResourceBundle.Control;
20 ilm 105
import java.util.Set;
80 ilm 106
import java.util.SortedMap;
107
import java.util.TreeSet;
73 ilm 108
import java.util.concurrent.Callable;
61 ilm 109
import java.util.concurrent.Executor;
67 ilm 110
import java.util.concurrent.ExecutorService;
73 ilm 111
import java.util.concurrent.FutureTask;
61 ilm 112
import java.util.concurrent.LinkedBlockingQueue;
113
import java.util.concurrent.ThreadPoolExecutor;
114
import java.util.concurrent.TimeUnit;
80 ilm 115
import java.util.logging.Level;
19 ilm 116
import java.util.logging.Logger;
80 ilm 117
import java.util.prefs.BackingStoreException;
18 ilm 118
import java.util.prefs.Preferences;
119
 
120
import javax.swing.SwingUtilities;
121
 
61 ilm 122
import net.jcip.annotations.GuardedBy;
123
import net.jcip.annotations.ThreadSafe;
124
 
18 ilm 125
/**
126
 * Hold the list of known modules and their status.
127
 *
128
 * @author Sylvain CUAZ
129
 */
61 ilm 130
@ThreadSafe
18 ilm 131
public class ModuleManager {
132
 
67 ilm 133
    /**
80 ilm 134
     * The right to install/uninstall modules in the database (everyone can install locally).
135
     */
136
    static public final String MODULE_DB_RIGHT = "moduleDBAdmin";
67 ilm 137
 
80 ilm 138
    static final Logger L = Logger.getLogger(ModuleManager.class.getPackage().getName());
67 ilm 139
    @GuardedBy("ModuleManager.class")
140
    private static ExecutorService exec = null;
19 ilm 141
 
67 ilm 142
    private static synchronized final Executor getExec() {
143
        if (exec == null)
156 ilm 144
            exec = new ThreadPoolExecutor(0, 2, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(), new ThreadFactory(ModuleManager.class.getSimpleName()
145
                    // not daemon since install() is not atomic
146
                    + " executor thread ", false));
67 ilm 147
        return exec;
148
    }
149
 
80 ilm 150
    static final Runnable EMPTY_RUNNABLE = new Runnable() {
151
        @Override
152
        public void run() {
153
        }
154
    };
67 ilm 155
 
80 ilm 156
    // public needed for XMLEncoder
157
    static public enum ModuleState {
158
        NOT_CREATED, CREATED, INSTALLED, REGISTERED, STARTED
159
    }
160
 
161
    static enum ModuleAction {
162
        INSTALL, START, STOP, UNINSTALL
163
    }
164
 
165
    private static final long MIN_VERSION = ModuleVersion.MIN.getMerged();
166
    private static final String MODULE_COLNAME = "MODULE_NAME";
167
    private static final String MODULE_VERSION_COLNAME = "MODULE_VERSION";
168
    private static final String TABLE_COLNAME = "TABLE";
169
    private static final String FIELD_COLNAME = "FIELD";
170
    private static final String ISKEY_COLNAME = "KEY";
171
    // Don't use String literals for the synchronized blocks
172
    private static final String FWK_MODULE_TABLENAME = new String("FWK_MODULE_METADATA");
173
    private static final String FWK_MODULE_DEP_TABLENAME = new String("FWK_MODULE_DEP");
174
    private static final String NEEDING_MODULE_COLNAME = "ID_MODULE";
175
    private static final String NEEDED_MODULE_COLNAME = "ID_MODULE_NEEDED";
61 ilm 176
    private static final String fileMutex = new String("modules");
80 ilm 177
 
178
    private static final Integer TO_INSTALL_VERSION = 1;
179
 
156 ilm 180
    private static final String OLD_BACKUP_DIR_SUFFIX = ".backup";
181
    private static final String OLD_FAILED_DIR_SUFFIX = ".failed";
182
    private static final String BACKUP_DIR = "Install backup";
183
    private static final String FAILED_DIR = "Install failed";
18 ilm 184
 
80 ilm 185
    // return true if the MainFrame is not displayable (or if there's none)
186
    static private boolean noDisplayableFrame() {
187
        final MainFrame mf = MainFrame.getInstance();
188
        if (mf == null)
189
            return true;
190
        final FutureTask<Boolean> f = new FutureTask<Boolean>(new Callable<Boolean>() {
191
            @Override
192
            public Boolean call() throws Exception {
193
                return !mf.isDisplayable();
194
            }
195
        });
196
        SwingThreadUtils.invoke(f);
197
        try {
198
            return f.get();
199
        } catch (Exception e) {
200
            Log.get().log(Level.WARNING, "Couldn't determine MainFrame displayability", e);
201
            return true;
202
        }
203
    }
204
 
67 ilm 205
    public static synchronized void tearDown() {
206
        if (exec != null) {
207
            exec.shutdown();
208
            exec = null;
209
        }
210
    }
211
 
80 ilm 212
    public static String getMDVariant(ModuleFactory f) {
213
        return getMDVariant(f.getReference());
57 ilm 214
    }
215
 
80 ilm 216
    public static String getMDVariant(ModuleReference ref) {
217
        return ref.getID();
218
    }
219
 
220
    static final Set<ModuleReference> versionsMapToSet(final Map<String, ModuleVersion> versions) {
221
        final Set<ModuleReference> res = new HashSet<ModuleReference>(versions.size());
222
        for (final Entry<String, ModuleVersion> e : versions.entrySet())
223
            res.add(new ModuleReference(e.getKey(), e.getValue()));
224
        return res;
225
    }
226
 
227
    // final (thus safely published) and thread-safe
228
    private final FactoriesByID factories;
182 ilm 229
    @GuardedBy("this")
230
    private List<File> folders = Collections.emptyList();
80 ilm 231
    // to avoid starting twice the same module
61 ilm 232
    // we synchronize the whole install/start and stop/uninstall
233
    @GuardedBy("this")
18 ilm 234
    private final Map<String, AbstractModule> runningModules;
61 ilm 235
    // in fact it is also already guarded by "this"
236
    @GuardedBy("modulesElements")
80 ilm 237
    private final Map<ModuleReference, IdentityHashMap<SQLElement, SQLElement>> modulesElements;
238
    @GuardedBy("this")
239
    private boolean inited;
61 ilm 240
    // only in EDT
19 ilm 241
    private final Map<String, ComponentsContext> modulesComponents;
80 ilm 242
    // graph of created modules, ATTN since we have no way to "unload" a module we only add and
243
    // never remove from it
61 ilm 244
    @GuardedBy("this")
80 ilm 245
    private final DependencyGraph dependencyGraph;
246
    @GuardedBy("this")
247
    private final Map<ModuleFactory, AbstractModule> createdModules;
18 ilm 248
 
80 ilm 249
    // Another mutex so we can query root or conf without having to wait for modules to
250
    // install/uninstall, or alternatively so that start() & stop() executed in the EDT don't need
251
    // this monitor (see uninstallUnsafe()). This lock is a leaf lock, it mustn't call code that
252
    // might need another lock.
253
    private final Object varLock = new String("varLock");
254
    @GuardedBy("varLock")
25 ilm 255
    private DBRoot root;
61 ilm 256
    @GuardedBy("this")
80 ilm 257
    private SQLPreferences dbPrefs;
258
    @GuardedBy("varLock")
25 ilm 259
    private Configuration conf;
80 ilm 260
    @GuardedBy("varLock")
261
    private boolean exitAllowed;
25 ilm 262
 
18 ilm 263
    public ModuleManager() {
80 ilm 264
        this.factories = new FactoriesByID();
265
        // stopModule() needs order to reset menu
266
        this.runningModules = new LinkedHashMap<String, AbstractModule>();
267
        this.dependencyGraph = new DependencyGraph();
268
        this.createdModules = new LinkedHashMap<ModuleFactory, AbstractModule>();
269
        this.modulesElements = new HashMap<ModuleReference, IdentityHashMap<SQLElement, SQLElement>>();
270
        this.inited = false;
19 ilm 271
        this.modulesComponents = new HashMap<String, ComponentsContext>();
25 ilm 272
 
273
        this.root = null;
80 ilm 274
        this.dbPrefs = null;
25 ilm 275
        this.conf = null;
80 ilm 276
        this.exitAllowed = true;
277
    }
67 ilm 278
 
80 ilm 279
    /**
280
     * Whether the current user can manage modules.
281
     *
282
     * @return <code>true</code> if the current user can manage modules.
283
     */
284
    public final boolean currentUserIsAdmin() {
285
        return UserRightsManager.getCurrentUserRights().haveRight(MODULE_DB_RIGHT);
18 ilm 286
    }
287
 
80 ilm 288
    // AdminRequiredModules means installed & started
289
    // possible AdminForbiddenModules means neither installed nor started
290
    public final boolean canCurrentUser(final ModuleAction action, final ModuleRow m) {
291
        if (currentUserIsAdmin())
292
            return true;
293
 
294
        if (action == ModuleAction.INSTALL || action == ModuleAction.UNINSTALL)
295
            return canCurrentUserInstall(action, m.isInstalledRemotely());
296
        else if (action == ModuleAction.START)
297
            return true;
298
        else if (action == ModuleAction.STOP)
299
            return !m.isAdminRequired();
300
        else
301
            throw new IllegalArgumentException("Unknown action " + action);
67 ilm 302
    }
303
 
80 ilm 304
    final boolean canCurrentUserInstall(final ModuleAction action, final ModuleReference ref, final InstallationState state) {
305
        return this.canCurrentUserInstall(action, state.getRemote().contains(ref));
306
    }
307
 
308
    private final boolean canCurrentUserInstall(final ModuleAction action, final boolean installedRemotely) {
309
        if (currentUserIsAdmin())
310
            return true;
311
 
312
        if (action == ModuleAction.INSTALL)
313
            return installedRemotely;
314
        else if (action == ModuleAction.UNINSTALL)
315
            return !installedRemotely;
316
        else
317
            throw new IllegalArgumentException("Illegal action " + action);
318
    }
319
 
18 ilm 320
    // *** factories (thread-safe)
321
 
182 ilm 322
    public final void setFolders(final File... dirs) {
323
        this.setFolders(Arrays.asList(dirs));
324
    }
325
 
326
    // The system directories should be before the user directories.
327
    // This order is important for getFolderToWrite() and allow the user modules to replace the
328
    // system ones.
329
    public final void setFolders(final List<File> dirs) {
330
        final List<File> absolutes = new ArrayList<>(dirs.size());
331
        for (final File dir : dirs) {
332
            final File abs = dir.getAbsoluteFile();
333
            this.addFactories(abs);
334
            absolutes.add(abs);
335
        }
336
        synchronized (this) {
337
            this.folders = Collections.unmodifiableList(absolutes);
338
        }
339
    }
340
 
341
    public synchronized final List<File> getFolders() {
342
        return this.folders;
343
    }
344
 
345
    public static final File getFolderToWrite(final List<File> dirs) {
346
        // Try to install in system directory first, then fall back to user directory.
347
        for (final File dir : dirs) {
348
            try {
349
                return BaseDirs.getFolderToWrite(dir);
350
            } catch (IOException e) {
351
                // try next one
352
            }
353
        }
354
        return null;
355
    }
356
 
18 ilm 357
    public final int addFactories(final File dir) {
19 ilm 358
        if (!dir.exists()) {
25 ilm 359
            L.warning("Module factory directory not found: " + dir.getAbsolutePath());
19 ilm 360
            return 0;
361
        }
18 ilm 362
        final File[] jars = dir.listFiles(new FileFilter() {
363
            @Override
364
            public boolean accept(File f) {
365
                return f.getName().endsWith(".jar");
366
            }
367
        });
368
        int i = 0;
19 ilm 369
        if (jars != null) {
370
            for (final File jar : jars) {
371
                try {
372
                    this.addFactory(new JarModuleFactory(jar));
373
                    i++;
374
                } catch (Exception e) {
25 ilm 375
                    L.warning("Couldn't add " + jar);
19 ilm 376
                    e.printStackTrace();
377
                }
18 ilm 378
            }
379
        }
380
        return i;
381
    }
382
 
383
    public final ModuleFactory addFactoryFromPackage(File jar) throws IOException {
384
        final ModuleFactory f = new JarModuleFactory(jar);
385
        this.addFactory(f);
386
        return f;
387
    }
388
 
389
    /**
390
     * Adds a factory.
391
     *
392
     * @param f the factory to add.
393
     * @return the ID of the factory.
394
     */
395
    public final String addFactory(ModuleFactory f) {
80 ilm 396
        final ModuleFactory prev = this.factories.add(f);
397
        if (prev != null)
398
            L.info("Changing the factory for " + f.getReference() + "\nfrom\t" + prev + "\nto\t" + f);
399
        return f.getID();
18 ilm 400
    }
401
 
80 ilm 402
    public final void addFactories(Collection<ModuleFactory> factories) {
403
        for (final ModuleFactory f : factories)
404
            this.addFactory(f);
405
    }
406
 
18 ilm 407
    public final String addFactoryAndStart(final ModuleFactory f, final boolean persistent) {
408
        return this.addFactory(f, true, persistent);
409
    }
410
 
411
    private final String addFactory(final ModuleFactory f, final boolean start, final boolean persistent) {
80 ilm 412
        this.addFactory(f);
413
        if (start) {
414
            L.config("addFactory() invoked start " + (persistent ? "" : "not") + " persistent for " + f);
18 ilm 415
            this.invoke(new IClosure<ModuleManager>() {
416
                @Override
417
                public void executeChecked(ModuleManager input) {
418
                    try {
149 ilm 419
                        if (!startModule(f.getReference(), persistent))
420
                            throw new IllegalStateException("Couldn't be started");
421
                    } catch (Throwable e) {
18 ilm 422
                        ExceptionHandler.handle(MainFrame.getInstance(), "Unable to start " + f, e);
423
                    }
424
                }
425
            });
80 ilm 426
        }
18 ilm 427
        return f.getID();
428
    }
429
 
80 ilm 430
    public final Map<String, SortedMap<ModuleVersion, ModuleFactory>> getFactories() {
431
        return this.factories.getMap();
18 ilm 432
    }
433
 
80 ilm 434
    public final FactoriesByID copyFactories() {
435
        return new FactoriesByID(this.factories);
19 ilm 436
    }
437
 
18 ilm 438
    public final void removeFactory(String id) {
80 ilm 439
        this.factories.remove(id);
18 ilm 440
    }
441
 
61 ilm 442
    // *** modules (thread-safe)
18 ilm 443
 
444
    /**
25 ilm 445
     * Call the passed closure at a time when modules can be started. In particular this manager has
446
     * been set up and the {@link MainFrame#getInstance() main frame} has been created.
447
     *
448
     * @param c the closure to execute.
18 ilm 449
     */
450
    public void invoke(final IClosure<ModuleManager> c) {
451
        MainFrame.invoke(new Runnable() {
452
            @Override
453
            public void run() {
67 ilm 454
                getExec().execute(new Runnable() {
61 ilm 455
                    @Override
456
                    public void run() {
457
                        c.executeChecked(ModuleManager.this);
458
                    }
459
                });
18 ilm 460
            }
461
        });
462
    }
463
 
19 ilm 464
    /**
61 ilm 465
     * Allow to access certain methods without a full {@link #setup(DBRoot, Configuration)}. If
466
     * setup() is subsequently called it must be passed the same root instance.
467
     *
468
     * @param root the root.
469
     * @throws IllegalStateException if already set.
470
     * @see #getDBInstalledModules()
471
     * @see #getCreatedItems(String)
472
     */
80 ilm 473
    public final void setRoot(final DBRoot root) {
474
        synchronized (this.varLock) {
475
            if (this.root != root) {
476
                if (this.root != null)
477
                    throw new IllegalStateException("Root already set");
478
                this.root = root;
479
            }
61 ilm 480
        }
481
    }
482
 
80 ilm 483
    public final boolean isSetup() {
484
        synchronized (this.varLock) {
485
            return this.getRoot() != null && this.getConf() != null;
486
        }
61 ilm 487
    }
488
 
489
    /**
80 ilm 490
     * Set up the module manager.
19 ilm 491
     *
25 ilm 492
     * @param root the root where the modules install.
493
     * @param conf the configuration the modules change.
80 ilm 494
     * @throws IllegalStateException if already {@link #isSetup() set up}.
19 ilm 495
     */
80 ilm 496
    public final void setup(final DBRoot root, final Configuration conf) throws IllegalStateException {
25 ilm 497
        if (root == null || conf == null)
498
            throw new NullPointerException();
80 ilm 499
        synchronized (this.varLock) {
500
            if (this.isSetup())
501
                throw new IllegalStateException("Already setup");
502
            assert this.modulesElements.isEmpty() && this.runningModules.isEmpty() && this.modulesComponents.isEmpty() : "Modules cannot start without root & conf";
503
            this.setRoot(root);
504
            this.conf = conf;
19 ilm 505
        }
506
    }
507
 
80 ilm 508
    public synchronized final boolean isInited() {
509
        return this.inited;
510
    }
511
 
512
    /**
513
     * Initialise the module manager.
514
     *
515
     * @throws Exception if required modules couldn't be registered.
516
     */
517
    public synchronized final void init() throws Exception {
518
        if (!this.isSetup())
519
            throw new IllegalStateException("Not setup");
520
        // don't check this.inited, that way we could register additional elements
521
 
522
        SQLPreferences.getPrefTable(this.getRoot());
523
 
151 ilm 524
        final List<ModuleReference> requiredModules = new ArrayList<>(this.getAdminRequiredModules());
525
        final List<ModuleReference> dbRequiredModules = new ArrayList<>(this.getDBRequiredModules());
80 ilm 526
        // add modules previously chosen (before restart)
527
        final File toInstallFile = this.getToInstallFile();
528
        Set<ModuleReference> toInstall = Collections.emptySet();
529
        Set<ModuleReference> userReferencesToInstall = Collections.emptySet();
530
        boolean persistent = false;
531
 
532
        ModuleState toInstallTargetState = ModuleState.NOT_CREATED;
533
        if (toInstallFile.exists()) {
534
            if (!toInstallFile.canRead() || !toInstallFile.isFile()) {
535
                L.warning("Couldn't read " + toInstallFile);
536
            } else {
156 ilm 537
                try (final XMLDecoder dec = new XMLDecoder(new FileInputStream(toInstallFile))) {
80 ilm 538
                    final Number version = (Number) dec.readObject();
93 ilm 539
                    if (version.intValue() != TO_INSTALL_VERSION.intValue())
80 ilm 540
                        throw new Exception("Version mismatch, expected " + TO_INSTALL_VERSION + " found " + version);
541
                    final Date fileDate = (Date) dec.readObject();
542
                    @SuppressWarnings("unchecked")
543
                    final Set<ModuleReference> toInstallUnsafe = (Set<ModuleReference>) dec.readObject();
544
                    @SuppressWarnings("unchecked")
545
                    final Set<ModuleReference> userReferencesToInstallUnsafe = (Set<ModuleReference>) dec.readObject();
546
                    toInstallTargetState = (ModuleState) dec.readObject();
547
                    persistent = (Boolean) dec.readObject();
548
                    try {
549
                        final Object extra = dec.readObject();
550
                        assert false : "Extra object " + extra;
551
                    } catch (ArrayIndexOutOfBoundsException e) {
552
                        // OK
553
                    }
554
 
555
                    final Date now = new Date();
556
                    if (fileDate.compareTo(now) > 0) {
557
                        L.warning("File is in the future : " + fileDate);
558
                        // check less than 2 hours
559
                    } else if (now.getTime() - fileDate.getTime() > 2 * 3600 * 1000) {
560
                        L.warning("File is too old : " + fileDate);
561
                    } else {
562
                        // no need to check that remote and local installed haven't changed since
563
                        // we're using ONLY_INSTALL_ARGUMENTS
564
                        toInstall = toInstallUnsafe;
565
                        userReferencesToInstall = userReferencesToInstallUnsafe;
566
                        if (toInstallTargetState.compareTo(ModuleState.REGISTERED) < 0)
567
                            L.warning("Forcing state to " + ModuleState.REGISTERED);
568
                    }
569
                } catch (Exception e) {
570
                    // move out file to allow the next init() to succeed
571
                    final File errorFile = FileUtils.addSuffix(toInstallFile, ".error");
572
                    errorFile.delete();
573
                    final boolean renamed = toInstallFile.renameTo(errorFile);
574
                    throw new Exception("Couldn't parse " + toInstallFile + " ; renamed : " + renamed, e);
575
                } finally {
156 ilm 576
                    // Either the installation will succeed and we don't want to execute it again,
577
                    // or the installation will fail and we don't want to be caught in an endless
578
                    // loop (since there's no API to remove this file).
579
                    if (toInstallFile.exists() && !toInstallFile.delete())
580
                        throw new IOException("Couldn't delete " + toInstallFile);
80 ilm 581
                }
582
            }
583
        }
151 ilm 584
        // handle upgrades : the old version was not uninstalled and thus still in
585
        // dbRequiredModules, and the new version is in toInstall, so the DepSolver will error out
586
        // since both can't be installed at the same time.
587
        for (final ModuleReference toInstallRef : toInstall) {
588
            final Iterator<ModuleReference> iter = dbRequiredModules.iterator();
589
            while (iter.hasNext()) {
590
                final ModuleReference dbReqRef = iter.next();
591
                if (dbReqRef.getID().equals(toInstallRef.getID())) {
592
                    L.config("Ignoring DB required " + dbReqRef + " because " + toInstallRef + " was requested from the last exit");
593
                    iter.remove();
594
                }
595
            }
596
        }
597
        requiredModules.addAll(dbRequiredModules);
80 ilm 598
        requiredModules.addAll(toInstall);
599
 
600
        // if there's some choice to make, let the user make it
601
        final Tuple2<Solutions, ModulesStateChangeResult> modules = this.createModules(requiredModules, NoChoicePredicate.ONLY_INSTALL_ARGUMENTS, ModuleState.REGISTERED);
602
        if (modules.get1().getNotCreated().size() > 0)
603
            throw new Exception("Impossible de créer les modules, not solved : " + modules.get0().getNotSolvedReferences() + " ; not created : " + modules.get1().getNotCreated());
604
        if (toInstallTargetState.compareTo(ModuleState.STARTED) >= 0) {
605
            // make them start by startPreviouslyRunningModules() (avoiding invokeLater() of
606
            // createModules())
607
            if (persistent) {
608
                setPersistentModules(userReferencesToInstall);
609
            } else {
610
                // NO_CHANGE since they were just installed above
611
                this.createModules(userReferencesToInstall, NoChoicePredicate.NO_CHANGE, ModuleState.STARTED, persistent);
612
            }
613
        }
614
        this.inited = true;
615
    }
616
 
617
    // whether module removal can exit the VM
618
    final void setExitAllowed(boolean exitAllowed) {
619
        synchronized (this.varLock) {
620
            this.exitAllowed = exitAllowed;
621
        }
622
    }
623
 
624
    final boolean isExitAllowed() {
625
        synchronized (this.varLock) {
626
            return this.exitAllowed;
627
        }
628
    }
629
 
630
    public final boolean needExit(final ModulesStateChange solution) {
151 ilm 631
        final Set<ModuleReference> refsToRemove = solution.getReferencesToRemove();
632
        if (!refsToRemove.isEmpty()) {
633
            // only need to exit if the module is loaded into memory
634
            final Set<ModuleReference> registeredModules = this.getRegisteredModules();
635
            for (final ModuleReference toRemove : refsToRemove) {
636
                if (registeredModules.contains(toRemove))
637
                    return true;
638
            }
639
        }
640
        return false;
80 ilm 641
    }
642
 
61 ilm 643
    // Preferences is thread-safe
18 ilm 644
    private Preferences getPrefs() {
645
        // modules are installed per business entity (perhaps we could add a per user option, i.e.
646
        // for all businesses of all databases)
647
        final StringBuilder path = new StringBuilder(32);
25 ilm 648
        for (final String item : DBFileCache.getJDBCAncestorNames(getRoot(), true)) {
19 ilm 649
            path.append(StringUtils.getBoundedLengthString(DBItemFileCache.encode(item), Preferences.MAX_NAME_LENGTH));
18 ilm 650
            path.append('/');
651
        }
652
        // path must not end with '/'
653
        path.setLength(path.length() - 1);
654
        return Preferences.userNodeForPackage(ModuleManager.class).node(path.toString());
655
    }
656
 
80 ilm 657
    // only put references passed by the user. That way if he installs module 'A' which depends on
658
    // module 'util' and he later upgrade 'A' to a version which doesn't need 'util' anymore it
659
    // won't get started. MAYBE even offer an "auto remove" feature.
18 ilm 660
    private Preferences getRunningIDsPrefs() {
661
        return getPrefs().node("toRun");
662
    }
663
 
80 ilm 664
    protected final Preferences getDBPrefs() {
665
        synchronized (this) {
666
            if (this.dbPrefs == null) {
667
                final DBRoot root = getRoot();
668
                if (root != null)
669
                    this.dbPrefs = (SQLPreferences) new SQLPreferences(root).node("modules");
670
            }
671
            return this.dbPrefs;
672
        }
27 ilm 673
    }
674
 
80 ilm 675
    private final Preferences getRequiredIDsPrefs() {
676
        return getDBPrefs().node("required");
67 ilm 677
    }
678
 
18 ilm 679
    protected final boolean isModuleInstalledLocally(String id) {
156 ilm 680
        final File versionFile = getLocalVersionFile(id);
681
        return versionFile != null && versionFile.exists();
18 ilm 682
    }
683
 
19 ilm 684
    protected final ModuleVersion getModuleVersionInstalledLocally(String id) {
61 ilm 685
        synchronized (fileMutex) {
686
            final File versionFile = getLocalVersionFile(id);
156 ilm 687
            if (versionFile != null && versionFile.exists()) {
61 ilm 688
                try {
689
                    return new ModuleVersion(Long.valueOf(FileUtils.read(versionFile)));
690
                } catch (IOException e) {
691
                    throw new IllegalStateException("Couldn't get installed version of " + id, e);
692
                }
693
            } else {
694
                return null;
25 ilm 695
            }
696
        }
19 ilm 697
    }
698
 
80 ilm 699
    public final Set<ModuleReference> getModulesInstalledLocally() {
700
        return versionsMapToSet(getModulesVersionInstalledLocally());
701
    }
702
 
703
    public final Map<String, ModuleVersion> getModulesVersionInstalledLocally() {
61 ilm 704
        synchronized (fileMutex) {
705
            final File dir = getLocalDirectory();
80 ilm 706
            if (!dir.isDirectory())
707
                return Collections.emptyMap();
708
            final Map<String, ModuleVersion> res = new HashMap<String, ModuleVersion>();
61 ilm 709
            for (final File d : dir.listFiles()) {
710
                final String id = d.getName();
711
                final ModuleVersion version = getModuleVersionInstalledLocally(id);
80 ilm 712
                if (version != null)
713
                    res.put(id, version);
61 ilm 714
            }
715
            return res;
19 ilm 716
        }
717
    }
718
 
80 ilm 719
    private void setModuleInstalledLocally(ModuleReference f, boolean b) {
25 ilm 720
        try {
61 ilm 721
            synchronized (fileMutex) {
80 ilm 722
                if (b) {
723
                    final ModuleVersion vers = f.getVersion();
724
                    vers.checkValidity();
725
                    final File versionFile = getLocalVersionFile(f.getID());
726
                    FileUtils.mkdir_p(versionFile.getParentFile());
727
                    FileUtils.write(String.valueOf(vers.getMerged()), versionFile);
728
                } else {
729
                    // perhaps add a parameter to only remove the versionFile
730
                    FileUtils.rm_R(getLocalDirectory(f.getID()));
731
                }
25 ilm 732
            }
733
        } catch (IOException e) {
734
            throw new IllegalStateException("Couldn't change installed status of " + f, e);
18 ilm 735
        }
736
    }
737
 
80 ilm 738
    private SQLTable getInstalledTable(final DBRoot r) throws SQLException {
739
        synchronized (FWK_MODULE_TABLENAME) {
740
            final List<SQLCreateTable> createTables = new ArrayList<SQLCreateTable>(4);
741
            final SQLCreateTable createTable;
742
            if (!r.contains(FWK_MODULE_TABLENAME)) {
743
                // store :
744
                // - currently installed module (TABLE_COLNAME & FIELD_COLNAME are null)
745
                // - created tables (FIELD_COLNAME is null)
746
                // - created fields (and whether they are keys)
747
                createTable = new SQLCreateTable(r, FWK_MODULE_TABLENAME);
748
                createTable.setPlain(true);
749
                // let SQLCreateTable know which column is the primary key so that createDepTable
750
                // can refer to it
751
                createTable.addColumn(SQLSyntax.ID_NAME, createTable.getSyntax().getPrimaryIDDefinitionShort());
752
                createTable.setPrimaryKey(SQLSyntax.ID_NAME);
753
                createTable.addVarCharColumn(MODULE_COLNAME, 128);
754
                createTable.addColumn(TABLE_COLNAME, "varchar(128) NULL");
755
                createTable.addColumn(FIELD_COLNAME, "varchar(128) NULL");
756
                createTable.addColumn(ISKEY_COLNAME, "boolean NULL");
757
                createTable.addColumn(MODULE_VERSION_COLNAME, "bigint NOT NULL");
758
 
759
                createTable.addUniqueConstraint("uniqModule", Arrays.asList(MODULE_COLNAME, TABLE_COLNAME, FIELD_COLNAME));
760
                createTables.add(createTable);
761
            } else {
762
                createTable = null;
763
            }
764
            if (!r.contains(FWK_MODULE_DEP_TABLENAME)) {
765
                final SQLCreateTable createDepTable = new SQLCreateTable(r, FWK_MODULE_DEP_TABLENAME);
766
                createDepTable.setPlain(true);
767
                final ForeignColSpec fk, fkNeeded;
768
                if (createTable != null) {
769
                    fk = ForeignColSpec.fromCreateTable(createTable);
770
                    fkNeeded = ForeignColSpec.fromCreateTable(createTable);
771
                } else {
772
                    final SQLTable moduleT = r.getTable(FWK_MODULE_TABLENAME);
773
                    fk = ForeignColSpec.fromTable(moduleT);
774
                    fkNeeded = ForeignColSpec.fromTable(moduleT);
775
                }
776
                // if we remove a module, remove it dependencies
777
                createDepTable.addForeignColumn(fk.setColumnName(NEEDING_MODULE_COLNAME), Rule.CASCADE, Rule.CASCADE);
778
                // if we try to remove a module that is needed, fail
779
                createDepTable.addForeignColumn(fkNeeded.setColumnName(NEEDED_MODULE_COLNAME), Rule.CASCADE, Rule.RESTRICT);
780
 
781
                createDepTable.setPrimaryKey(NEEDING_MODULE_COLNAME, NEEDED_MODULE_COLNAME);
782
                createTables.add(createDepTable);
783
            }
784
            r.createTables(createTables);
18 ilm 785
        }
80 ilm 786
        return r.getTable(FWK_MODULE_TABLENAME);
18 ilm 787
    }
788
 
80 ilm 789
    private final SQLTable getDepTable() {
790
        return getRoot().getTable(FWK_MODULE_DEP_TABLENAME);
791
    }
792
 
793
    public final DBRoot getRoot() {
794
        synchronized (this.varLock) {
795
            return this.root;
796
        }
797
    }
798
 
25 ilm 799
    private SQLDataSource getDS() {
800
        return getRoot().getDBSystemRoot().getDataSource();
801
    }
802
 
80 ilm 803
    public final Configuration getConf() {
804
        synchronized (this.varLock) {
805
            return this.conf;
806
        }
25 ilm 807
    }
808
 
809
    private SQLElementDirectory getDirectory() {
810
        return getConf().getDirectory();
811
    }
812
 
156 ilm 813
    public final File getLocalDirectory() {
25 ilm 814
        return new File(this.getConf().getConfDir(getRoot()), "modules");
815
    }
816
 
156 ilm 817
    public final Set<String> migrateOldTransientDirs() {
818
        Set<String> res = Collections.emptySet();
819
        final File[] listFiles = this.getLocalDirectory().listFiles();
820
        if (listFiles != null) {
821
            for (final File f : listFiles) {
822
                if (f.isDirectory()) {
823
                    res = migrateOldDir(res, f, OLD_BACKUP_DIR_SUFFIX);
824
                    res = migrateOldDir(res, f, OLD_FAILED_DIR_SUFFIX);
825
                }
826
            }
827
        }
828
        return res;
829
    }
830
 
831
    private final String getOldID(final File f, final String oldSuffix) {
832
        if (f.getName().endsWith(oldSuffix)) {
833
            final String id = f.getName().substring(0, f.getName().length() - oldSuffix.length());
834
            return ModuleReference.checkID(id, false);
835
        }
836
        return null;
837
    }
838
 
839
    private final Set<String> migrateOldDir(Set<String> res, final File f, final String oldSuffix) {
840
        final String id = getOldID(f, oldSuffix);
841
        if (id != null) {
842
            final Path localFailedDir = getLocalFailedDirectory(id);
843
            try {
844
                if (Files.exists(localFailedDir)) {
845
                    // newer already exists, remove old one
846
                    FileUtils.rm_R(f);
847
                } else {
848
                    Files.createDirectories(localFailedDir.getParent());
849
                    Files.move(f.toPath(), localFailedDir);
850
                }
851
            } catch (IOException e) {
852
                L.log(Level.CONFIG, "Couldn't migrate " + f, e);
853
                if (res.isEmpty())
854
                    res = new HashSet<>();
855
                res.add(id);
856
            }
857
        }
858
        return res;
859
    }
860
 
80 ilm 861
    // file specifying which module (and only those, dependencies won't be installed automatically)
862
    // to install during the next application launch.
863
    private final File getToInstallFile() {
864
        return new File(getLocalDirectory(), "toInstall");
865
    }
866
 
156 ilm 867
    private final File getLocalDataSubDir(final File f) {
868
        // TODO return "Local Data" and a migration method
869
        return f;
870
    }
871
 
872
    // => create "internal state" and "local data" folders inside getLocalDirectory() (i.e.
873
    // module.id/local data and not local data/module.id so that we can easily copy to
874
    // getLocalBackupDirectory() or uninstall a module)
875
 
876
    private final Path getInternalStateSubDir(final Path f) {
877
        // TODO return "Internal State" and a migration method
878
        return f;
879
    }
880
 
881
    protected final File getLocalDataDirectory(final String id) {
882
        return getLocalDataSubDir(this.getLocalDirectory(id));
883
    }
884
 
885
    private final Path getInternalStateDirectory(final String id) {
886
        final File l = this.getLocalDirectory(id);
887
        return l == null ? null : getInternalStateSubDir(l.toPath());
888
    }
889
 
890
    private final File getLocalDirectory(final String id) {
891
        if (ModuleReference.checkID(id, false) == null)
892
            return null;
25 ilm 893
        return new File(this.getLocalDirectory(), id);
894
    }
895
 
156 ilm 896
    // contains a copy of local module data during install
897
    protected final Path getLocalBackupDirectory(final String id) {
898
        // will be ignored by getModulesVersionInstalledLocally()
899
        assert ModuleReference.checkID(BACKUP_DIR, false) == null;
900
        return this.getLocalDirectory().toPath().resolve(BACKUP_DIR).resolve(id);
901
    }
902
 
903
    // contains local module data of the last failed install
904
    protected final Path getLocalFailedDirectory(final String id) {
905
        // will be ignored by getModulesVersionInstalledLocally()
906
        assert ModuleReference.checkID(FAILED_DIR, false) == null;
907
        return this.getLocalDirectory().toPath().resolve(FAILED_DIR).resolve(id);
908
    }
909
 
25 ilm 910
    private final File getLocalVersionFile(final String id) {
156 ilm 911
        final Path dir = this.getInternalStateDirectory(id);
912
        if (dir == null)
913
            return null;
914
        return new File(dir.toFile(), "version");
25 ilm 915
    }
916
 
80 ilm 917
    public final ModuleVersion getDBInstalledModuleVersion(final String id) throws SQLException {
918
        return getDBInstalledModules(id).get(id);
18 ilm 919
    }
920
 
80 ilm 921
    public final Set<ModuleReference> getModulesInstalledRemotely() throws SQLException {
922
        return getDBInstalledModuleRowsByRef(null).keySet();
923
    }
18 ilm 924
 
80 ilm 925
    public final Map<String, ModuleVersion> getDBInstalledModules() throws SQLException {
926
        return getDBInstalledModules(null);
927
    }
73 ilm 928
 
80 ilm 929
    private final Where getModuleRowWhere(final TableRef installedTable) throws SQLException {
930
        return Where.isNull(installedTable.getField(TABLE_COLNAME)).and(Where.isNull(installedTable.getField(FIELD_COLNAME)));
931
    }
67 ilm 932
 
80 ilm 933
    private final List<SQLRow> getDBInstalledModuleRows(final String id) throws SQLException {
934
        final SQLTable installedTable = getInstalledTable(getRoot());
935
        final SQLSelect sel = new SQLSelect().addSelectStar(installedTable);
936
        sel.setWhere(getModuleRowWhere(installedTable));
937
        if (id != null)
938
            sel.andWhere(new Where(installedTable.getField(MODULE_COLNAME), "=", id));
939
        return SQLRowListRSH.execute(sel);
940
    }
67 ilm 941
 
80 ilm 942
    private final ModuleReference getRef(final SQLRow r) throws SQLException {
943
        return new ModuleReference(r.getString(MODULE_COLNAME), new ModuleVersion(r.getLong(MODULE_VERSION_COLNAME)));
944
    }
945
 
946
    private final Map<String, ModuleVersion> getDBInstalledModules(final String id) throws SQLException {
947
        final Map<String, ModuleVersion> res = new HashMap<String, ModuleVersion>();
948
        for (final SQLRow r : getDBInstalledModuleRows(id)) {
949
            final ModuleReference ref = getRef(r);
950
            res.put(ref.getID(), ref.getVersion());
18 ilm 951
        }
80 ilm 952
        return res;
953
    }
18 ilm 954
 
80 ilm 955
    private final Map<ModuleReference, SQLRow> getDBInstalledModuleRowsByRef(final String id) throws SQLException {
956
        final Map<ModuleReference, SQLRow> res = new HashMap<ModuleReference, SQLRow>();
957
        for (final SQLRow r : getDBInstalledModuleRows(id)) {
958
            res.put(getRef(r), r);
959
        }
960
        return res;
19 ilm 961
    }
962
 
80 ilm 963
    private SQLRow setDBInstalledModule(ModuleReference f, boolean b) throws SQLException {
964
        final SQLTable installedTable = getInstalledTable(getRoot());
965
        final Where idW = new Where(installedTable.getField(MODULE_COLNAME), "=", f.getID());
966
        final Where w = idW.and(getModuleRowWhere(installedTable));
967
        if (b) {
968
            final SQLSelect sel = new SQLSelect();
969
            sel.addSelect(installedTable.getKey());
970
            sel.setWhere(w);
971
            final Number id = (Number) installedTable.getDBSystemRoot().getDataSource().executeScalar(sel.asString());
972
            final SQLRowValues vals = new SQLRowValues(installedTable);
973
            vals.put(MODULE_VERSION_COLNAME, f.getVersion().getMerged());
974
            if (id != null) {
975
                vals.setID(id);
976
                return vals.update();
977
            } else {
978
                vals.put(MODULE_COLNAME, f.getID());
979
                vals.put(TABLE_COLNAME, null);
980
                vals.put(FIELD_COLNAME, null);
981
                return vals.insert();
982
            }
983
        } else {
984
            installedTable.getDBSystemRoot().getDataSource().execute("DELETE FROM " + installedTable.getSQLName().quote() + " WHERE " + w.getClause());
985
            installedTable.fireTableModified(SQLRow.NONEXISTANT_ID);
986
            return null;
987
        }
988
    }
19 ilm 989
 
80 ilm 990
    public final Tuple2<Set<String>, Set<SQLName>> getCreatedItems(final String id) throws SQLException {
991
        final SQLTable installedTable = getInstalledTable(getRoot());
992
        final SQLSelect sel = new SQLSelect();
993
        sel.addSelect(installedTable.getKey());
994
        sel.addSelect(installedTable.getField(TABLE_COLNAME));
995
        sel.addSelect(installedTable.getField(FIELD_COLNAME));
996
        sel.setWhere(new Where(installedTable.getField(MODULE_COLNAME), "=", id).and(Where.isNotNull(installedTable.getField(TABLE_COLNAME))));
997
        final Set<String> tables = new HashSet<String>();
998
        final Set<SQLName> fields = new HashSet<SQLName>();
999
        for (final SQLRow r : SQLRowListRSH.execute(sel)) {
1000
            final String tableName = r.getString(TABLE_COLNAME);
1001
            final String fieldName = r.getString(FIELD_COLNAME);
1002
            if (fieldName == null)
1003
                tables.add(tableName);
1004
            else
1005
                fields.add(new SQLName(tableName, fieldName));
1006
        }
1007
        return Tuple2.create(tables, fields);
1008
    }
1009
 
1010
    private void updateModuleFields(ModuleFactory factory, DepSolverGraph graph, final DBContext ctxt) throws SQLException {
1011
        final SQLTable installedTable = getInstalledTable(getRoot());
1012
        final Where idW = new Where(installedTable.getField(MODULE_COLNAME), "=", factory.getID());
1013
        // removed items
1014
        {
1015
            final List<Where> dropWheres = new ArrayList<Where>();
1016
            for (final String dropped : ctxt.getRemovedTables()) {
1017
                dropWheres.add(new Where(installedTable.getField(TABLE_COLNAME), "=", dropped));
1018
            }
1019
            for (final SQLName dropped : ctxt.getRemovedFieldsFromExistingTables()) {
1020
                dropWheres.add(new Where(installedTable.getField(TABLE_COLNAME), "=", dropped.getItem(0)).and(new Where(installedTable.getField(FIELD_COLNAME), "=", dropped.getItem(1))));
1021
            }
1022
            if (dropWheres.size() > 0)
1023
                installedTable.getDBSystemRoot().getDataSource().execute("DELETE FROM " + installedTable.getSQLName().quote() + " WHERE " + Where.or(dropWheres).and(idW).getClause());
1024
        }
1025
        // added items
1026
        {
1027
            final SQLRowValues vals = new SQLRowValues(installedTable);
1028
            vals.put(MODULE_VERSION_COLNAME, factory.getVersion().getMerged());
1029
            vals.put(MODULE_COLNAME, factory.getID());
1030
            for (final String added : ctxt.getAddedTables()) {
1031
                vals.put(TABLE_COLNAME, added).put(FIELD_COLNAME, null).insert();
1032
                final SQLTable t = ctxt.getRoot().findTable(added);
83 ilm 1033
                if (t == null) {
1034
                    throw new IllegalStateException("Unable to find added table " + added + " in root " + ctxt.getRoot().getName());
1035
                }
80 ilm 1036
                for (final SQLField field : t.getFields()) {
1037
                    vals.put(TABLE_COLNAME, added).put(FIELD_COLNAME, field.getName()).put(ISKEY_COLNAME, field.isKey()).insert();
67 ilm 1038
                }
80 ilm 1039
                vals.remove(ISKEY_COLNAME);
19 ilm 1040
            }
80 ilm 1041
            for (final SQLName added : ctxt.getAddedFieldsToExistingTables()) {
1042
                final SQLTable t = ctxt.getRoot().findTable(added.getItem(0));
1043
                final SQLField field = t.getField(added.getItem(1));
1044
                vals.put(TABLE_COLNAME, t.getName()).put(FIELD_COLNAME, field.getName()).put(ISKEY_COLNAME, field.isKey()).insert();
1045
            }
1046
            vals.remove(ISKEY_COLNAME);
1047
        }
1048
        // Always put true, even if getCreatedItems() is empty, since for now we can't be sure that
1049
        // the module didn't insert rows or otherwise changed the DB (MAYBE change SQLDataSource to
1050
        // hand out connections with read only user for a new ThreadGroup, or even no connections at
1051
        // all). If we could assert that the module didn't access at all the DB, we could add an
1052
        // option so that the module can declare not accessing the DB and install() would know that
1053
        // the DB version of the module is null. This could be beneficial since different users
1054
        // could install different version of modules that only change the UI.
1055
        final SQLRow moduleRow = setDBInstalledModule(factory.getReference(), true);
19 ilm 1056
 
80 ilm 1057
        // update dependencies
1058
        final SQLTable depT = getDepTable();
1059
        depT.getDBSystemRoot().getDataSource().execute("DELETE FROM " + depT.getSQLName().quote() + " WHERE " + new Where(depT.getField(NEEDING_MODULE_COLNAME), "=", moduleRow.getID()).getClause());
1060
        depT.fireTableModified(SQLRow.NONEXISTANT_ID);
1061
        final SQLRowValues vals = new SQLRowValues(depT).put(NEEDING_MODULE_COLNAME, moduleRow.getID());
1062
        final Map<ModuleReference, SQLRow> moduleRows = getDBInstalledModuleRowsByRef(null);
1063
        for (final ModuleFactory dep : graph.getDependencies(factory).values()) {
1064
            vals.put(NEEDED_MODULE_COLNAME, moduleRows.get(dep.getReference()).getID()).insertVerbatim();
1065
        }
1066
    }
19 ilm 1067
 
80 ilm 1068
    private void removeModuleFields(ModuleReference f) throws SQLException {
1069
        final SQLTable installedTable = getInstalledTable(getRoot());
1070
        final Where idW = new Where(installedTable.getField(MODULE_COLNAME), "=", f.getID());
1071
        installedTable.getDBSystemRoot().getDataSource()
1072
                .execute("DELETE FROM " + installedTable.getSQLName().quote() + " WHERE " + Where.isNotNull(installedTable.getField(TABLE_COLNAME)).and(idW).getClause());
1073
        setDBInstalledModule(f, false);
1074
 
1075
        // FWK_MODULE_DEP_TABLENAME rows removed with CASCADE
1076
        getDepTable().fireTableModified(SQLRow.NONEXISTANT_ID);
1077
    }
1078
 
1079
    /**
1080
     * Get the modules required because they have created a new table or new foreign key. E.g. if a
1081
     * module created a child table, as long as the table is in the database it needs to be archived
1082
     * along its parent.
1083
     *
1084
     * @return the modules.
1085
     * @throws SQLException if an error occurs.
1086
     */
1087
    final List<ModuleReference> getDBRequiredModules() throws SQLException {
1088
        // modules which have created a table or a key
1089
        final SQLTable installedTable = getInstalledTable(getRoot());
1090
        final AliasedTable installedTableVers = new AliasedTable(installedTable, "vers");
1091
        final SQLSelect sel = new SQLSelect();
1092
        sel.addSelect(installedTable.getField(MODULE_COLNAME));
1093
        // for each row, get the version from the main row
1094
        sel.addJoin("INNER", new Where(installedTable.getField(MODULE_COLNAME), "=", installedTableVers.getField(MODULE_COLNAME)).and(getModuleRowWhere(installedTableVers)));
1095
        sel.addSelect(installedTableVers.getField(MODULE_VERSION_COLNAME));
1096
 
1097
        final Where tableCreated = Where.isNotNull(installedTable.getField(TABLE_COLNAME)).and(Where.isNull(installedTable.getField(FIELD_COLNAME)));
1098
        final Where keyCreated = Where.isNotNull(installedTable.getField(FIELD_COLNAME)).and(new Where(installedTable.getField(ISKEY_COLNAME), "=", Boolean.TRUE));
1099
        sel.setWhere(tableCreated.or(keyCreated));
1100
        sel.addGroupBy(installedTable.getField(MODULE_COLNAME));
1101
        // allow to reference the field in the SELECT and shouldn't change anything since each
1102
        // module has only one version
1103
        sel.addGroupBy(installedTableVers.getField(MODULE_VERSION_COLNAME));
1104
        @SuppressWarnings("unchecked")
1105
        final List<Map<String, Object>> maps = (List<Map<String, Object>>) installedTable.getDBSystemRoot().getDataSource().execute(sel.asString());
1106
        final List<ModuleReference> res = new ArrayList<ModuleReference>(maps.size());
1107
        for (final Map<String, Object> m : maps) {
1108
            final String moduleID = (String) m.get(MODULE_COLNAME);
1109
            final ModuleVersion vers = new ModuleVersion(((Number) m.get(MODULE_VERSION_COLNAME)).longValue());
1110
            res.add(new ModuleReference(moduleID, vers));
1111
        }
1112
        L.config("getDBRequiredModules() found " + res);
1113
        return res;
1114
    }
1115
 
1116
    private void install(final AbstractModule module, final DepSolverGraph graph) throws Exception {
1117
        assert Thread.holdsLock(this);
1118
        final ModuleFactory factory = module.getFactory();
1119
        final ModuleVersion localVersion = getModuleVersionInstalledLocally(factory.getID());
1120
        final ModuleVersion lastInstalledVersion = getDBInstalledModuleVersion(factory.getID());
1121
        final ModuleVersion moduleVersion = module.getFactory().getVersion();
1122
        final boolean dbOK = moduleVersion.equals(lastInstalledVersion);
1123
 
1124
        if (!dbOK && !currentUserIsAdmin())
1125
            throw new IllegalStateException("Not allowed to install " + module.getFactory() + " in the database");
1126
 
1127
        if (lastInstalledVersion != null && moduleVersion.compareTo(lastInstalledVersion) < 0)
1128
            throw new IllegalArgumentException("Module older than the one installed in the DB : " + moduleVersion + " < " + lastInstalledVersion);
1129
        if (localVersion != null && moduleVersion.compareTo(localVersion) < 0)
1130
            throw new IllegalArgumentException("Module older than the one installed locally : " + moduleVersion + " < " + localVersion);
1131
        if (!moduleVersion.equals(localVersion) || !dbOK) {
1132
            // local
1133
            final File localDir = getLocalDirectory(factory.getID());
1134
            // There are 2 choices to handle the update of files :
1135
            // 1. copy dir to a new one and pass it to DBContext, then either rename it to dir or
1136
            // rename it failed
1137
            // 2. copy dir to a backup, pass dir to DBContext, then either remove backup or rename
1138
            // it to dir
1139
            // Choice 2 is simpler since the module deals with the same directory in both install()
1140
            // and start()
156 ilm 1141
            final Path backupDir;
80 ilm 1142
            // check if we need a backup
1143
            if (localDir.exists()) {
156 ilm 1144
                backupDir = getLocalBackupDirectory(factory.getID());
1145
                FileUtils.rm_R(backupDir, false);
1146
                Files.createDirectories(backupDir.getParent());
1147
                FileUtils.copyDirectory(localDir.toPath(), backupDir, false, StandardCopyOption.COPY_ATTRIBUTES);
80 ilm 1148
            } else {
1149
                backupDir = null;
1150
                FileUtils.mkdir_p(localDir);
1151
            }
1152
            assert localDir.exists();
1153
            try {
1154
                SQLUtils.executeAtomic(getDS(), new ConnectionHandlerNoSetup<Object, IOException>() {
1155
                    @Override
1156
                    public Object handle(SQLDataSource ds) throws SQLException, IOException {
1157
                        final Tuple2<Set<String>, Set<SQLName>> alreadyCreatedItems = getCreatedItems(factory.getID());
156 ilm 1158
                        final DBContext ctxt = new DBContext(getLocalDataSubDir(localDir), localVersion, getRoot(), lastInstalledVersion, alreadyCreatedItems.get0(), alreadyCreatedItems.get1(),
1159
                                getDirectory());
80 ilm 1160
                        // install local (i.e. ctxt stores the actions to carry on the DB)
1161
                        // TODO pass a data source with no rights to modify the data definition (or
1162
                        // even no rights to modify the data if DB version is up to date)
1163
                        module.install(ctxt);
1164
                        if (!localDir.exists())
1165
                            throw new IOException("Modules shouldn't remove their directory");
1166
                        // install in DB
151 ilm 1167
                        ctxt.executeSQL();
80 ilm 1168
                        updateModuleFields(factory, graph, ctxt);
1169
                        return null;
25 ilm 1170
                    }
80 ilm 1171
                });
1172
            } catch (Exception e) {
1173
                // install did not complete successfully
1174
                if (getRoot().getServer().getSQLSystem() == SQLSystem.MYSQL)
1175
                    L.warning("MySQL cannot rollback DDL statements");
1176
                // keep failed install files and restore previous files
156 ilm 1177
                final Path failed = getLocalFailedDirectory(factory.getID());
1178
                boolean moved = false;
1179
                try {
1180
                    FileUtils.rm_R(failed, false);
1181
                    Files.createDirectories(failed.getParent());
1182
                    Files.move(localDir.toPath(), failed);
1183
                    final String errorMsg = "Couldn't install " + module + " :\n" + ExceptionUtils.getStackTrace(e);
1184
                    // TODO as in getLocalVersionFile(), separate internal state (i.e. version and
1185
                    // error) from module local data
1186
                    Files.write(getInternalStateSubDir(failed).resolve("Install error.txt"), errorMsg.getBytes(StandardCharsets.UTF_8));
1187
                    moved = true;
1188
                } catch (Exception e1) {
1189
                    L.log(Level.WARNING, "Couldn't move " + localDir + " to " + failed, e1);
1190
                }
1191
                // restore if needed
1192
                if (moved && backupDir != null) {
80 ilm 1193
                    assert !localDir.exists();
156 ilm 1194
                    try {
1195
                        Files.move(backupDir, localDir.toPath());
1196
                    } catch (Exception e1) {
1197
                        L.log(Level.WARNING, "Couldn't restore " + backupDir + " to " + localDir, e1);
1198
                    }
18 ilm 1199
                }
80 ilm 1200
                throw e;
25 ilm 1201
            }
80 ilm 1202
            // DB transaction was committed, remove backup files
1203
            assert localDir.exists();
1204
            if (backupDir != null)
1205
                FileUtils.rm_R(backupDir);
1206
            setModuleInstalledLocally(factory.getReference(), true);
18 ilm 1207
        }
80 ilm 1208
        assert moduleVersion.equals(getModuleVersionInstalledLocally(factory.getID())) && moduleVersion.equals(getDBInstalledModuleVersion(factory.getID()));
18 ilm 1209
    }
1210
 
151 ilm 1211
    private void registerSQLElements(final AbstractModule module, Map<SQLTable, SQLElement> beforeElements) throws IOException {
80 ilm 1212
        final ModuleReference id = module.getFactory().getReference();
25 ilm 1213
        synchronized (this.modulesElements) {
80 ilm 1214
            // perhaps check that no other version of the module has been registered
25 ilm 1215
            if (!this.modulesElements.containsKey(id)) {
1216
                final SQLElementDirectory dir = getDirectory();
1217
                module.setupElements(dir);
80 ilm 1218
                final IdentityHashMap<SQLElement, SQLElement> elements = new IdentityHashMap<SQLElement, SQLElement>();
25 ilm 1219
                // use IdentitySet so as not to call equals() since it triggers initFF()
1220
                final IdentitySet<SQLElement> beforeElementsSet = new IdentityHashSet<SQLElement>(beforeElements.values());
83 ilm 1221
                // copy to be able to restore elements while iterating
1222
                final IdentitySet<SQLElement> afterElementsSet = new IdentityHashSet<SQLElement>(dir.getElements());
1223
                for (final SQLElement elem : afterElementsSet) {
25 ilm 1224
                    if (!beforeElementsSet.contains(elem)) {
80 ilm 1225
                        if (!(elem instanceof ModuleElement))
1226
                            L.warning("Module added an element that isn't a ModuleElement : " + elem);
25 ilm 1227
                        if (beforeElements.containsKey(elem.getTable())) {
80 ilm 1228
                            final SQLElement replacedElem = beforeElements.get(elem.getTable());
1229
                            // Code safety : a module can make sure that its elements won't be
1230
                            // replaced. We thus require that elem is a subclass of replacedElem,
1231
                            // i.e. a module can use standard java access rules (e.g. package
1232
                            // private constructor, final method).
1233
                            final boolean codeSafe = replacedElem.getClass().isInstance(elem);
1234
 
83 ilm 1235
                            final boolean mngrSafe = isMngrSafe(module, replacedElem);
1236
                            if (codeSafe && mngrSafe) {
80 ilm 1237
                                // store replacedElem so that it can be restored in unregister()
1238
                                elements.put(elem, replacedElem);
1239
                            } else {
83 ilm 1240
                                final List<String> pbs = new ArrayList<String>(2);
1241
                                if (!codeSafe)
1242
                                    pbs.add(elem + " isn't a subclass of " + replacedElem);
1243
                                if (!mngrSafe)
1244
                                    pbs.add(module + " doesn't depend on " + replacedElem);
1245
                                L.warning("Trying to replace element for " + elem.getTable() + " with " + elem + " but\n" + CollectionUtils.join(pbs, "\n"));
80 ilm 1246
                                dir.addSQLElement(replacedElem);
1247
                            }
25 ilm 1248
                        } else {
80 ilm 1249
                            elements.put(elem, null);
25 ilm 1250
                        }
1251
                    }
19 ilm 1252
                }
80 ilm 1253
 
156 ilm 1254
                // Load translations after registering elements since SQLFieldTranslator needs them.
1255
                // If setupElements() needs translations then perhaps we should store translations
1256
                // with the element code, without needing the element.
1257
                final String mdVariant = getMDVariant(module.getFactory());
1258
                final Set<SQLTable> tablesWithMD = loadTranslations(getConf().getTranslator(), module, mdVariant);
1259
 
80 ilm 1260
                // insert just loaded labels into the search path
1261
                for (final SQLTable tableWithDoc : tablesWithMD) {
1262
                    final SQLElement sqlElem = this.getDirectory().getElement(tableWithDoc);
1263
                    if (sqlElem == null)
1264
                        throw new IllegalStateException("Missing element for table with metadata : " + tableWithDoc);
1265
                    // avoid duplicates
1266
                    final boolean already = sqlElem instanceof ModuleElement && ((ModuleElement) sqlElem).getFactory() == module.getFactory();
1267
                    if (!already)
1268
                        sqlElem.addToMDPath(mdVariant);
1269
                }
1270
 
25 ilm 1271
                this.modulesElements.put(id, elements);
19 ilm 1272
            }
1273
        }
1274
    }
1275
 
80 ilm 1276
    // Manager safety : when a module is unregistered, replacedElem can be restored. We thus require
1277
    // that replacedElem was registered by one of our dependencies (or by the core application),
1278
    // forcing a predictable order (or error if two unrelated modules want to replace the same
1279
    // element).
1280
    // FIXME modules are only unregistered when uninstalled (e.g. to know how to archive additional
1281
    // fields), so even though a module is stopped its UI (getName(), getComboRequest(),
1282
    // createComponent()) will still be used.
1283
    private boolean isMngrSafe(final AbstractModule module, final SQLElement replacedElem) {
1284
        final boolean mngrSafe;
1285
        final ModuleReference moduleForElement = getModuleForElement(replacedElem);
1286
        if (moduleForElement == null) {
1287
            // module from core app
1288
            mngrSafe = true;
1289
        } else {
1290
            // MAYBE handle non direct dependency
1291
            final ModuleFactory replacedFactory = this.factories.getFactory(moduleForElement);
1292
            mngrSafe = this.dependencyGraph.containsEdge(module.getFactory(), replacedFactory);
1293
        }
1294
        return mngrSafe;
1295
    }
1296
 
1297
    private final ModuleReference getModuleForElement(final SQLElement elem) {
1298
        synchronized (this.modulesElements) {
1299
            for (final Entry<ModuleReference, IdentityHashMap<SQLElement, SQLElement>> e : this.modulesElements.entrySet()) {
1300
                final IdentityHashMap<SQLElement, SQLElement> map = e.getValue();
1301
                assert map instanceof IdentityHashMap : "identity needed but got " + map.getClass();
1302
                if (map.containsKey(elem))
1303
                    return e.getKey();
1304
            }
1305
        }
1306
        return null;
1307
    }
1308
 
1309
    public final Set<ModuleReference> getRegisteredModules() {
1310
        synchronized (this.modulesElements) {
1311
            return new HashSet<ModuleReference>(this.modulesElements.keySet());
1312
        }
1313
    }
1314
 
151 ilm 1315
    final Set<SQLElement> getRegisteredElements(final ModuleReference ref) {
1316
        synchronized (this.modulesElements) {
1317
            final IdentityHashMap<SQLElement, SQLElement> map = this.modulesElements.get(ref);
1318
            if (map == null || map.isEmpty())
1319
                return Collections.emptySet();
1320
            return Collections.unmodifiableSet(new HashSet<SQLElement>(map.keySet()));
1321
        }
1322
    }
1323
 
80 ilm 1324
    private void setupComponents(final AbstractModule module, final Tuple2<Set<String>, Set<SQLName>> alreadyCreatedItems, final MenuAndActions ma) throws SQLException {
61 ilm 1325
        assert SwingUtilities.isEventDispatchThread();
19 ilm 1326
        final String id = module.getFactory().getID();
1327
        if (!this.modulesComponents.containsKey(id)) {
25 ilm 1328
            final SQLElementDirectory dir = getDirectory();
80 ilm 1329
            final ComponentsContext ctxt = new ComponentsContext(dir, getRoot(), alreadyCreatedItems.get0(), alreadyCreatedItems.get1());
19 ilm 1330
            module.setupComponents(ctxt);
177 ilm 1331
            TranslationManager.addTranslationStreamFromClass(module.getClass());
73 ilm 1332
            this.setupMenu(module, ma);
19 ilm 1333
            this.modulesComponents.put(id, ctxt);
1334
        }
1335
    }
1336
 
80 ilm 1337
    final List<ModuleReference> getAdminRequiredModules() throws IOException {
1338
        return this.getAdminRequiredModules(false);
1339
    }
67 ilm 1340
 
80 ilm 1341
    /**
1342
     * Get the modules required by the administrator.
1343
     *
1344
     * @param refresh <code>true</code> if the cache should be refreshed.
1345
     * @return the references.
1346
     * @throws IOException if an error occurs.
1347
     */
1348
    final List<ModuleReference> getAdminRequiredModules(final boolean refresh) throws IOException {
1349
        final Preferences prefs = getRequiredIDsPrefs();
1350
        if (refresh) {
1351
            try {
1352
                prefs.sync();
1353
            } catch (BackingStoreException e) {
1354
                // hide exception with a more common one
1355
                throw new IOException("Couldn't sync preferences", e);
67 ilm 1356
            }
1357
        }
80 ilm 1358
        final List<ModuleReference> res = getRefs(prefs);
1359
        L.config("getAdminRequiredModules() found " + res);
1360
        return res;
1361
    }
1362
 
1363
    private final boolean isAdminRequired(ModuleReference ref) {
1364
        final long version = ref.getVersion().getMerged();
1365
        assert version >= MIN_VERSION;
1366
        return version == getRequiredIDsPrefs().getLong(ref.getID(), MIN_VERSION - 1);
1367
    }
1368
 
1369
    final void setAdminRequiredModules(final Set<ModuleReference> refs, final boolean required) throws BackingStoreException {
1370
        final Set<ModuleReference> emptySet = Collections.<ModuleReference> emptySet();
1371
        setAdminRequiredModules(required ? refs : emptySet, !required ? refs : emptySet);
1372
    }
1373
 
1374
    /**
1375
     * Change which modules are required. This also {@link Preferences#sync()} the preferences if
1376
     * they are modified.
1377
     *
1378
     * @param requiredRefs the modules required.
1379
     * @param notRequiredRefs the modules not required.
1380
     * @throws BackingStoreException if an error occurs.
1381
     * @see #getAdminRequiredModules(boolean)
1382
     */
1383
    final void setAdminRequiredModules(final Set<ModuleReference> requiredRefs, final Set<ModuleReference> notRequiredRefs) throws BackingStoreException {
1384
        if (requiredRefs.size() + notRequiredRefs.size() == 0)
1385
            return;
1386
        if (!currentUserIsAdmin())
1387
            throw new IllegalStateException("Not allowed to not require " + notRequiredRefs + " and to require " + requiredRefs);
1388
        final Preferences prefs = getRequiredIDsPrefs();
1389
        putRefs(prefs, requiredRefs);
1390
        for (final ModuleReference ref : notRequiredRefs) {
1391
            prefs.remove(ref.getID());
67 ilm 1392
        }
80 ilm 1393
        prefs.sync();
1394
    }
67 ilm 1395
 
80 ilm 1396
    public final void startRequiredModules() throws Exception {
1397
        // use NO_CHANGE as installation should have been handled in init()
1398
        startModules(getAdminRequiredModules(), NoChoicePredicate.NO_CHANGE, false);
27 ilm 1399
    }
1400
 
80 ilm 1401
    static private final List<ModuleReference> getRefs(final Preferences prefs) throws IOException {
1402
        final String[] ids;
1403
        try {
1404
            ids = prefs.keys();
1405
        } catch (BackingStoreException e) {
1406
            // hide exception with a more common one
1407
            throw new IOException("Couldn't access preferences", e);
1408
        }
1409
        final List<ModuleReference> refs = new ArrayList<ModuleReference>(ids.length);
1410
        for (final String id : ids) {
1411
            final long merged = prefs.getLong(id, MIN_VERSION - 1);
1412
            refs.add(new ModuleReference(id, merged < MIN_VERSION ? null : new ModuleVersion(merged)));
1413
        }
1414
        return refs;
1415
    }
1416
 
1417
    static private final void putRefs(final Preferences prefs, final Collection<ModuleReference> refs) throws BackingStoreException {
1418
        for (final ModuleReference ref : refs) {
1419
            prefs.putLong(ref.getID(), ref.getVersion().getMerged());
1420
        }
1421
        prefs.flush();
1422
    }
1423
 
61 ilm 1424
    /**
1425
     * Start modules that were deemed persistent.
1426
     *
1427
     * @throws Exception if an error occurs.
1428
     * @see #startModules(Collection, boolean)
1429
     * @see #stopModule(String, boolean)
1430
     */
25 ilm 1431
    public final void startPreviouslyRunningModules() throws Exception {
80 ilm 1432
        final List<ModuleReference> ids = getRefs(getRunningIDsPrefs());
1433
        L.config("startPreviouslyRunningModules() found " + ids);
1434
        startModules(ids, NoChoicePredicate.ONLY_INSTALL_ARGUMENTS, false);
18 ilm 1435
    }
1436
 
1437
    public final boolean startModule(final String id) throws Exception {
1438
        return this.startModule(id, true);
1439
    }
1440
 
1441
    public final boolean startModule(final String id, final boolean persistent) throws Exception {
80 ilm 1442
        return this.startModule(new ModuleReference(id, null), persistent);
18 ilm 1443
    }
1444
 
80 ilm 1445
    public final boolean startModule(final ModuleReference id, final boolean persistent) throws Exception {
1446
        return this.startModule(id, NoChoicePredicate.ONLY_INSTALL_ARGUMENTS, persistent);
1447
    }
1448
 
1449
    // return true if the module is now (or at least submitted to invokeLater()) started (even if
1450
    // the module wasn't started by this method)
1451
    public final boolean startModule(final ModuleReference id, final NoChoicePredicate noChoicePredicate, final boolean persistent) throws Exception {
1452
        final Set<ModuleReference> notStarted = startModules(Collections.singleton(id), noChoicePredicate, persistent);
1453
        final boolean res = notStarted.isEmpty();
1454
        assert res == this.runningModules.containsKey(id.getID());
1455
        return res;
1456
    }
1457
 
61 ilm 1458
    /**
1459
     * Start the passed modules. If this method is called outside of the EDT the modules will be
1460
     * actually started using {@link SwingUtilities#invokeLater(Runnable)}, thus code that needs the
1461
     * module to be actually started must also be called inside an invokeLater().
1462
     *
1463
     * @param ids which modules to start.
80 ilm 1464
     * @param noChoicePredicate which modules are allowed to be installed or removed.
61 ilm 1465
     * @param persistent <code>true</code> to start them the next time the application is launched,
1466
     *        see {@link #startPreviouslyRunningModules()}.
80 ilm 1467
     * @return the not started modules.
61 ilm 1468
     * @throws Exception if an error occurs.
1469
     */
80 ilm 1470
    public synchronized final Set<ModuleReference> startModules(final Collection<ModuleReference> ids, final NoChoicePredicate noChoicePredicate, final boolean persistent) throws Exception {
1471
        // since we ask to start ids, only the not created are not started
1472
        return this.createModules(ids, noChoicePredicate, ModuleState.STARTED, persistent).get1().getNotCreated();
1473
    }
1474
 
1475
    // ATTN versions are ignored (this.runningModules is used)
1476
    synchronized Set<ModuleReference> setPersistentModules(final Collection<ModuleReference> ids) throws BackingStoreException {
1477
        Map<String, ModuleVersion> modulesInstalled = null;
1478
        final Set<ModuleReference> toSet = new HashSet<ModuleReference>();
1479
        for (final ModuleReference ref : ids) {
1480
            // use installedRef, since ids can contain null version
1481
            final ModuleReference installedRef;
1482
 
1483
            // first cheap in-memory check with this.runningModules
1484
            final AbstractModule m = this.runningModules.get(ref.getID());
1485
            if (m != null) {
1486
                installedRef = m.getFactory().getReference();
1487
            } else {
1488
                // else check with local file system
1489
                if (modulesInstalled == null)
1490
                    modulesInstalled = getModulesVersionInstalledLocally();
1491
                installedRef = new ModuleReference(ref.getID(), modulesInstalled.get(ref.getID()));
18 ilm 1492
            }
80 ilm 1493
            if (installedRef.getVersion() != null) {
1494
                toSet.add(installedRef);
1495
            }
18 ilm 1496
        }
80 ilm 1497
        putRefs(getRunningIDsPrefs(), toSet);
1498
        return toSet;
18 ilm 1499
    }
1500
 
80 ilm 1501
    static public enum InvalidRef {
1502
        /**
1503
         * The reference has no available factory.
1504
         */
1505
        NO_FACTORY,
1506
        /**
1507
         * The reference conflicts with another reference in the same call.
1508
         */
1509
        SELF_CONFLICT,
1510
        /**
1511
         * The reference cannot be installed (e.g. missing dependency, cycle...).
1512
         */
1513
        NO_SOLUTION
18 ilm 1514
    }
1515
 
80 ilm 1516
    // the pool to use
1517
    // refs that could be installed
1518
    // refs that cannot be installed
1519
    // => instances of <code>refs</code> not returned are duplicates or references without version
1520
    private final Tuple3<FactoriesByID, List<ModuleReference>, SetMap<InvalidRef, ModuleReference>> resolveRefs(final Collection<ModuleReference> refs) {
1521
        // remove duplicates
1522
        final Set<ModuleReference> refsSet = new HashSet<ModuleReference>(refs);
1523
        // only keep references without version if no other specifies a version
1524
        final Set<String> nonNullVersions = new HashSet<String>();
1525
        for (final ModuleReference ref : refsSet) {
1526
            if (ref.getVersion() != null)
1527
                nonNullVersions.add(ref.getID());
1528
        }
1529
        // refs with only one factory (either specifying one version or with only one version
1530
        // available)
1531
        final List<ModuleFactory> factories = new ArrayList<ModuleFactory>();
1532
        final List<ModuleReference> atLeast1 = new ArrayList<ModuleReference>();
1533
        final Iterator<ModuleReference> iter = refsSet.iterator();
1534
        while (iter.hasNext()) {
1535
            final ModuleReference ref = iter.next();
1536
            if (ref.getVersion() == null && nonNullVersions.contains(ref.getID())) {
1537
                // only use the reference that specifies a version
1538
                iter.remove();
1539
            } else {
1540
                final List<ModuleFactory> factoriesForRef = this.factories.getFactories(ref);
1541
                final int size = factoriesForRef.size();
1542
                if (size > 0) {
1543
                    iter.remove();
1544
                    atLeast1.add(ref);
1545
                    if (size == 1) {
1546
                        factories.add(factoriesForRef.get(0));
1547
                    }
67 ilm 1548
                }
18 ilm 1549
            }
1550
        }
80 ilm 1551
        final SetMap<InvalidRef, ModuleReference> invalidRefs = new SetMap<InvalidRef, ModuleReference>(Mode.NULL_FORBIDDEN);
1552
        invalidRefs.putCollection(InvalidRef.NO_FACTORY, refsSet);
1553
 
1554
        final FactoriesByID fByID = this.copyFactories();
1555
        // conflicts with requested references
1556
        final Set<ModuleFactory> conflicts = fByID.getConflicts(factories);
1557
        final Collection<ModuleFactory> selfConflicts = CollectionUtils.intersection(factories, conflicts);
1558
        for (final ModuleFactory f : selfConflicts) {
1559
            invalidRefs.add(InvalidRef.SELF_CONFLICT, f.getReference());
1560
            // don't bother trying
1561
            atLeast1.remove(f.getReference());
1562
        }
1563
        fByID.removeAll(conflicts);
1564
        // make sure that the pool is coherent with the solving graph
1565
        fByID.addAll(this.dependencyGraph.vertexSet());
1566
        return Tuple3.create(fByID, atLeast1, invalidRefs);
1567
    }
1568
 
1569
    /**
1570
     * Allow to create modules without user interaction.
1571
     *
1572
     * @author Sylvain
1573
     */
1574
    static enum NoChoicePredicate {
1575
        /** No install, no uninstall */
1576
        NO_CHANGE,
1577
        /**
1578
         * No uninstall, only install passed modules.
1579
         */
1580
        ONLY_INSTALL_ARGUMENTS,
1581
        /**
1582
         * No uninstall, only install passed modules and their dependencies.
1583
         */
1584
        ONLY_INSTALL
1585
    }
1586
 
1587
    synchronized private final DepSolver createSolver(final int maxCount, final NoChoicePredicate s, final Collection<ModuleReference> ids) throws Exception {
1588
        final InstallationState installState = new InstallationState(this);
1589
        final DepSolver depSolver = new DepSolver().setMaxSuccess(maxCount);
1590
        depSolver.setResultFactory(new Factory() {
1591
            @Override
1592
            public DepSolverResult create(DepSolverResult parent, int tryCount, String error, DepSolverGraph graph) {
1593
                final DepSolverResultMM res = new DepSolverResultMM((DepSolverResultMM) parent, tryCount, error, graph);
1594
                res.init(ModuleManager.this, installState, s, ids);
1595
                return res;
67 ilm 1596
            }
80 ilm 1597
        });
144 ilm 1598
        depSolver.setResultFilter(DepSolverResultMM.VALID_PRED);
80 ilm 1599
        return depSolver;
1600
    }
1601
 
1602
    synchronized final Tuple2<Solutions, ModulesStateChangeResult> createModules(final Collection<ModuleReference> ids, final NoChoicePredicate s, final ModuleState targetState) throws Exception {
1603
        return this.createModules(ids, s, targetState, checkPersistentNeeded(targetState));
1604
    }
1605
 
1606
    // allow to not pass unneeded argument
1607
    private boolean checkPersistentNeeded(final ModuleState targetState) {
1608
        if (targetState.compareTo(ModuleState.STARTED) >= 0)
1609
            throw new IllegalArgumentException("For STARTED the persistent parameter must be supplied");
1610
        return false;
1611
    }
1612
 
1613
    // not public since it returns instance of AbstractModules
1614
    synchronized final Tuple2<Solutions, ModulesStateChangeResult> createModules(final Collection<ModuleReference> ids, final NoChoicePredicate s, final ModuleState targetState,
1615
            final boolean startPersistent) throws Exception {
1616
        // Don't uninstall automatically, use getSolutions() then applyChange()
1617
        if (s == null)
1618
            throw new NullPointerException();
1619
        if (ids.size() == 0 || targetState == ModuleState.NOT_CREATED)
1620
            return Tuple2.create(Solutions.EMPTY, ModulesStateChangeResult.empty());
1621
 
1622
        final DepSolver depSolver = createSolver(1, s, ids);
1623
        final Solutions solutions = getSolutions(depSolver, ids);
1624
        final SetMap<InvalidRef, ModuleReference> cannotCreate = solutions.getNotSolvedReferences();
1625
        final ModulesStateChangeResult changeRes;
1626
        // don't partially install
1627
        if (cannotCreate != null && !cannotCreate.isEmpty()) {
1628
            changeRes = ModulesStateChangeResult.noneCreated(new HashSet<ModuleReference>(ids));
1629
        } else {
1630
            // at least one solution otherwise cannotCreate wouldn't be empty
1631
            changeRes = this.applyChange((DepSolverResultMM) solutions.getSolutions().get(0), targetState, startPersistent);
61 ilm 1632
        }
80 ilm 1633
        return Tuple2.create(solutions, changeRes);
1634
    }
18 ilm 1635
 
80 ilm 1636
    synchronized final Solutions getSolutions(final Collection<ModuleReference> ids, final int maxCount) throws Exception {
1637
        return this.getSolutions(createSolver(maxCount, null, ids), ids);
1638
    }
67 ilm 1639
 
80 ilm 1640
    synchronized private final Solutions getSolutions(final DepSolver depSolver, final Collection<ModuleReference> ids) throws Exception {
1641
        if (ids.size() == 0)
1642
            return Solutions.EMPTY;
67 ilm 1643
 
80 ilm 1644
        final Tuple3<FactoriesByID, List<ModuleReference>, SetMap<InvalidRef, ModuleReference>> resolvedRefs = resolveRefs(ids);
1645
        final FactoriesByID pool = resolvedRefs.get0();
1646
        final List<ModuleReference> atLeast1 = resolvedRefs.get1();
1647
        final SetMap<InvalidRef, ModuleReference> invalidRefs = resolvedRefs.get2();
61 ilm 1648
 
80 ilm 1649
        final List<DepSolverResult> solutions;
1650
        if (atLeast1.isEmpty()) {
1651
            // we were passed non empty references to install but no candidates remain. If we passed
1652
            // an empty list to DepSolver it will immediately return successfully.
1653
            solutions = Collections.emptyList();
1654
        } else {
1655
            solutions = depSolver.solve(pool, this.dependencyGraph, atLeast1);
1656
        }
1657
        if (solutions.size() == 0) {
1658
            invalidRefs.putCollection(InvalidRef.NO_SOLUTION, atLeast1);
1659
        }
1660
        invalidRefs.removeAllEmptyCollections();
1661
        return new Solutions(invalidRefs, solutions.size() == 0 ? Collections.<ModuleReference> emptyList() : atLeast1, solutions);
1662
    }
61 ilm 1663
 
80 ilm 1664
    synchronized final ModulesStateChangeResult applyChange(final ModulesStateChange change, final ModuleState targetState) throws Exception {
1665
        return applyChange(change, targetState, checkPersistentNeeded(targetState));
1666
    }
1667
 
1668
    // not public since it returns instances of AbstractModule
1669
    // @param targetState target state for modules in graph
1670
    // @param startPersistent only used if <code>targetState</code> is STARTED
1671
    synchronized final ModulesStateChangeResult applyChange(final ModulesStateChange change, final ModuleState targetState, final boolean startPersistent) throws Exception {
1672
        if (change == null || change.getError() != null) {
1673
            return null;
1674
        } else if (!new InstallationState(this).equals(change.getInstallState())) {
1675
            throw new IllegalStateException("Installation state has changed since getSolutions()");
1676
        }
1677
 
151 ilm 1678
        // call it before stopping/uninstalling
1679
        final boolean exit = this.isExitAllowed() && this.needExit(change);
1680
 
156 ilm 1681
        final DepSolverGraph graph = change.getGraph();
80 ilm 1682
        final Set<ModuleReference> toRemove = change.getReferencesToRemove();
1683
        final Set<ModuleReference> removed;
1684
        if (toRemove.size() > 0) {
149 ilm 1685
            final Set<String> idsToInstall = change.getIDsToInstall();
1686
 
80 ilm 1687
            removed = new HashSet<ModuleReference>();
1688
            for (final ModuleReference ref : toRemove) {
149 ilm 1689
                // don't uninstall modules to upgrade but since this loop might uninstall modules
1690
                // needed by ref, at least stop it like uninstallUnsafe() does
1691
                if (idsToInstall.contains(ref.getID()))
1692
                    this.stopModule(ref.getID(), false);
156 ilm 1693
                else if (this.uninstallUnsafe(ref, !change.forceRemove(), change.getInstallState()))
80 ilm 1694
                    removed.add(ref);
61 ilm 1695
            }
80 ilm 1696
        } else {
1697
            removed = Collections.emptySet();
1698
        }
1699
 
1700
        // MAYBE compare states with targetState to avoid going further (e.g ids are all started)
1701
 
151 ilm 1702
        if (exit) {
80 ilm 1703
            // restart to make sure the uninstalled modules are really gone from the memory and
1704
            // none of its effects present. We could check that the class loader for the module
1705
            // is garbage collected, but
1706
            // 1. this cannot work if the module is in the class path
1707
            // 2. an ill-behaved modules might have modified a static value
149 ilm 1708
            assert noDisplayableFrame() : "A change needs to exit but there still a displayable frame : " + change;
80 ilm 1709
            final Set<ModuleReference> toInstall = change.getReferencesToInstall();
1710
            // don't use only getReferencesToInstall() as even if no modules need installing, their
1711
            // state might need to change (e.g. start)
1712
            if (toInstall.size() > 0 || (targetState.compareTo(ModuleState.INSTALLED) > 0 && change.getUserReferencesToInstall().size() > 0)) {
1713
                // record current time and actions
1714
                final File f = getToInstallFile();
156 ilm 1715
                try (final XMLEncoder xmlEncoder = new XMLEncoder(new FileOutputStream(f))) {
80 ilm 1716
                    xmlEncoder.setExceptionListener(XMLCodecUtils.EXCEPTION_LISTENER);
1717
                    xmlEncoder.setPersistenceDelegate(ModuleVersion.class, ModuleVersion.PERSIST_DELEGATE);
1718
                    xmlEncoder.setPersistenceDelegate(ModuleReference.class, ModuleReference.PERSIST_DELEGATE);
1719
                    xmlEncoder.writeObject(TO_INSTALL_VERSION);
1720
                    xmlEncoder.writeObject(new Date());
1721
                    xmlEncoder.writeObject(toInstall);
1722
                    xmlEncoder.writeObject(change.getUserReferencesToInstall());
1723
                    xmlEncoder.writeObject(targetState);
1724
                    xmlEncoder.writeObject(startPersistent);
1725
                } catch (Exception e) {
1726
                    // try to delete invalid file before throwing exception
156 ilm 1727
                    // "any catch or finally block is run after the resources have been closed."
80 ilm 1728
                    f.delete();
1729
                    throw e;
73 ilm 1730
                }
80 ilm 1731
            }
156 ilm 1732
            return new ModulesStateChangeResult(removed, change.getReferencesToInstall(), graph, Collections.emptyMap());
18 ilm 1733
        }
1734
 
80 ilm 1735
        // don't use getReferencesToInstall() as even if no modules need installing, their state
1736
        // might need to change (e.g. start)
1737
        if (targetState.compareTo(ModuleState.CREATED) < 0)
1738
            return ModulesStateChangeResult.onlyRemoved(removed);
1739
 
1740
        if (graph == null)
156 ilm 1741
            throw new IllegalArgumentException("target state is " + targetState + " but no graph was provided by " + change);
80 ilm 1742
 
1743
        // modules created by this method
1744
        final Map<ModuleReference, AbstractModule> modules = new LinkedHashMap<ModuleReference, AbstractModule>(graph.getFactories().size());
1745
        // MAYBE try to continue even if some modules couldn't be created
1746
        final Set<ModuleReference> cannotCreate = Collections.emptySet();
1747
 
1748
        final List<AbstractModule> toStart = new ArrayList<AbstractModule>();
1749
 
1750
        for (final ModuleFactory useableFactory : graph.flatten()) {
1751
            final String id = useableFactory.getID();
1752
            // already created
1753
            if (!this.dependencyGraph.containsVertex(useableFactory)) {
1754
                final Map<Object, ModuleFactory> dependenciesFactory = graph.getDependencies(useableFactory);
1755
                final Map<Object, AbstractModule> dependenciesModule = new HashMap<Object, AbstractModule>(dependenciesFactory.size());
1756
                for (final Entry<Object, ModuleFactory> e : dependenciesFactory.entrySet()) {
1757
                    final AbstractModule module = this.createdModules.get(e.getValue());
1758
                    assert module != null;
1759
                    dependenciesModule.put(e.getKey(), module);
1760
                }
156 ilm 1761
                final AbstractModule createdModule = useableFactory.createModule(this.getLocalDataDirectory(id), Collections.unmodifiableMap(dependenciesModule));
80 ilm 1762
                modules.put(useableFactory.getReference(), createdModule);
1763
                this.createdModules.put(useableFactory, createdModule);
1764
 
1765
                // update graph
1766
                final boolean added = this.dependencyGraph.addVertex(useableFactory);
1767
                assert added : "Module was already in graph : " + useableFactory;
1768
                for (final Entry<Object, ModuleFactory> e : dependenciesFactory.entrySet()) {
1769
                    this.dependencyGraph.addEdge(useableFactory, e.getKey(), e.getValue());
1770
                }
1771
            }
1772
            // even if the module was created in a previous invocation, it might not have been
1773
            // started then
1774
            if (!this.runningModules.containsKey(id))
1775
                toStart.add(this.createdModules.get(useableFactory));
1776
        }
1777
 
1778
        // don't test toStart emptiness as even if all modules were started, they might need to be
1779
        // made persistent
1780
        if (targetState.compareTo(ModuleState.INSTALLED) >= 0) {
149 ilm 1781
            // register each module just after install, so that the next module can use its elements
1782
            // in its install
1783
            for (final AbstractModule module : toStart) {
80 ilm 1784
                installAndRegister(module, graph);
149 ilm 1785
            }
80 ilm 1786
 
1787
            if (targetState == ModuleState.STARTED) {
1788
                start(toStart);
1789
                if (startPersistent)
1790
                    // only mark persistent passed modules (not their dependencies)
1791
                    this.setPersistentModules(change.getUserReferencesToInstall());
1792
            }
1793
        }
1794
 
1795
        // ATTN modules indexed by resolved references, not the ones passed
1796
        return new ModulesStateChangeResult(removed, cannotCreate, graph, modules);
18 ilm 1797
    }
1798
 
80 ilm 1799
    synchronized final void startFactories(final List<ModuleFactory> toStart) throws Exception {
1800
        final List<AbstractModule> modules = new ArrayList<AbstractModule>(toStart.size());
1801
        for (final ModuleFactory f : toStart) {
1802
            final AbstractModule m = this.createdModules.get(f);
1803
            if (m == null)
1804
                throw new IllegalStateException("Not created : " + f);
1805
            else if (!this.isModuleRunning(f.getID()))
1806
                modules.add(m);
1807
        }
1808
        this.start(modules);
1809
    }
67 ilm 1810
 
80 ilm 1811
    synchronized private final void start(final List<AbstractModule> toStart) throws Exception {
1812
        if (toStart.size() == 0)
1813
            return;
1814
        // check install state before starting
1815
        final Set<ModuleReference> registeredModules = this.getRegisteredModules();
1816
        for (final AbstractModule m : toStart) {
1817
            final ModuleReference ref = m.getFactory().getReference();
1818
            if (!registeredModules.contains(ref))
1819
                throw new IllegalStateException("Not installed and registered : " + ref);
1820
        }
1821
        // a module can always start if installed
1822
 
1823
        final FutureTask<MenuAndActions> menuAndActions = new FutureTask<MenuAndActions>(new Callable<MenuAndActions>() {
1824
            @Override
1825
            public MenuAndActions call() throws Exception {
1826
                return MenuManager.getInstance().copyMenuAndActions();
1827
            }
1828
        });
1829
        SwingThreadUtils.invoke(menuAndActions);
1830
        for (final AbstractModule module : toStart) {
1831
            final ModuleFactory f = module.getFactory();
1832
            final String id = f.getID();
67 ilm 1833
            try {
80 ilm 1834
                // do the request here instead of in the EDT in setupComponents()
1835
                assert !this.runningModules.containsKey(id) : "Doing a request for nothing";
1836
                final Tuple2<Set<String>, Set<SQLName>> createdItems = getCreatedItems(id);
1837
                // execute right away if possible, allowing the caller to handle any exceptions
1838
                if (SwingUtilities.isEventDispatchThread()) {
1839
                    startModule(module, createdItems, menuAndActions.get());
1840
                } else {
1841
                    // keep the for outside to avoid halting the EDT too long
1842
                    SwingUtilities.invokeLater(new Runnable() {
1843
                        @Override
1844
                        public void run() {
1845
                            try {
1846
                                startModule(module, createdItems, menuAndActions.get());
1847
                            } catch (Exception e) {
1848
                                ExceptionHandler.handle(MainFrame.getInstance(), "Unable to start " + f, e);
1849
                            }
1850
                        }
1851
                    });
1852
                }
1853
            } catch (Exception e) {
1854
                throw new Exception("Couldn't start module " + module, e);
1855
            }
1856
 
1857
            this.runningModules.put(id, module);
1858
        }
1859
        SwingThreadUtils.invoke(new Runnable() {
1860
            @Override
1861
            public void run() {
1862
                try {
1863
                    MenuManager.getInstance().setMenuAndActions(menuAndActions.get());
1864
                } catch (Exception e) {
1865
                    ExceptionHandler.handle(MainFrame.getInstance(), "Unable to update menu", e);
1866
                }
1867
            }
1868
        });
1869
    }
1870
 
1871
    private final Set<SQLTable> loadTranslations(final SQLFieldTranslator trns, final AbstractModule module, final String mdVariant) throws IOException {
177 ilm 1872
        final Locale locale = getConf().getLocale();
80 ilm 1873
        final Control cntrl = TranslationManager.getControl();
1874
        final String baseName = "labels";
1875
 
1876
        final Set<SQLTable> res = new HashSet<SQLTable>();
1877
        boolean found = false;
1878
        for (Locale targetLocale = locale; targetLocale != null && !found; targetLocale = cntrl.getFallbackLocale(baseName, targetLocale)) {
1879
            final List<Locale> langs = cntrl.getCandidateLocales(baseName, targetLocale);
1880
            // SQLFieldTranslator overwrite, so we need to load from general to specific
1881
            final ListIterator<Locale> listIterator = CollectionUtils.getListIterator(langs, true);
1882
            while (listIterator.hasNext()) {
1883
                final Locale lang = listIterator.next();
156 ilm 1884
                final SQLElementNamesFromXML elemNames = new SQLElementNamesFromXML(lang);
1885
                final String bundleName = cntrl.toBundleName(baseName, lang);
1886
                final String resourceName = cntrl.toResourceName(bundleName, "xml");
81 ilm 1887
                final InputStream ins = module.getClass().getResourceAsStream(resourceName);
80 ilm 1888
                // do not force to have one mapping for each locale
1889
                if (ins != null) {
83 ilm 1890
                    L.config("module " + module.getName() + " loading translation from " + resourceName);
80 ilm 1891
                    final Set<SQLTable> loadedTables;
67 ilm 1892
                    try {
156 ilm 1893
                        loadedTables = trns.load(getRoot(), mdVariant, ins, elemNames).get0();
67 ilm 1894
                    } finally {
80 ilm 1895
                        ins.close();
67 ilm 1896
                    }
80 ilm 1897
                    if (loadedTables.size() > 0) {
1898
                        res.addAll(loadedTables);
1899
                        found |= true;
1900
                    }
156 ilm 1901
 
1902
                    // As in PropsConfiguration.loadTranslations(), perhaps load the class at
1903
                    // module.getClass().getPackage().getName() + '.' + bundleName
1904
                    // to allow more flexibility. Perhaps pass a ModuleSQLFieldTranslator to
1905
                    // restrict what a module can do (and have SQLElementNamesFromXML, mdVariant).
19 ilm 1906
                }
18 ilm 1907
            }
1908
        }
80 ilm 1909
        return res;
18 ilm 1910
    }
1911
 
80 ilm 1912
    private final void installAndRegister(final AbstractModule module, DepSolverGraph graph) throws Exception {
1913
        assert Thread.holdsLock(this);
1914
        assert !isModuleRunning(module.getFactory().getID());
151 ilm 1915
        // Snapshot now to allow install() to register and use its own elements
1916
        // Also needed for checks, since install() can do arbitrary changes to the directory
1917
        final Map<SQLTable, SQLElement> beforeElements = new HashMap<SQLTable, SQLElement>(getDirectory().getElementsMap());
80 ilm 1918
        try {
1919
            install(module, graph);
1920
        } catch (Exception e) {
1921
            throw new Exception("Couldn't install module " + module, e);
1922
        }
1923
        try {
151 ilm 1924
            this.registerSQLElements(module, beforeElements);
80 ilm 1925
        } catch (Exception e) {
1926
            throw new Exception("Couldn't register module " + module, e);
1927
        }
1928
    }
1929
 
1930
    private final void startModule(final AbstractModule module, final Tuple2<Set<String>, Set<SQLName>> createdItems, final MenuAndActions menuAndActions) throws Exception {
18 ilm 1931
        assert SwingUtilities.isEventDispatchThread();
80 ilm 1932
        this.setupComponents(module, createdItems, menuAndActions);
61 ilm 1933
        module.start();
1934
    }
1935
 
73 ilm 1936
    private final void setupMenu(final AbstractModule module, final MenuAndActions menuAndActions) {
1937
        module.setupMenu(new MenuContext(menuAndActions, module.getFactory().getID(), getDirectory(), getRoot()));
1938
    }
1939
 
61 ilm 1940
    public synchronized final boolean isModuleRunning(final String id) {
18 ilm 1941
        return this.runningModules.containsKey(id);
1942
    }
1943
 
61 ilm 1944
    /**
1945
     * The modules that are currently running. NOTE : if {@link #startModules(Collection, boolean)}
1946
     * or {@link #stopModule(String, boolean)} wasn't called from the EDT the modules will only be
1947
     * actually started/stopped when the EDT executes the invokeLater(). In other words a module can
1948
     * be in the result but not yet on screen, or module can no longer be in the result but still on
1949
     * screen.
1950
     *
1951
     * @return the started modules.
1952
     */
1953
    public synchronized final Map<String, AbstractModule> getRunningModules() {
1954
        return new HashMap<String, AbstractModule>(this.runningModules);
18 ilm 1955
    }
1956
 
80 ilm 1957
    /**
1958
     * The running modules depending on the passed one. E.g. if it isn't running returns an empty
1959
     * list.
1960
     *
1961
     * @param id a module.
1962
     * @return the running modules needing <code>id</code> (including itself), in stop order (i.e.
1963
     *         the first item isn't depended on).
1964
     */
1965
    public synchronized final List<ModuleReference> getRunningDependentModulesRecursively(final String id) {
18 ilm 1966
        if (!this.isModuleRunning(id))
80 ilm 1967
            return Collections.emptyList();
18 ilm 1968
 
1969
        final ModuleFactory f = this.runningModules.get(id).getFactory();
80 ilm 1970
        return getRunningDependentModulesRecursively(f.getReference(), new LinkedList<ModuleReference>());
1971
    }
1972
 
1973
    private synchronized final List<ModuleReference> getRunningDependentModulesRecursively(final ModuleReference ref, final List<ModuleReference> res) {
1974
        // can happen if a module depends on two others and they share a dependency, e.g.
1975
        // __ B
1976
        // A < > D
1977
        // __ C
1978
        if (!res.contains(ref) && this.isModuleRunning(ref.getID())) {
1979
            final ModuleFactory f = this.runningModules.get(ref.getID()).getFactory();
1980
            // the graph has no cycle, so we don't need to protected against infinite loop
1981
            final Set<ModuleReference> deps = new TreeSet<ModuleReference>(ModuleReference.COMP_ID_ASC_VERSION_DESC);
1982
            for (final DirectedEdge<ModuleFactory> e : this.dependencyGraph.incomingEdgesOf(f)) {
1983
                deps.add(e.getSource().getReference());
1984
            }
1985
            for (final ModuleReference dep : deps) {
1986
                this.getRunningDependentModulesRecursively(dep, res);
1987
            }
1988
            res.add(f.getReference());
18 ilm 1989
        }
80 ilm 1990
        return res;
18 ilm 1991
    }
1992
 
156 ilm 1993
    public final Set<ModuleReference> stopAllModules() {
1994
        // this method is not synchronized, so don't just return getRunningModules()
1995
        final Set<ModuleReference> res = new HashSet<>();
1996
        for (final String id : this.getRunningModules().keySet()) {
1997
            res.addAll(this.stopModuleRecursively(id));
1998
        }
1999
        return res;
2000
    }
2001
 
2002
    public synchronized final List<ModuleReference> stopModuleRecursively(final String id) {
2003
        final List<ModuleReference> res = getRunningDependentModulesRecursively(id);
2004
        for (final ModuleReference ref : res) {
80 ilm 2005
            this.stopModule(ref.getID());
2006
        }
156 ilm 2007
        return res;
80 ilm 2008
    }
2009
 
156 ilm 2010
    public final boolean stopModule(final String id) {
2011
        return this.stopModule(id, true);
18 ilm 2012
    }
2013
 
80 ilm 2014
    // TODO pass ModuleReference instead of ID (need to change this.runningModules)
156 ilm 2015
    public synchronized final boolean stopModule(final String id, final boolean persistent) {
18 ilm 2016
        if (!this.isModuleRunning(id))
156 ilm 2017
            return false;
18 ilm 2018
 
2019
        final ModuleFactory f = this.runningModules.get(id).getFactory();
80 ilm 2020
        if (this.isAdminRequired(f.getReference()) && !currentUserIsAdmin())
2021
            throw new IllegalStateException("Not allowed to stop a module required by the administrator " + f);
2022
        final Set<DepLink> deps = this.dependencyGraph.incomingEdgesOf(f);
2023
        for (final DepLink l : deps) {
2024
            if (this.isModuleRunning(l.getSource().getID()))
2025
                throw new IllegalArgumentException("Some dependents still running : " + deps);
2026
        }
18 ilm 2027
        final AbstractModule m = this.runningModules.remove(id);
61 ilm 2028
        try {
2029
            // execute right away if possible, allowing the caller to handle any exceptions
2030
            if (SwingUtilities.isEventDispatchThread()) {
2031
                stopModule(m);
2032
            } else {
2033
                SwingUtilities.invokeLater(new Runnable() {
2034
                    @Override
2035
                    public void run() {
2036
                        try {
2037
                            stopModule(m);
2038
                        } catch (Exception e) {
2039
                            ExceptionHandler.handle(MainFrame.getInstance(), "Unable to stop " + f, e);
2040
                        }
2041
                    }
2042
                });
2043
            }
2044
        } catch (Exception e) {
2045
            throw new IllegalStateException("Couldn't stop module " + m, e);
2046
        }
73 ilm 2047
        // we can't undo what the module has done, so just start from the base menu and re-apply all
2048
        // modifications
2049
        final MenuAndActions menuAndActions = MenuManager.getInstance().createBaseMenuAndActions();
2050
        final ArrayList<AbstractModule> modules = new ArrayList<AbstractModule>(this.runningModules.values());
2051
        SwingThreadUtils.invoke(new Runnable() {
156 ilm 2052
 
73 ilm 2053
            @Override
2054
            public void run() {
2055
                for (final AbstractModule m : modules) {
2056
                    setupMenu(m, menuAndActions);
2057
                }
2058
                MenuManager.getInstance().setMenuAndActions(menuAndActions);
2059
            }
156 ilm 2060
 
73 ilm 2061
        });
2062
 
18 ilm 2063
        if (persistent)
2064
            getRunningIDsPrefs().remove(m.getFactory().getID());
2065
        assert !this.isModuleRunning(id);
156 ilm 2066
        return true;
18 ilm 2067
    }
2068
 
61 ilm 2069
    private final void stopModule(final AbstractModule m) {
80 ilm 2070
        // this must not attempt to lock this monitor, see uninstallUnsafe()
61 ilm 2071
        assert SwingUtilities.isEventDispatchThread();
2072
        m.stop();
2073
        this.tearDownComponents(m);
2074
    }
2075
 
19 ilm 2076
    private void unregisterSQLElements(final AbstractModule module) {
80 ilm 2077
        final ModuleReference id = module.getFactory().getReference();
25 ilm 2078
        synchronized (this.modulesElements) {
2079
            if (this.modulesElements.containsKey(id)) {
80 ilm 2080
                final IdentityHashMap<SQLElement, SQLElement> elements = this.modulesElements.remove(id);
25 ilm 2081
                final SQLElementDirectory dir = getDirectory();
80 ilm 2082
                for (final Entry<SQLElement, SQLElement> e : elements.entrySet()) {
2083
                    dir.removeSQLElement(e.getKey());
2084
                    // restore replaced element if any
2085
                    if (e.getValue() != null) {
2086
                        dir.addSQLElement(e.getValue());
2087
                    }
2088
                }
2089
 
2090
                final String mdVariant = getMDVariant(module.getFactory());
2091
                // perhaps record which element this module modified in start()
2092
                for (final SQLElement elem : this.getDirectory().getElements()) {
2093
                    elem.removeFromMDPath(mdVariant);
2094
                }
2095
                getConf().getTranslator().removeDescFor(null, null, mdVariant, null);
25 ilm 2096
            }
19 ilm 2097
        }
2098
    }
2099
 
2100
    private void tearDownComponents(final AbstractModule module) {
61 ilm 2101
        assert SwingUtilities.isEventDispatchThread();
19 ilm 2102
        final String id = module.getFactory().getID();
2103
        if (this.modulesComponents.containsKey(id)) {
2104
            final ComponentsContext ctxt = this.modulesComponents.remove(id);
83 ilm 2105
            for (final Entry<SQLElement, ? extends Collection<String>> e : ctxt.getFields().entrySet())
19 ilm 2106
                for (final String fieldName : e.getValue())
2107
                    e.getKey().removeAdditionalField(fieldName);
83 ilm 2108
            for (final Entry<SQLElement, ? extends Collection<IListeAction>> e : ctxt.getRowActions().entrySet())
19 ilm 2109
                e.getKey().getRowActions().removeAll(e.getValue());
177 ilm 2110
            TranslationManager.removeTranslationStreamFromClass(module.getClass());
73 ilm 2111
            // can't undo so menu is reset in stopModule()
19 ilm 2112
        }
2113
    }
2114
 
80 ilm 2115
    private final List<ModuleReference> getDBDependentModules(final ModuleReference ref) throws Exception {
2116
        // dependencies are stored in the DB that way we can uninstall dependent modules even
2117
        // without their factories
73 ilm 2118
 
80 ilm 2119
        final SQLTable installedTable = getInstalledTable(getRoot());
2120
        final TableRef needingModule = new AliasedTable(installedTable, "needingModule");
2121
        final SQLTable depT = getDepTable();
2122
 
2123
        final SQLSelect sel = new SQLSelect();
2124
        sel.setWhere(getModuleRowWhere(installedTable).and(new Where(installedTable.getField(MODULE_COLNAME), "=", ref.getID())));
2125
        if (ref.getVersion() != null)
2126
            sel.andWhere(new Where(installedTable.getField(MODULE_VERSION_COLNAME), "=", ref.getVersion().getMerged()));
2127
        sel.addBackwardJoin("INNER", depT.getField(NEEDED_MODULE_COLNAME), null);
2128
        sel.addJoin("INNER", new Where(depT.getField(NEEDING_MODULE_COLNAME), "=", needingModule.getKey()));
2129
        sel.addSelect(needingModule.getKey());
2130
        sel.addSelect(needingModule.getField(MODULE_COLNAME));
2131
        sel.addSelect(needingModule.getField(MODULE_VERSION_COLNAME));
2132
 
2133
        @SuppressWarnings("unchecked")
2134
        final List<Map<String, Object>> rows = installedTable.getDBSystemRoot().getDataSource().execute(sel.asString());
2135
        final List<ModuleReference> res = new ArrayList<ModuleReference>(rows.size());
2136
        for (final Map<String, Object> row : rows) {
2137
            res.add(getRef(new SQLRow(needingModule.getTable(), row)));
19 ilm 2138
        }
80 ilm 2139
        return res;
19 ilm 2140
    }
2141
 
80 ilm 2142
    // ATTN the result is not in removal order since it might contain itself dependent modules, e.g.
2143
    // getDependentModules(C) will return A, B but the removal order is B, A :
2144
    // A
2145
    // ^
2146
    // |> C
2147
    // B
2148
    private synchronized final Set<ModuleReference> getDependentModules(final ModuleReference ref) throws Exception {
2149
        // predictable order
2150
        final Set<ModuleReference> res = new TreeSet<ModuleReference>(ModuleReference.COMP_ID_ASC_VERSION_DESC);
2151
        // ATTN if in the future we make local-only modules, we will have to record the dependencies
2152
        // in the local file system
2153
        res.addAll(getDBDependentModules(ref));
2154
        return res;
2155
    }
2156
 
19 ilm 2157
    /**
80 ilm 2158
     * The list of installed modules depending on the passed one.
19 ilm 2159
     *
80 ilm 2160
     * @param ref the module.
2161
     * @return the modules needing <code>ref</code> (excluding it), in uninstallation order (i.e.
2162
     *         the first item isn't depended on).
19 ilm 2163
     * @throws Exception if an error occurs.
2164
     */
80 ilm 2165
    public final List<ModuleReference> getDependentModulesRecursively(final ModuleReference ref) throws Exception {
2166
        return getDependentModulesRecursively(ref, new ArrayList<ModuleReference>());
2167
    }
67 ilm 2168
 
80 ilm 2169
    private synchronized final List<ModuleReference> getDependentModulesRecursively(final ModuleReference ref, final List<ModuleReference> res) throws Exception {
2170
        for (final ModuleReference depModule : getDependentModules(ref)) {
2171
            // can happen if a module depends on two others and they share a dependency, e.g.
2172
            // __ B
2173
            // A < > D
2174
            // __ C
2175
            if (!res.contains(depModule)) {
2176
                // the graph has no cycle, so we don't need to protected against infinite loop
2177
                final List<ModuleReference> depModules = this.getDependentModulesRecursively(depModule, res);
2178
                assert !depModules.contains(depModule) : "cycle with " + depModule;
2179
                res.add(depModule);
2180
            }
19 ilm 2181
        }
2182
        return res;
2183
    }
2184
 
2185
    // ids + modules depending on them in uninstallation order
80 ilm 2186
    // ATTN return ids even if not installed
2187
    synchronized final LinkedHashSet<ModuleReference> getAllOrderedDependentModulesRecursively(final Set<ModuleReference> ids) throws Exception {
2188
        final LinkedHashSet<ModuleReference> depModules = new LinkedHashSet<ModuleReference>();
2189
        for (final ModuleReference id : ids) {
19 ilm 2190
            if (!depModules.contains(id)) {
2191
                depModules.addAll(getDependentModulesRecursively(id));
2192
                // even without this line the result could still contain some of ids if it contained
2193
                // a module and one of its dependencies
2194
                depModules.add(id);
2195
            }
2196
        }
2197
        return depModules;
2198
    }
2199
 
80 ilm 2200
    public synchronized final Set<ModuleReference> uninstall(final Set<ModuleReference> ids, final boolean recurse) throws Exception {
2201
        return this.uninstall(ids, recurse, false);
19 ilm 2202
    }
2203
 
80 ilm 2204
    public synchronized final Set<ModuleReference> uninstall(final Set<ModuleReference> ids, final boolean recurse, final boolean force) throws Exception {
2205
        return this.applyChange(this.getUninstallSolution(ids, recurse, force), ModuleState.NOT_CREATED).getRemoved();
19 ilm 2206
    }
2207
 
80 ilm 2208
    // ATTN this doesn't use canCurrentUserInstall(), as (at least for now) there's one and only one
2209
    // solution. That way, the UI can list the modules that need to be uninstalled.
2210
    public synchronized final ModulesStateChange getUninstallSolution(final Set<ModuleReference> passedRefs, final boolean recurse, final boolean force) throws Exception {
2211
        // compute now, at the same time as the solution not in each
2212
        // ModulesStateChange.getInstallState()
2213
        final InstallationState installationState = new InstallationState(this);
19 ilm 2214
 
80 ilm 2215
        final Set<ModuleReference> ids = new HashSet<ModuleReference>();
2216
        for (final ModuleReference ref : passedRefs) {
2217
            if (ref.getVersion() == null)
2218
                throw new UnsupportedOperationException("Version needed for " + ref);
2219
            if (installationState.getLocalOrRemote().contains(ref)) {
2220
                ids.add(ref);
19 ilm 2221
            }
2222
        }
2223
 
80 ilm 2224
        final int size = ids.size();
2225
        final Set<ModuleReference> toRemove;
2226
        // optimize by not calling recursively getDependentModules()
2227
        if (!recurse && size == 1) {
2228
            final Set<ModuleReference> depModules = this.getDependentModules(ids.iterator().next());
2229
            if (depModules.size() > 0)
2230
                throw new IllegalStateException("Dependent modules not uninstalled : " + depModules);
2231
            toRemove = ids;
2232
        } else if (size > 0) {
2233
            toRemove = getAllOrderedDependentModulesRecursively(ids);
2234
        } else {
2235
            toRemove = Collections.emptySet();
2236
        }
2237
        // if size == 1, already tested
2238
        if (!recurse && size > 1) {
2239
            final Collection<ModuleReference> depModulesNotRequested = CollectionUtils.substract(toRemove, ids);
2240
            if (!depModulesNotRequested.isEmpty())
2241
                throw new IllegalStateException("Dependent modules not uninstalled : " + depModulesNotRequested);
2242
        }
2243
        return new ModulesStateChange() {
19 ilm 2244
 
80 ilm 2245
            @Override
2246
            public String getError() {
2247
                return null;
2248
            }
19 ilm 2249
 
80 ilm 2250
            @Override
2251
            public InstallationState getInstallState() {
2252
                return installationState;
2253
            }
18 ilm 2254
 
80 ilm 2255
            @Override
2256
            public Set<ModuleReference> getUserReferencesToInstall() {
2257
                return Collections.emptySet();
2258
            }
19 ilm 2259
 
80 ilm 2260
            @Override
2261
            public Set<ModuleReference> getReferencesToRemove() {
2262
                return toRemove;
2263
            }
67 ilm 2264
 
80 ilm 2265
            @Override
2266
            public boolean forceRemove() {
2267
                return force;
67 ilm 2268
            }
80 ilm 2269
 
2270
            @Override
2271
            public Set<ModuleReference> getReferencesToInstall() {
2272
                return Collections.emptySet();
67 ilm 2273
            }
2274
 
80 ilm 2275
            @Override
2276
            public DepSolverGraph getGraph() {
2277
                return null;
2278
            }
156 ilm 2279
 
2280
            @Override
2281
            public String toString() {
2282
                return "Uninstall solution for " + this.getReferencesToRemove();
2283
            }
80 ilm 2284
        };
67 ilm 2285
    }
19 ilm 2286
 
80 ilm 2287
    public final void uninstall(final ModuleReference ref) throws Exception {
2288
        this.uninstall(ref, false);
67 ilm 2289
    }
2290
 
80 ilm 2291
    public synchronized final Set<ModuleReference> uninstall(final ModuleReference id, final boolean recurse) throws Exception {
2292
        return this.uninstall(id, recurse, false);
67 ilm 2293
    }
2294
 
80 ilm 2295
    public synchronized final Set<ModuleReference> uninstall(final ModuleReference id, final boolean recurse, final boolean force) throws Exception {
2296
        return this.uninstall(Collections.singleton(id), recurse, force);
18 ilm 2297
    }
67 ilm 2298
 
156 ilm 2299
    // return the version in installed that matches ref
2300
    private final ModuleReference filter(final Set<ModuleReference> installed, final ModuleReference ref) {
2301
        for (final ModuleReference installedRef : installed) {
2302
            if (installedRef.getID().equals(ref.getID()) && (ref.getVersion() == null || installedRef.getVersion().equals(ref.getVersion())))
2303
                return installedRef;
2304
        }
2305
        return null;
67 ilm 2306
    }
2307
 
80 ilm 2308
    // unsafe because this method doesn't check dependents
2309
    // dbVersions parameter to avoid requests to the DB
2310
    // return true if the mref was actually uninstalled (i.e. it was installed locally or remotely)
156 ilm 2311
    private boolean uninstallUnsafe(final ModuleReference mref, final boolean requireModule, final InstallationState installState) throws SQLException, Exception {
80 ilm 2312
        assert Thread.holdsLock(this);
2313
        final String id = mref.getID();
2314
        // versions to uninstall
156 ilm 2315
        final ModuleReference localRef = filter(installState.getLocal(), mref);
2316
        final ModuleReference dbRef = filter(installState.getRemote(), mref);
2317
        final ModuleVersion dbVersion = dbRef == null ? null : dbRef.getVersion();
67 ilm 2318
 
80 ilm 2319
        // otherwise it will get re-installed the next launch
2320
        getRunningIDsPrefs().remove(id);
2321
        final Set<ModuleReference> refs = new HashSet<ModuleReference>(2);
156 ilm 2322
        if (localRef != null)
2323
            refs.add(localRef);
2324
        if (dbRef != null)
2325
            refs.add(dbRef);
80 ilm 2326
        setAdminRequiredModules(refs, false);
67 ilm 2327
 
80 ilm 2328
        // only return after having cleared required, so that we don't need to install just to
2329
        // not require
156 ilm 2330
        if (localRef == null && dbRef == null)
80 ilm 2331
            return false;
67 ilm 2332
 
156 ilm 2333
        if (dbRef != null && !currentUserIsAdmin())
80 ilm 2334
            throw new IllegalStateException("Not allowed to uninstall " + id + " from the database");
67 ilm 2335
 
80 ilm 2336
        // DB module
2337
        final AbstractModule module;
2338
        if (!this.isModuleRunning(id)) {
156 ilm 2339
            if (dbRef == null) {
2340
                assert localRef != null;
80 ilm 2341
                // only installed locally
2342
                module = null;
2343
            } else {
2344
                final SortedMap<ModuleVersion, ModuleFactory> available = this.factories.getVersions(id);
2345
                final ModuleReference ref;
2346
                if (available.containsKey(dbVersion)) {
156 ilm 2347
                    ref = dbRef;
80 ilm 2348
                } else {
2349
                    // perhaps modules should specify which versions they can uninstall
2350
                    final SortedMap<ModuleVersion, ModuleFactory> moreRecent = available.headMap(dbVersion);
2351
                    if (moreRecent.size() == 0) {
2352
                        ref = null;
2353
                    } else {
2354
                        // take the closest
2355
                        ref = new ModuleReference(id, moreRecent.lastKey());
2356
                    }
2357
                }
2358
                if (ref != null) {
2359
                    assert ref.getVersion().compareTo(dbVersion) >= 0;
2360
                    final ModuleFactory f = available.get(ref.getVersion());
2361
                    assert f != null;
2362
                    // only call expensive method if necessary
2363
                    if (!this.createdModules.containsKey(f)) {
156 ilm 2364
                        // * Don't use the result, instead use this.createdModules since the module
2365
                        // might have been created before.
2366
                        // * Cannot call directly applyChange(), we need DepSolver to create modules
2367
                        // that ref depends on, as they might be required by
2368
                        // AbstractModule.uninstall().
2369
                        // * Cannot pass NoChoicePredicate.NO_CHANGE as ref won't be created if not
2370
                        // already installed both locally and remotely. No installation will occur
2371
                        // since we pass ModuleState.CREATED.
2372
                        this.createModules(Collections.singleton(ref), NoChoicePredicate.ONLY_INSTALL, ModuleState.CREATED);
80 ilm 2373
                    }
2374
                    module = this.createdModules.get(f);
2375
                } else {
2376
                    module = null;
2377
                }
2378
                if (module == null && requireModule) {
2379
                    final String reason;
2380
                    if (ref == null) {
2381
                        reason = "No version recent enough to uninstall " + dbVersion + " : " + available.keySet();
2382
                    } else {
2383
                        // TODO include InvalidRef in ModulesStateChangeResult
2384
                        reason = "Creation of " + ref + " failed (e.g. missing factory, dependency)";
2385
                    }
2386
                    throw new IllegalStateException("Couldn't get module " + id + " : " + reason);
2387
                }
73 ilm 2388
            }
80 ilm 2389
        } else {
156 ilm 2390
            final ModuleVersion localVersion = localRef.getVersion();
80 ilm 2391
            if (!localVersion.equals(dbVersion))
2392
                L.warning("Someone else has changed the database version while we were running :" + localVersion + " != " + dbVersion);
2393
            module = this.runningModules.get(id);
2394
            assert localVersion.equals(module.getFactory().getVersion());
2395
            this.stopModule(id, false);
2396
            // The module has to be stopped before we can proceed
2397
            // ATTN we hold this monitor, so stop() should never try to acquire it in the EDT
2398
            if (!SwingUtilities.isEventDispatchThread()) {
2399
                SwingUtilities.invokeAndWait(EMPTY_RUNNABLE);
2400
            }
67 ilm 2401
        }
156 ilm 2402
        assert (module == null) == (!requireModule || dbRef == null);
67 ilm 2403
 
80 ilm 2404
        SQLUtils.executeAtomic(getDS(), new SQLFactory<Object>() {
2405
            @Override
2406
            public Object create() throws SQLException {
2407
                final DBRoot root = getRoot();
2408
                if (module != null) {
2409
                    module.uninstall(root);
2410
                    unregisterSQLElements(module);
2411
                }
156 ilm 2412
                if (localRef != null)
2413
                    setModuleInstalledLocally(localRef, false);
67 ilm 2414
 
80 ilm 2415
                // uninstall from DB
156 ilm 2416
                if (dbRef != null) {
80 ilm 2417
                    final Tuple2<Set<String>, Set<SQLName>> createdItems = getCreatedItems(id);
2418
                    final List<ChangeTable<?>> l = new ArrayList<ChangeTable<?>>();
2419
                    final Set<String> tableNames = createdItems.get0();
2420
                    for (final SQLName field : createdItems.get1()) {
2421
                        final SQLField f = root.getDesc(field, SQLField.class);
2422
                        // dropped by DROP TABLE
2423
                        if (!tableNames.contains(f.getTable().getName())) {
2424
                            // cascade needed since the module might have created constraints
2425
                            // (e.g. on H2 a foreign column cannot be dropped)
2426
                            l.add(new AlterTable(f.getTable()).dropColumnCascade(f.getName()));
2427
                        }
2428
                    }
2429
                    for (final String table : tableNames) {
2430
                        l.add(new DropTable(root.getTable(table)));
2431
                    }
2432
                    if (l.size() > 0) {
2433
                        for (final String s : ChangeTable.cat(l, root.getName()))
2434
                            root.getDBSystemRoot().getDataSource().execute(s);
2435
                        root.getSchema().updateVersion();
2436
                        root.refetch();
2437
                    }
67 ilm 2438
 
156 ilm 2439
                    removeModuleFields(dbRef);
80 ilm 2440
                }
2441
                return null;
2442
            }
2443
        });
2444
        return true;
67 ilm 2445
    }
18 ilm 2446
}