OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

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