OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

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