OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

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