Dépôt officiel du code source de l'ERP OpenConcerto
Rev 177 | Blame | Compare with Previous | Last modification | View Log | RSS feed
/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright 2011-2019 OpenConcerto, by ILM Informatique. All rights reserved.
*
* The contents of this file are subject to the terms of the GNU General Public License Version 3
* only ("GPL"). You may not use this file except in compliance with the License. You can obtain a
* copy of the License at http://www.gnu.org/licenses/gpl-3.0.html See the License for the specific
* language governing permissions and limitations under the License.
*
* When distributing the software, include this License Header Notice in each file.
*/
package org.openconcerto.erp.modules;
import org.openconcerto.erp.config.Log;
import org.openconcerto.erp.config.MainFrame;
import org.openconcerto.erp.config.MenuAndActions;
import org.openconcerto.erp.config.MenuManager;
import org.openconcerto.erp.modules.DepSolverResult.Factory;
import org.openconcerto.erp.modules.ModuleTableModel.ModuleRow;
import org.openconcerto.sql.Configuration;
import org.openconcerto.sql.element.SQLElement;
import org.openconcerto.sql.element.SQLElementDirectory;
import org.openconcerto.sql.element.SQLElementNamesFromXML;
import org.openconcerto.sql.model.AliasedTable;
import org.openconcerto.sql.model.ConnectionHandlerNoSetup;
import org.openconcerto.sql.model.DBFileCache;
import org.openconcerto.sql.model.DBItemFileCache;
import org.openconcerto.sql.model.DBRoot;
import org.openconcerto.sql.model.SQLDataSource;
import org.openconcerto.sql.model.SQLField;
import org.openconcerto.sql.model.SQLName;
import org.openconcerto.sql.model.SQLRow;
import org.openconcerto.sql.model.SQLRowListRSH;
import org.openconcerto.sql.model.SQLRowValues;
import org.openconcerto.sql.model.SQLSelect;
import org.openconcerto.sql.model.SQLSyntax;
import org.openconcerto.sql.model.SQLSystem;
import org.openconcerto.sql.model.SQLTable;
import org.openconcerto.sql.model.TableRef;
import org.openconcerto.sql.model.Where;
import org.openconcerto.sql.model.graph.DirectedEdge;
import org.openconcerto.sql.model.graph.Link.Rule;
import org.openconcerto.sql.preferences.SQLPreferences;
import org.openconcerto.sql.request.SQLFieldTranslator;
import org.openconcerto.sql.users.rights.UserRightsManager;
import org.openconcerto.sql.utils.AlterTable;
import org.openconcerto.sql.utils.ChangeTable;
import org.openconcerto.sql.utils.ChangeTable.ForeignColSpec;
import org.openconcerto.sql.utils.DropTable;
import org.openconcerto.sql.utils.SQLCreateTable;
import org.openconcerto.sql.utils.SQLUtils;
import org.openconcerto.sql.utils.SQLUtils.SQLFactory;
import org.openconcerto.sql.view.list.IListeAction;
import org.openconcerto.ui.SwingThreadUtils;
import org.openconcerto.utils.BaseDirs;
import org.openconcerto.utils.CollectionMap2.Mode;
import org.openconcerto.utils.CollectionUtils;
import org.openconcerto.utils.ExceptionHandler;
import org.openconcerto.utils.ExceptionUtils;
import org.openconcerto.utils.FileUtils;
import org.openconcerto.utils.SetMap;
import org.openconcerto.utils.StringUtils;
import org.openconcerto.utils.ThreadFactory;
import org.openconcerto.utils.Tuple2;
import org.openconcerto.utils.Tuple3;
import org.openconcerto.utils.cc.IClosure;
import org.openconcerto.utils.cc.IdentityHashSet;
import org.openconcerto.utils.cc.IdentitySet;
import org.openconcerto.utils.i18n.TranslationManager;
import org.openconcerto.xml.XMLCodecUtils;
import java.beans.XMLDecoder;
import java.beans.XMLEncoder;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.ResourceBundle.Control;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeSet;
import java.util.concurrent.Callable;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.FutureTask;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.prefs.BackingStoreException;
import java.util.prefs.Preferences;
import javax.swing.SwingUtilities;
import net.jcip.annotations.GuardedBy;
import net.jcip.annotations.ThreadSafe;
/**
* Hold the list of known modules and their status.
*
* @author Sylvain CUAZ
*/
@ThreadSafe
public class ModuleManager {
/**
* The right to install/uninstall modules in the database (everyone can install locally).
*/
static public final String MODULE_DB_RIGHT = "moduleDBAdmin";
static final Logger L = Logger.getLogger(ModuleManager.class.getPackage().getName());
@GuardedBy("ModuleManager.class")
private static ExecutorService exec = null;
private static synchronized final Executor getExec() {
if (exec == null)
exec = new ThreadPoolExecutor(0, 2, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(), new ThreadFactory(ModuleManager.class.getSimpleName()
// not daemon since install() is not atomic
+ " executor thread ", false));
return exec;
}
static final Runnable EMPTY_RUNNABLE = new Runnable() {
@Override
public void run() {
}
};
// public needed for XMLEncoder
static public enum ModuleState {
NOT_CREATED, CREATED, INSTALLED, REGISTERED, STARTED
}
static enum ModuleAction {
INSTALL, START, STOP, UNINSTALL
}
private static final long MIN_VERSION = ModuleVersion.MIN.getMerged();
private static final String MODULE_COLNAME = "MODULE_NAME";
private static final String MODULE_VERSION_COLNAME = "MODULE_VERSION";
private static final String TABLE_COLNAME = "TABLE";
private static final String FIELD_COLNAME = "FIELD";
private static final String ISKEY_COLNAME = "KEY";
// Don't use String literals for the synchronized blocks
private static final String FWK_MODULE_TABLENAME = new String("FWK_MODULE_METADATA");
private static final String FWK_MODULE_DEP_TABLENAME = new String("FWK_MODULE_DEP");
private static final String NEEDING_MODULE_COLNAME = "ID_MODULE";
private static final String NEEDED_MODULE_COLNAME = "ID_MODULE_NEEDED";
private static final String fileMutex = new String("modules");
private static final Integer TO_INSTALL_VERSION = 1;
private static final String OLD_BACKUP_DIR_SUFFIX = ".backup";
private static final String OLD_FAILED_DIR_SUFFIX = ".failed";
private static final String BACKUP_DIR = "Install backup";
private static final String FAILED_DIR = "Install failed";
// return true if the MainFrame is not displayable (or if there's none)
static private boolean noDisplayableFrame() {
final MainFrame mf = MainFrame.getInstance();
if (mf == null)
return true;
final FutureTask<Boolean> f = new FutureTask<Boolean>(new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
return !mf.isDisplayable();
}
});
SwingThreadUtils.invoke(f);
try {
return f.get();
} catch (Exception e) {
Log.get().log(Level.WARNING, "Couldn't determine MainFrame displayability", e);
return true;
}
}
public static synchronized void tearDown() {
if (exec != null) {
exec.shutdown();
exec = null;
}
}
public static String getMDVariant(ModuleFactory f) {
return getMDVariant(f.getReference());
}
public static String getMDVariant(ModuleReference ref) {
return ref.getID();
}
static final Set<ModuleReference> versionsMapToSet(final Map<String, ModuleVersion> versions) {
final Set<ModuleReference> res = new HashSet<ModuleReference>(versions.size());
for (final Entry<String, ModuleVersion> e : versions.entrySet())
res.add(new ModuleReference(e.getKey(), e.getValue()));
return res;
}
// final (thus safely published) and thread-safe
private final FactoriesByID factories;
@GuardedBy("this")
private List<File> folders = Collections.emptyList();
// to avoid starting twice the same module
// we synchronize the whole install/start and stop/uninstall
@GuardedBy("this")
private final Map<String, AbstractModule> runningModules;
// in fact it is also already guarded by "this"
@GuardedBy("modulesElements")
private final Map<ModuleReference, IdentityHashMap<SQLElement, SQLElement>> modulesElements;
@GuardedBy("this")
private boolean inited;
// only in EDT
private final Map<String, ComponentsContext> modulesComponents;
// graph of created modules, ATTN since we have no way to "unload" a module we only add and
// never remove from it
@GuardedBy("this")
private final DependencyGraph dependencyGraph;
@GuardedBy("this")
private final Map<ModuleFactory, AbstractModule> createdModules;
// Another mutex so we can query root or conf without having to wait for modules to
// install/uninstall, or alternatively so that start() & stop() executed in the EDT don't need
// this monitor (see uninstallUnsafe()). This lock is a leaf lock, it mustn't call code that
// might need another lock.
private final Object varLock = new String("varLock");
@GuardedBy("varLock")
private DBRoot root;
@GuardedBy("this")
private SQLPreferences dbPrefs;
@GuardedBy("varLock")
private Configuration conf;
@GuardedBy("varLock")
private boolean exitAllowed;
public ModuleManager() {
this.factories = new FactoriesByID();
// stopModule() needs order to reset menu
this.runningModules = new LinkedHashMap<String, AbstractModule>();
this.dependencyGraph = new DependencyGraph();
this.createdModules = new LinkedHashMap<ModuleFactory, AbstractModule>();
this.modulesElements = new HashMap<ModuleReference, IdentityHashMap<SQLElement, SQLElement>>();
this.inited = false;
this.modulesComponents = new HashMap<String, ComponentsContext>();
this.root = null;
this.dbPrefs = null;
this.conf = null;
this.exitAllowed = true;
}
/**
* Whether the current user can manage modules.
*
* @return <code>true</code> if the current user can manage modules.
*/
public final boolean currentUserIsAdmin() {
return UserRightsManager.getCurrentUserRights().haveRight(MODULE_DB_RIGHT);
}
// AdminRequiredModules means installed & started
// possible AdminForbiddenModules means neither installed nor started
public final boolean canCurrentUser(final ModuleAction action, final ModuleRow m) {
if (currentUserIsAdmin())
return true;
if (action == ModuleAction.INSTALL || action == ModuleAction.UNINSTALL)
return canCurrentUserInstall(action, m.isInstalledRemotely());
else if (action == ModuleAction.START)
return true;
else if (action == ModuleAction.STOP)
return !m.isAdminRequired();
else
throw new IllegalArgumentException("Unknown action " + action);
}
final boolean canCurrentUserInstall(final ModuleAction action, final ModuleReference ref, final InstallationState state) {
return this.canCurrentUserInstall(action, state.getRemote().contains(ref));
}
private final boolean canCurrentUserInstall(final ModuleAction action, final boolean installedRemotely) {
if (currentUserIsAdmin())
return true;
if (action == ModuleAction.INSTALL)
return installedRemotely;
else if (action == ModuleAction.UNINSTALL)
return !installedRemotely;
else
throw new IllegalArgumentException("Illegal action " + action);
}
// *** factories (thread-safe)
public final void setFolders(final File... dirs) {
this.setFolders(Arrays.asList(dirs));
}
// The system directories should be before the user directories.
// This order is important for getFolderToWrite() and allow the user modules to replace the
// system ones.
public final void setFolders(final List<File> dirs) {
final List<File> absolutes = new ArrayList<>(dirs.size());
for (final File dir : dirs) {
final File abs = dir.getAbsoluteFile();
this.addFactories(abs);
absolutes.add(abs);
}
synchronized (this) {
this.folders = Collections.unmodifiableList(absolutes);
}
}
public synchronized final List<File> getFolders() {
return this.folders;
}
public static final File getFolderToWrite(final List<File> dirs) {
// Try to install in system directory first, then fall back to user directory.
for (final File dir : dirs) {
try {
return BaseDirs.getFolderToWrite(dir);
} catch (IOException e) {
// try next one
}
}
return null;
}
public final int addFactories(final File dir) {
if (!dir.exists()) {
L.warning("Module factory directory not found: " + dir.getAbsolutePath());
return 0;
}
final File[] jars = dir.listFiles(new FileFilter() {
@Override
public boolean accept(File f) {
return f.getName().endsWith(".jar");
}
});
int i = 0;
if (jars != null) {
for (final File jar : jars) {
try {
this.addFactory(new JarModuleFactory(jar));
i++;
} catch (Exception e) {
L.warning("Couldn't add " + jar);
e.printStackTrace();
}
}
}
return i;
}
public final ModuleFactory addFactoryFromPackage(File jar) throws IOException {
final ModuleFactory f = new JarModuleFactory(jar);
this.addFactory(f);
return f;
}
/**
* Adds a factory.
*
* @param f the factory to add.
* @return the ID of the factory.
*/
public final String addFactory(ModuleFactory f) {
final ModuleFactory prev = this.factories.add(f);
if (prev != null)
L.info("Changing the factory for " + f.getReference() + "\nfrom\t" + prev + "\nto\t" + f);
return f.getID();
}
public final void addFactories(Collection<ModuleFactory> factories) {
for (final ModuleFactory f : factories)
this.addFactory(f);
}
public final String addFactoryAndStart(final ModuleFactory f, final boolean persistent) {
return this.addFactory(f, true, persistent);
}
private final String addFactory(final ModuleFactory f, final boolean start, final boolean persistent) {
this.addFactory(f);
if (start) {
L.config("addFactory() invoked start " + (persistent ? "" : "not") + " persistent for " + f);
this.invoke(new IClosure<ModuleManager>() {
@Override
public void executeChecked(ModuleManager input) {
try {
if (!startModule(f.getReference(), persistent))
throw new IllegalStateException("Couldn't be started");
} catch (Throwable e) {
ExceptionHandler.handle(MainFrame.getInstance(), "Unable to start " + f, e);
}
}
});
}
return f.getID();
}
public final Map<String, SortedMap<ModuleVersion, ModuleFactory>> getFactories() {
return this.factories.getMap();
}
public final FactoriesByID copyFactories() {
return new FactoriesByID(this.factories);
}
public final void removeFactory(String id) {
this.factories.remove(id);
}
// *** modules (thread-safe)
/**
* Call the passed closure at a time when modules can be started. In particular this manager has
* been set up and the {@link MainFrame#getInstance() main frame} has been created.
*
* @param c the closure to execute.
*/
public void invoke(final IClosure<ModuleManager> c) {
MainFrame.invoke(new Runnable() {
@Override
public void run() {
getExec().execute(new Runnable() {
@Override
public void run() {
c.executeChecked(ModuleManager.this);
}
});
}
});
}
/**
* Allow to access certain methods without a full {@link #setup(DBRoot, Configuration)}. If
* setup() is subsequently called it must be passed the same root instance.
*
* @param root the root.
* @throws IllegalStateException if already set.
* @see #getDBInstalledModules()
* @see #getCreatedItems(String)
*/
public final void setRoot(final DBRoot root) {
synchronized (this.varLock) {
if (this.root != root) {
if (this.root != null)
throw new IllegalStateException("Root already set");
this.root = root;
}
}
}
public final boolean isSetup() {
synchronized (this.varLock) {
return this.getRoot() != null && this.getConf() != null;
}
}
/**
* Set up the module manager.
*
* @param root the root where the modules install.
* @param conf the configuration the modules change.
* @throws IllegalStateException if already {@link #isSetup() set up}.
*/
public final void setup(final DBRoot root, final Configuration conf) throws IllegalStateException {
if (root == null || conf == null)
throw new NullPointerException();
synchronized (this.varLock) {
if (this.isSetup())
throw new IllegalStateException("Already setup");
assert this.modulesElements.isEmpty() && this.runningModules.isEmpty() && this.modulesComponents.isEmpty() : "Modules cannot start without root & conf";
this.setRoot(root);
this.conf = conf;
}
}
public synchronized final boolean isInited() {
return this.inited;
}
/**
* Initialise the module manager.
*
* @throws Exception if required modules couldn't be registered.
*/
public synchronized final void init() throws Exception {
if (!this.isSetup())
throw new IllegalStateException("Not setup");
// don't check this.inited, that way we could register additional elements
SQLPreferences.getPrefTable(this.getRoot());
final List<ModuleReference> requiredModules = new ArrayList<>(this.getAdminRequiredModules());
final List<ModuleReference> dbRequiredModules = new ArrayList<>(this.getDBRequiredModules());
// add modules previously chosen (before restart)
final File toInstallFile = this.getToInstallFile();
Set<ModuleReference> toInstall = Collections.emptySet();
Set<ModuleReference> userReferencesToInstall = Collections.emptySet();
boolean persistent = false;
ModuleState toInstallTargetState = ModuleState.NOT_CREATED;
if (toInstallFile.exists()) {
if (!toInstallFile.canRead() || !toInstallFile.isFile()) {
L.warning("Couldn't read " + toInstallFile);
} else {
try (final XMLDecoder dec = new XMLDecoder(new FileInputStream(toInstallFile))) {
final Number version = (Number) dec.readObject();
if (version.intValue() != TO_INSTALL_VERSION.intValue())
throw new Exception("Version mismatch, expected " + TO_INSTALL_VERSION + " found " + version);
final Date fileDate = (Date) dec.readObject();
@SuppressWarnings("unchecked")
final Set<ModuleReference> toInstallUnsafe = (Set<ModuleReference>) dec.readObject();
@SuppressWarnings("unchecked")
final Set<ModuleReference> userReferencesToInstallUnsafe = (Set<ModuleReference>) dec.readObject();
toInstallTargetState = (ModuleState) dec.readObject();
persistent = (Boolean) dec.readObject();
try {
final Object extra = dec.readObject();
assert false : "Extra object " + extra;
} catch (ArrayIndexOutOfBoundsException e) {
// OK
}
final Date now = new Date();
if (fileDate.compareTo(now) > 0) {
L.warning("File is in the future : " + fileDate);
// check less than 2 hours
} else if (now.getTime() - fileDate.getTime() > 2 * 3600 * 1000) {
L.warning("File is too old : " + fileDate);
} else {
// no need to check that remote and local installed haven't changed since
// we're using ONLY_INSTALL_ARGUMENTS
toInstall = toInstallUnsafe;
userReferencesToInstall = userReferencesToInstallUnsafe;
if (toInstallTargetState.compareTo(ModuleState.REGISTERED) < 0)
L.warning("Forcing state to " + ModuleState.REGISTERED);
}
} catch (Exception e) {
// move out file to allow the next init() to succeed
final File errorFile = FileUtils.addSuffix(toInstallFile, ".error");
errorFile.delete();
final boolean renamed = toInstallFile.renameTo(errorFile);
throw new Exception("Couldn't parse " + toInstallFile + " ; renamed : " + renamed, e);
} finally {
// Either the installation will succeed and we don't want to execute it again,
// or the installation will fail and we don't want to be caught in an endless
// loop (since there's no API to remove this file).
if (toInstallFile.exists() && !toInstallFile.delete())
throw new IOException("Couldn't delete " + toInstallFile);
}
}
}
// handle upgrades : the old version was not uninstalled and thus still in
// dbRequiredModules, and the new version is in toInstall, so the DepSolver will error out
// since both can't be installed at the same time.
for (final ModuleReference toInstallRef : toInstall) {
final Iterator<ModuleReference> iter = dbRequiredModules.iterator();
while (iter.hasNext()) {
final ModuleReference dbReqRef = iter.next();
if (dbReqRef.getID().equals(toInstallRef.getID())) {
L.config("Ignoring DB required " + dbReqRef + " because " + toInstallRef + " was requested from the last exit");
iter.remove();
}
}
}
requiredModules.addAll(dbRequiredModules);
requiredModules.addAll(toInstall);
// if there's some choice to make, let the user make it
final Tuple2<Solutions, ModulesStateChangeResult> modules = this.createModules(requiredModules, NoChoicePredicate.ONLY_INSTALL_ARGUMENTS, ModuleState.REGISTERED);
if (modules.get1().getNotCreated().size() > 0)
throw new Exception("Impossible de créer les modules, not solved : " + modules.get0().getNotSolvedReferences() + " ; not created : " + modules.get1().getNotCreated());
if (toInstallTargetState.compareTo(ModuleState.STARTED) >= 0) {
// make them start by startPreviouslyRunningModules() (avoiding invokeLater() of
// createModules())
if (persistent) {
setPersistentModules(userReferencesToInstall);
} else {
// NO_CHANGE since they were just installed above
this.createModules(userReferencesToInstall, NoChoicePredicate.NO_CHANGE, ModuleState.STARTED, persistent);
}
}
this.inited = true;
}
// whether module removal can exit the VM
final void setExitAllowed(boolean exitAllowed) {
synchronized (this.varLock) {
this.exitAllowed = exitAllowed;
}
}
final boolean isExitAllowed() {
synchronized (this.varLock) {
return this.exitAllowed;
}
}
public final boolean needExit(final ModulesStateChange solution) {
final Set<ModuleReference> refsToRemove = solution.getReferencesToRemove();
if (!refsToRemove.isEmpty()) {
// only need to exit if the module is loaded into memory
final Set<ModuleReference> registeredModules = this.getRegisteredModules();
for (final ModuleReference toRemove : refsToRemove) {
if (registeredModules.contains(toRemove))
return true;
}
}
return false;
}
// Preferences is thread-safe
private Preferences getPrefs() {
// modules are installed per business entity (perhaps we could add a per user option, i.e.
// for all businesses of all databases)
final StringBuilder path = new StringBuilder(32);
for (final String item : DBFileCache.getJDBCAncestorNames(getRoot(), true)) {
path.append(StringUtils.getBoundedLengthString(DBItemFileCache.encode(item), Preferences.MAX_NAME_LENGTH));
path.append('/');
}
// path must not end with '/'
path.setLength(path.length() - 1);
return Preferences.userNodeForPackage(ModuleManager.class).node(path.toString());
}
// only put references passed by the user. That way if he installs module 'A' which depends on
// module 'util' and he later upgrade 'A' to a version which doesn't need 'util' anymore it
// won't get started. MAYBE even offer an "auto remove" feature.
private Preferences getRunningIDsPrefs() {
return getPrefs().node("toRun");
}
protected final Preferences getDBPrefs() {
synchronized (this) {
if (this.dbPrefs == null) {
final DBRoot root = getRoot();
if (root != null)
this.dbPrefs = (SQLPreferences) new SQLPreferences(root).node("modules");
}
return this.dbPrefs;
}
}
private final Preferences getRequiredIDsPrefs() {
return getDBPrefs().node("required");
}
protected final boolean isModuleInstalledLocally(String id) {
final File versionFile = getLocalVersionFile(id);
return versionFile != null && versionFile.exists();
}
protected final ModuleVersion getModuleVersionInstalledLocally(String id) {
synchronized (fileMutex) {
final File versionFile = getLocalVersionFile(id);
if (versionFile != null && versionFile.exists()) {
try {
return new ModuleVersion(Long.valueOf(FileUtils.read(versionFile)));
} catch (IOException e) {
throw new IllegalStateException("Couldn't get installed version of " + id, e);
}
} else {
return null;
}
}
}
public final Set<ModuleReference> getModulesInstalledLocally() {
return versionsMapToSet(getModulesVersionInstalledLocally());
}
public final Map<String, ModuleVersion> getModulesVersionInstalledLocally() {
synchronized (fileMutex) {
final File dir = getLocalDirectory();
if (!dir.isDirectory())
return Collections.emptyMap();
final Map<String, ModuleVersion> res = new HashMap<String, ModuleVersion>();
for (final File d : dir.listFiles()) {
final String id = d.getName();
final ModuleVersion version = getModuleVersionInstalledLocally(id);
if (version != null)
res.put(id, version);
}
return res;
}
}
private void setModuleInstalledLocally(ModuleReference f, boolean b) {
try {
synchronized (fileMutex) {
if (b) {
final ModuleVersion vers = f.getVersion();
vers.checkValidity();
final File versionFile = getLocalVersionFile(f.getID());
FileUtils.mkdir_p(versionFile.getParentFile());
FileUtils.write(String.valueOf(vers.getMerged()), versionFile);
} else {
// perhaps add a parameter to only remove the versionFile
FileUtils.rm_R(getLocalDirectory(f.getID()));
}
}
} catch (IOException e) {
throw new IllegalStateException("Couldn't change installed status of " + f, e);
}
}
private SQLTable getInstalledTable(final DBRoot r) throws SQLException {
synchronized (FWK_MODULE_TABLENAME) {
final List<SQLCreateTable> createTables = new ArrayList<SQLCreateTable>(4);
final SQLCreateTable createTable;
if (!r.contains(FWK_MODULE_TABLENAME)) {
// store :
// - currently installed module (TABLE_COLNAME & FIELD_COLNAME are null)
// - created tables (FIELD_COLNAME is null)
// - created fields (and whether they are keys)
createTable = new SQLCreateTable(r, FWK_MODULE_TABLENAME);
createTable.setPlain(true);
// let SQLCreateTable know which column is the primary key so that createDepTable
// can refer to it
createTable.addColumn(SQLSyntax.ID_NAME, createTable.getSyntax().getPrimaryIDDefinitionShort());
createTable.setPrimaryKey(SQLSyntax.ID_NAME);
createTable.addVarCharColumn(MODULE_COLNAME, 128);
createTable.addColumn(TABLE_COLNAME, "varchar(128) NULL");
createTable.addColumn(FIELD_COLNAME, "varchar(128) NULL");
createTable.addColumn(ISKEY_COLNAME, "boolean NULL");
createTable.addColumn(MODULE_VERSION_COLNAME, "bigint NOT NULL");
createTable.addUniqueConstraint("uniqModule", Arrays.asList(MODULE_COLNAME, TABLE_COLNAME, FIELD_COLNAME));
createTables.add(createTable);
} else {
createTable = null;
}
if (!r.contains(FWK_MODULE_DEP_TABLENAME)) {
final SQLCreateTable createDepTable = new SQLCreateTable(r, FWK_MODULE_DEP_TABLENAME);
createDepTable.setPlain(true);
final ForeignColSpec fk, fkNeeded;
if (createTable != null) {
fk = ForeignColSpec.fromCreateTable(createTable);
fkNeeded = ForeignColSpec.fromCreateTable(createTable);
} else {
final SQLTable moduleT = r.getTable(FWK_MODULE_TABLENAME);
fk = ForeignColSpec.fromTable(moduleT);
fkNeeded = ForeignColSpec.fromTable(moduleT);
}
// if we remove a module, remove it dependencies
createDepTable.addForeignColumn(fk.setColumnName(NEEDING_MODULE_COLNAME), Rule.CASCADE, Rule.CASCADE);
// if we try to remove a module that is needed, fail
createDepTable.addForeignColumn(fkNeeded.setColumnName(NEEDED_MODULE_COLNAME), Rule.CASCADE, Rule.RESTRICT);
createDepTable.setPrimaryKey(NEEDING_MODULE_COLNAME, NEEDED_MODULE_COLNAME);
createTables.add(createDepTable);
}
r.createTables(createTables);
}
return r.getTable(FWK_MODULE_TABLENAME);
}
private final SQLTable getDepTable() {
return getRoot().getTable(FWK_MODULE_DEP_TABLENAME);
}
public final DBRoot getRoot() {
synchronized (this.varLock) {
return this.root;
}
}
private SQLDataSource getDS() {
return getRoot().getDBSystemRoot().getDataSource();
}
public final Configuration getConf() {
synchronized (this.varLock) {
return this.conf;
}
}
private SQLElementDirectory getDirectory() {
return getConf().getDirectory();
}
public final File getLocalDirectory() {
return new File(this.getConf().getConfDir(getRoot()), "modules");
}
public final Set<String> migrateOldTransientDirs() {
Set<String> res = Collections.emptySet();
final File[] listFiles = this.getLocalDirectory().listFiles();
if (listFiles != null) {
for (final File f : listFiles) {
if (f.isDirectory()) {
res = migrateOldDir(res, f, OLD_BACKUP_DIR_SUFFIX);
res = migrateOldDir(res, f, OLD_FAILED_DIR_SUFFIX);
}
}
}
return res;
}
private final String getOldID(final File f, final String oldSuffix) {
if (f.getName().endsWith(oldSuffix)) {
final String id = f.getName().substring(0, f.getName().length() - oldSuffix.length());
return ModuleReference.checkID(id, false);
}
return null;
}
private final Set<String> migrateOldDir(Set<String> res, final File f, final String oldSuffix) {
final String id = getOldID(f, oldSuffix);
if (id != null) {
final Path localFailedDir = getLocalFailedDirectory(id);
try {
if (Files.exists(localFailedDir)) {
// newer already exists, remove old one
FileUtils.rm_R(f);
} else {
Files.createDirectories(localFailedDir.getParent());
Files.move(f.toPath(), localFailedDir);
}
} catch (IOException e) {
L.log(Level.CONFIG, "Couldn't migrate " + f, e);
if (res.isEmpty())
res = new HashSet<>();
res.add(id);
}
}
return res;
}
// file specifying which module (and only those, dependencies won't be installed automatically)
// to install during the next application launch.
private final File getToInstallFile() {
return new File(getLocalDirectory(), "toInstall");
}
private final File getLocalDataSubDir(final File f) {
// TODO return "Local Data" and a migration method
return f;
}
// => create "internal state" and "local data" folders inside getLocalDirectory() (i.e.
// module.id/local data and not local data/module.id so that we can easily copy to
// getLocalBackupDirectory() or uninstall a module)
private final Path getInternalStateSubDir(final Path f) {
// TODO return "Internal State" and a migration method
return f;
}
protected final File getLocalDataDirectory(final String id) {
return getLocalDataSubDir(this.getLocalDirectory(id));
}
private final Path getInternalStateDirectory(final String id) {
final File l = this.getLocalDirectory(id);
return l == null ? null : getInternalStateSubDir(l.toPath());
}
private final File getLocalDirectory(final String id) {
if (ModuleReference.checkID(id, false) == null)
return null;
return new File(this.getLocalDirectory(), id);
}
// contains a copy of local module data during install
protected final Path getLocalBackupDirectory(final String id) {
// will be ignored by getModulesVersionInstalledLocally()
assert ModuleReference.checkID(BACKUP_DIR, false) == null;
return this.getLocalDirectory().toPath().resolve(BACKUP_DIR).resolve(id);
}
// contains local module data of the last failed install
protected final Path getLocalFailedDirectory(final String id) {
// will be ignored by getModulesVersionInstalledLocally()
assert ModuleReference.checkID(FAILED_DIR, false) == null;
return this.getLocalDirectory().toPath().resolve(FAILED_DIR).resolve(id);
}
private final File getLocalVersionFile(final String id) {
final Path dir = this.getInternalStateDirectory(id);
if (dir == null)
return null;
return new File(dir.toFile(), "version");
}
public final ModuleVersion getDBInstalledModuleVersion(final String id) throws SQLException {
return getDBInstalledModules(id).get(id);
}
public final Set<ModuleReference> getModulesInstalledRemotely() throws SQLException {
return getDBInstalledModuleRowsByRef(null).keySet();
}
public final Map<String, ModuleVersion> getDBInstalledModules() throws SQLException {
return getDBInstalledModules(null);
}
private final Where getModuleRowWhere(final TableRef installedTable) throws SQLException {
return Where.isNull(installedTable.getField(TABLE_COLNAME)).and(Where.isNull(installedTable.getField(FIELD_COLNAME)));
}
private final List<SQLRow> getDBInstalledModuleRows(final String id) throws SQLException {
final SQLTable installedTable = getInstalledTable(getRoot());
final SQLSelect sel = new SQLSelect().addSelectStar(installedTable);
sel.setWhere(getModuleRowWhere(installedTable));
if (id != null)
sel.andWhere(new Where(installedTable.getField(MODULE_COLNAME), "=", id));
return SQLRowListRSH.execute(sel);
}
private final ModuleReference getRef(final SQLRow r) throws SQLException {
return new ModuleReference(r.getString(MODULE_COLNAME), new ModuleVersion(r.getLong(MODULE_VERSION_COLNAME)));
}
private final Map<String, ModuleVersion> getDBInstalledModules(final String id) throws SQLException {
final Map<String, ModuleVersion> res = new HashMap<String, ModuleVersion>();
for (final SQLRow r : getDBInstalledModuleRows(id)) {
final ModuleReference ref = getRef(r);
res.put(ref.getID(), ref.getVersion());
}
return res;
}
private final Map<ModuleReference, SQLRow> getDBInstalledModuleRowsByRef(final String id) throws SQLException {
final Map<ModuleReference, SQLRow> res = new HashMap<ModuleReference, SQLRow>();
for (final SQLRow r : getDBInstalledModuleRows(id)) {
res.put(getRef(r), r);
}
return res;
}
private SQLRow setDBInstalledModule(ModuleReference f, boolean b) throws SQLException {
final SQLTable installedTable = getInstalledTable(getRoot());
final Where idW = new Where(installedTable.getField(MODULE_COLNAME), "=", f.getID());
final Where w = idW.and(getModuleRowWhere(installedTable));
if (b) {
final SQLSelect sel = new SQLSelect();
sel.addSelect(installedTable.getKey());
sel.setWhere(w);
final Number id = (Number) installedTable.getDBSystemRoot().getDataSource().executeScalar(sel.asString());
final SQLRowValues vals = new SQLRowValues(installedTable);
vals.put(MODULE_VERSION_COLNAME, f.getVersion().getMerged());
if (id != null) {
vals.setID(id);
return vals.update();
} else {
vals.put(MODULE_COLNAME, f.getID());
vals.put(TABLE_COLNAME, null);
vals.put(FIELD_COLNAME, null);
return vals.insert();
}
} else {
installedTable.getDBSystemRoot().getDataSource().execute("DELETE FROM " + installedTable.getSQLName().quote() + " WHERE " + w.getClause());
installedTable.fireTableModified(SQLRow.NONEXISTANT_ID);
return null;
}
}
public final Tuple2<Set<String>, Set<SQLName>> getCreatedItems(final String id) throws SQLException {
final SQLTable installedTable = getInstalledTable(getRoot());
final SQLSelect sel = new SQLSelect();
sel.addSelect(installedTable.getKey());
sel.addSelect(installedTable.getField(TABLE_COLNAME));
sel.addSelect(installedTable.getField(FIELD_COLNAME));
sel.setWhere(new Where(installedTable.getField(MODULE_COLNAME), "=", id).and(Where.isNotNull(installedTable.getField(TABLE_COLNAME))));
final Set<String> tables = new HashSet<String>();
final Set<SQLName> fields = new HashSet<SQLName>();
for (final SQLRow r : SQLRowListRSH.execute(sel)) {
final String tableName = r.getString(TABLE_COLNAME);
final String fieldName = r.getString(FIELD_COLNAME);
if (fieldName == null)
tables.add(tableName);
else
fields.add(new SQLName(tableName, fieldName));
}
return Tuple2.create(tables, fields);
}
private void updateModuleFields(ModuleFactory factory, DepSolverGraph graph, final DBContext ctxt) throws SQLException {
final SQLTable installedTable = getInstalledTable(getRoot());
final Where idW = new Where(installedTable.getField(MODULE_COLNAME), "=", factory.getID());
// removed items
{
final List<Where> dropWheres = new ArrayList<Where>();
for (final String dropped : ctxt.getRemovedTables()) {
dropWheres.add(new Where(installedTable.getField(TABLE_COLNAME), "=", dropped));
}
for (final SQLName dropped : ctxt.getRemovedFieldsFromExistingTables()) {
dropWheres.add(new Where(installedTable.getField(TABLE_COLNAME), "=", dropped.getItem(0)).and(new Where(installedTable.getField(FIELD_COLNAME), "=", dropped.getItem(1))));
}
if (dropWheres.size() > 0)
installedTable.getDBSystemRoot().getDataSource().execute("DELETE FROM " + installedTable.getSQLName().quote() + " WHERE " + Where.or(dropWheres).and(idW).getClause());
}
// added items
{
final SQLRowValues vals = new SQLRowValues(installedTable);
vals.put(MODULE_VERSION_COLNAME, factory.getVersion().getMerged());
vals.put(MODULE_COLNAME, factory.getID());
for (final String added : ctxt.getAddedTables()) {
vals.put(TABLE_COLNAME, added).put(FIELD_COLNAME, null).insert();
final SQLTable t = ctxt.getRoot().findTable(added);
if (t == null) {
throw new IllegalStateException("Unable to find added table " + added + " in root " + ctxt.getRoot().getName());
}
for (final SQLField field : t.getFields()) {
vals.put(TABLE_COLNAME, added).put(FIELD_COLNAME, field.getName()).put(ISKEY_COLNAME, field.isKey()).insert();
}
vals.remove(ISKEY_COLNAME);
}
for (final SQLName added : ctxt.getAddedFieldsToExistingTables()) {
final SQLTable t = ctxt.getRoot().findTable(added.getItem(0));
final SQLField field = t.getField(added.getItem(1));
vals.put(TABLE_COLNAME, t.getName()).put(FIELD_COLNAME, field.getName()).put(ISKEY_COLNAME, field.isKey()).insert();
}
vals.remove(ISKEY_COLNAME);
}
// Always put true, even if getCreatedItems() is empty, since for now we can't be sure that
// the module didn't insert rows or otherwise changed the DB (MAYBE change SQLDataSource to
// hand out connections with read only user for a new ThreadGroup, or even no connections at
// all). If we could assert that the module didn't access at all the DB, we could add an
// option so that the module can declare not accessing the DB and install() would know that
// the DB version of the module is null. This could be beneficial since different users
// could install different version of modules that only change the UI.
final SQLRow moduleRow = setDBInstalledModule(factory.getReference(), true);
// update dependencies
final SQLTable depT = getDepTable();
depT.getDBSystemRoot().getDataSource().execute("DELETE FROM " + depT.getSQLName().quote() + " WHERE " + new Where(depT.getField(NEEDING_MODULE_COLNAME), "=", moduleRow.getID()).getClause());
depT.fireTableModified(SQLRow.NONEXISTANT_ID);
final SQLRowValues vals = new SQLRowValues(depT).put(NEEDING_MODULE_COLNAME, moduleRow.getID());
final Map<ModuleReference, SQLRow> moduleRows = getDBInstalledModuleRowsByRef(null);
for (final ModuleFactory dep : graph.getDependencies(factory).values()) {
vals.put(NEEDED_MODULE_COLNAME, moduleRows.get(dep.getReference()).getID()).insertVerbatim();
}
}
private void removeModuleFields(ModuleReference f) throws SQLException {
final SQLTable installedTable = getInstalledTable(getRoot());
final Where idW = new Where(installedTable.getField(MODULE_COLNAME), "=", f.getID());
installedTable.getDBSystemRoot().getDataSource()
.execute("DELETE FROM " + installedTable.getSQLName().quote() + " WHERE " + Where.isNotNull(installedTable.getField(TABLE_COLNAME)).and(idW).getClause());
setDBInstalledModule(f, false);
// FWK_MODULE_DEP_TABLENAME rows removed with CASCADE
getDepTable().fireTableModified(SQLRow.NONEXISTANT_ID);
}
/**
* Get the modules required because they have created a new table or new foreign key. E.g. if a
* module created a child table, as long as the table is in the database it needs to be archived
* along its parent.
*
* @return the modules.
* @throws SQLException if an error occurs.
*/
final List<ModuleReference> getDBRequiredModules() throws SQLException {
// modules which have created a table or a key
final SQLTable installedTable = getInstalledTable(getRoot());
final AliasedTable installedTableVers = new AliasedTable(installedTable, "vers");
final SQLSelect sel = new SQLSelect();
sel.addSelect(installedTable.getField(MODULE_COLNAME));
// for each row, get the version from the main row
sel.addJoin("INNER", new Where(installedTable.getField(MODULE_COLNAME), "=", installedTableVers.getField(MODULE_COLNAME)).and(getModuleRowWhere(installedTableVers)));
sel.addSelect(installedTableVers.getField(MODULE_VERSION_COLNAME));
final Where tableCreated = Where.isNotNull(installedTable.getField(TABLE_COLNAME)).and(Where.isNull(installedTable.getField(FIELD_COLNAME)));
final Where keyCreated = Where.isNotNull(installedTable.getField(FIELD_COLNAME)).and(new Where(installedTable.getField(ISKEY_COLNAME), "=", Boolean.TRUE));
sel.setWhere(tableCreated.or(keyCreated));
sel.addGroupBy(installedTable.getField(MODULE_COLNAME));
// allow to reference the field in the SELECT and shouldn't change anything since each
// module has only one version
sel.addGroupBy(installedTableVers.getField(MODULE_VERSION_COLNAME));
@SuppressWarnings("unchecked")
final List<Map<String, Object>> maps = (List<Map<String, Object>>) installedTable.getDBSystemRoot().getDataSource().execute(sel.asString());
final List<ModuleReference> res = new ArrayList<ModuleReference>(maps.size());
for (final Map<String, Object> m : maps) {
final String moduleID = (String) m.get(MODULE_COLNAME);
final ModuleVersion vers = new ModuleVersion(((Number) m.get(MODULE_VERSION_COLNAME)).longValue());
res.add(new ModuleReference(moduleID, vers));
}
L.config("getDBRequiredModules() found " + res);
return res;
}
private void install(final AbstractModule module, final DepSolverGraph graph) throws Exception {
assert Thread.holdsLock(this);
final ModuleFactory factory = module.getFactory();
final ModuleVersion localVersion = getModuleVersionInstalledLocally(factory.getID());
final ModuleVersion lastInstalledVersion = getDBInstalledModuleVersion(factory.getID());
final ModuleVersion moduleVersion = module.getFactory().getVersion();
final boolean dbOK = moduleVersion.equals(lastInstalledVersion);
if (!dbOK && !currentUserIsAdmin())
throw new IllegalStateException("Not allowed to install " + module.getFactory() + " in the database");
if (lastInstalledVersion != null && moduleVersion.compareTo(lastInstalledVersion) < 0)
throw new IllegalArgumentException("Module older than the one installed in the DB : " + moduleVersion + " < " + lastInstalledVersion);
if (localVersion != null && moduleVersion.compareTo(localVersion) < 0)
throw new IllegalArgumentException("Module older than the one installed locally : " + moduleVersion + " < " + localVersion);
if (!moduleVersion.equals(localVersion) || !dbOK) {
// local
final File localDir = getLocalDirectory(factory.getID());
// There are 2 choices to handle the update of files :
// 1. copy dir to a new one and pass it to DBContext, then either rename it to dir or
// rename it failed
// 2. copy dir to a backup, pass dir to DBContext, then either remove backup or rename
// it to dir
// Choice 2 is simpler since the module deals with the same directory in both install()
// and start()
final Path backupDir;
// check if we need a backup
if (localDir.exists()) {
backupDir = getLocalBackupDirectory(factory.getID());
FileUtils.rm_R(backupDir, false);
Files.createDirectories(backupDir.getParent());
FileUtils.copyDirectory(localDir.toPath(), backupDir, false, StandardCopyOption.COPY_ATTRIBUTES);
} else {
backupDir = null;
FileUtils.mkdir_p(localDir);
}
assert localDir.exists();
try {
SQLUtils.executeAtomic(getDS(), new ConnectionHandlerNoSetup<Object, IOException>() {
@Override
public Object handle(SQLDataSource ds) throws SQLException, IOException {
final Tuple2<Set<String>, Set<SQLName>> alreadyCreatedItems = getCreatedItems(factory.getID());
final DBContext ctxt = new DBContext(getLocalDataSubDir(localDir), localVersion, getRoot(), lastInstalledVersion, alreadyCreatedItems.get0(), alreadyCreatedItems.get1(),
getDirectory());
// install local (i.e. ctxt stores the actions to carry on the DB)
// TODO pass a data source with no rights to modify the data definition (or
// even no rights to modify the data if DB version is up to date)
module.install(ctxt);
if (!localDir.exists())
throw new IOException("Modules shouldn't remove their directory");
// install in DB
ctxt.executeSQL();
updateModuleFields(factory, graph, ctxt);
return null;
}
});
} catch (Exception e) {
// install did not complete successfully
if (getRoot().getServer().getSQLSystem() == SQLSystem.MYSQL)
L.warning("MySQL cannot rollback DDL statements");
// keep failed install files and restore previous files
final Path failed = getLocalFailedDirectory(factory.getID());
boolean moved = false;
try {
FileUtils.rm_R(failed, false);
Files.createDirectories(failed.getParent());
Files.move(localDir.toPath(), failed);
final String errorMsg = "Couldn't install " + module + " :\n" + ExceptionUtils.getStackTrace(e);
// TODO as in getLocalVersionFile(), separate internal state (i.e. version and
// error) from module local data
Files.write(getInternalStateSubDir(failed).resolve("Install error.txt"), errorMsg.getBytes(StandardCharsets.UTF_8));
moved = true;
} catch (Exception e1) {
L.log(Level.WARNING, "Couldn't move " + localDir + " to " + failed, e1);
}
// restore if needed
if (moved && backupDir != null) {
assert !localDir.exists();
try {
Files.move(backupDir, localDir.toPath());
} catch (Exception e1) {
L.log(Level.WARNING, "Couldn't restore " + backupDir + " to " + localDir, e1);
}
}
throw e;
}
// DB transaction was committed, remove backup files
assert localDir.exists();
if (backupDir != null)
FileUtils.rm_R(backupDir);
setModuleInstalledLocally(factory.getReference(), true);
}
assert moduleVersion.equals(getModuleVersionInstalledLocally(factory.getID())) && moduleVersion.equals(getDBInstalledModuleVersion(factory.getID()));
}
private void registerSQLElements(final AbstractModule module, Map<SQLTable, SQLElement> beforeElements) throws IOException {
final ModuleReference id = module.getFactory().getReference();
synchronized (this.modulesElements) {
// perhaps check that no other version of the module has been registered
if (!this.modulesElements.containsKey(id)) {
final SQLElementDirectory dir = getDirectory();
module.setupElements(dir);
final IdentityHashMap<SQLElement, SQLElement> elements = new IdentityHashMap<SQLElement, SQLElement>();
// use IdentitySet so as not to call equals() since it triggers initFF()
final IdentitySet<SQLElement> beforeElementsSet = new IdentityHashSet<SQLElement>(beforeElements.values());
// copy to be able to restore elements while iterating
final IdentitySet<SQLElement> afterElementsSet = new IdentityHashSet<SQLElement>(dir.getElements());
for (final SQLElement elem : afterElementsSet) {
if (!beforeElementsSet.contains(elem)) {
if (!(elem instanceof ModuleElement))
L.warning("Module added an element that isn't a ModuleElement : " + elem);
if (beforeElements.containsKey(elem.getTable())) {
final SQLElement replacedElem = beforeElements.get(elem.getTable());
// Code safety : a module can make sure that its elements won't be
// replaced. We thus require that elem is a subclass of replacedElem,
// i.e. a module can use standard java access rules (e.g. package
// private constructor, final method).
final boolean codeSafe = replacedElem.getClass().isInstance(elem);
final boolean mngrSafe = isMngrSafe(module, replacedElem);
if (codeSafe && mngrSafe) {
// store replacedElem so that it can be restored in unregister()
elements.put(elem, replacedElem);
} else {
final List<String> pbs = new ArrayList<String>(2);
if (!codeSafe)
pbs.add(elem + " isn't a subclass of " + replacedElem);
if (!mngrSafe)
pbs.add(module + " doesn't depend on " + replacedElem);
L.warning("Trying to replace element for " + elem.getTable() + " with " + elem + " but\n" + CollectionUtils.join(pbs, "\n"));
dir.addSQLElement(replacedElem);
}
} else {
elements.put(elem, null);
}
}
}
// Load translations after registering elements since SQLFieldTranslator needs them.
// If setupElements() needs translations then perhaps we should store translations
// with the element code, without needing the element.
final String mdVariant = getMDVariant(module.getFactory());
final Set<SQLTable> tablesWithMD = loadTranslations(getConf().getTranslator(), module, mdVariant);
// insert just loaded labels into the search path
for (final SQLTable tableWithDoc : tablesWithMD) {
final SQLElement sqlElem = this.getDirectory().getElement(tableWithDoc);
if (sqlElem == null)
throw new IllegalStateException("Missing element for table with metadata : " + tableWithDoc);
// avoid duplicates
final boolean already = sqlElem instanceof ModuleElement && ((ModuleElement) sqlElem).getFactory() == module.getFactory();
if (!already)
sqlElem.addToMDPath(mdVariant);
}
this.modulesElements.put(id, elements);
}
}
}
// Manager safety : when a module is unregistered, replacedElem can be restored. We thus require
// that replacedElem was registered by one of our dependencies (or by the core application),
// forcing a predictable order (or error if two unrelated modules want to replace the same
// element).
// FIXME modules are only unregistered when uninstalled (e.g. to know how to archive additional
// fields), so even though a module is stopped its UI (getName(), getComboRequest(),
// createComponent()) will still be used.
private boolean isMngrSafe(final AbstractModule module, final SQLElement replacedElem) {
final boolean mngrSafe;
final ModuleReference moduleForElement = getModuleForElement(replacedElem);
if (moduleForElement == null) {
// module from core app
mngrSafe = true;
} else {
// MAYBE handle non direct dependency
final ModuleFactory replacedFactory = this.factories.getFactory(moduleForElement);
mngrSafe = this.dependencyGraph.containsEdge(module.getFactory(), replacedFactory);
}
return mngrSafe;
}
private final ModuleReference getModuleForElement(final SQLElement elem) {
synchronized (this.modulesElements) {
for (final Entry<ModuleReference, IdentityHashMap<SQLElement, SQLElement>> e : this.modulesElements.entrySet()) {
final IdentityHashMap<SQLElement, SQLElement> map = e.getValue();
assert map instanceof IdentityHashMap : "identity needed but got " + map.getClass();
if (map.containsKey(elem))
return e.getKey();
}
}
return null;
}
public final Set<ModuleReference> getRegisteredModules() {
synchronized (this.modulesElements) {
return new HashSet<ModuleReference>(this.modulesElements.keySet());
}
}
final Set<SQLElement> getRegisteredElements(final ModuleReference ref) {
synchronized (this.modulesElements) {
final IdentityHashMap<SQLElement, SQLElement> map = this.modulesElements.get(ref);
if (map == null || map.isEmpty())
return Collections.emptySet();
return Collections.unmodifiableSet(new HashSet<SQLElement>(map.keySet()));
}
}
private void setupComponents(final AbstractModule module, final Tuple2<Set<String>, Set<SQLName>> alreadyCreatedItems, final MenuAndActions ma) throws SQLException {
assert SwingUtilities.isEventDispatchThread();
final String id = module.getFactory().getID();
if (!this.modulesComponents.containsKey(id)) {
final SQLElementDirectory dir = getDirectory();
final ComponentsContext ctxt = new ComponentsContext(dir, getRoot(), alreadyCreatedItems.get0(), alreadyCreatedItems.get1());
module.setupComponents(ctxt);
TranslationManager.addTranslationStreamFromClass(module.getClass());
this.setupMenu(module, ma);
this.modulesComponents.put(id, ctxt);
}
}
final List<ModuleReference> getAdminRequiredModules() throws IOException {
return this.getAdminRequiredModules(false);
}
/**
* Get the modules required by the administrator.
*
* @param refresh <code>true</code> if the cache should be refreshed.
* @return the references.
* @throws IOException if an error occurs.
*/
final List<ModuleReference> getAdminRequiredModules(final boolean refresh) throws IOException {
final Preferences prefs = getRequiredIDsPrefs();
if (refresh) {
try {
prefs.sync();
} catch (BackingStoreException e) {
// hide exception with a more common one
throw new IOException("Couldn't sync preferences", e);
}
}
final List<ModuleReference> res = getRefs(prefs);
L.config("getAdminRequiredModules() found " + res);
return res;
}
private final boolean isAdminRequired(ModuleReference ref) {
final long version = ref.getVersion().getMerged();
assert version >= MIN_VERSION;
return version == getRequiredIDsPrefs().getLong(ref.getID(), MIN_VERSION - 1);
}
final void setAdminRequiredModules(final Set<ModuleReference> refs, final boolean required) throws BackingStoreException {
final Set<ModuleReference> emptySet = Collections.<ModuleReference> emptySet();
setAdminRequiredModules(required ? refs : emptySet, !required ? refs : emptySet);
}
/**
* Change which modules are required. This also {@link Preferences#sync()} the preferences if
* they are modified.
*
* @param requiredRefs the modules required.
* @param notRequiredRefs the modules not required.
* @throws BackingStoreException if an error occurs.
* @see #getAdminRequiredModules(boolean)
*/
final void setAdminRequiredModules(final Set<ModuleReference> requiredRefs, final Set<ModuleReference> notRequiredRefs) throws BackingStoreException {
if (requiredRefs.size() + notRequiredRefs.size() == 0)
return;
if (!currentUserIsAdmin())
throw new IllegalStateException("Not allowed to not require " + notRequiredRefs + " and to require " + requiredRefs);
final Preferences prefs = getRequiredIDsPrefs();
putRefs(prefs, requiredRefs);
for (final ModuleReference ref : notRequiredRefs) {
prefs.remove(ref.getID());
}
prefs.sync();
}
public final void startRequiredModules() throws Exception {
// use NO_CHANGE as installation should have been handled in init()
startModules(getAdminRequiredModules(), NoChoicePredicate.NO_CHANGE, false);
}
static private final List<ModuleReference> getRefs(final Preferences prefs) throws IOException {
final String[] ids;
try {
ids = prefs.keys();
} catch (BackingStoreException e) {
// hide exception with a more common one
throw new IOException("Couldn't access preferences", e);
}
final List<ModuleReference> refs = new ArrayList<ModuleReference>(ids.length);
for (final String id : ids) {
final long merged = prefs.getLong(id, MIN_VERSION - 1);
refs.add(new ModuleReference(id, merged < MIN_VERSION ? null : new ModuleVersion(merged)));
}
return refs;
}
static private final void putRefs(final Preferences prefs, final Collection<ModuleReference> refs) throws BackingStoreException {
for (final ModuleReference ref : refs) {
prefs.putLong(ref.getID(), ref.getVersion().getMerged());
}
prefs.flush();
}
/**
* Start modules that were deemed persistent.
*
* @throws Exception if an error occurs.
* @see #startModules(Collection, boolean)
* @see #stopModule(String, boolean)
*/
public final void startPreviouslyRunningModules() throws Exception {
final List<ModuleReference> ids = getRefs(getRunningIDsPrefs());
L.config("startPreviouslyRunningModules() found " + ids);
startModules(ids, NoChoicePredicate.ONLY_INSTALL_ARGUMENTS, false);
}
public final boolean startModule(final String id) throws Exception {
return this.startModule(id, true);
}
public final boolean startModule(final String id, final boolean persistent) throws Exception {
return this.startModule(new ModuleReference(id, null), persistent);
}
public final boolean startModule(final ModuleReference id, final boolean persistent) throws Exception {
return this.startModule(id, NoChoicePredicate.ONLY_INSTALL_ARGUMENTS, persistent);
}
// return true if the module is now (or at least submitted to invokeLater()) started (even if
// the module wasn't started by this method)
public final boolean startModule(final ModuleReference id, final NoChoicePredicate noChoicePredicate, final boolean persistent) throws Exception {
final Set<ModuleReference> notStarted = startModules(Collections.singleton(id), noChoicePredicate, persistent);
final boolean res = notStarted.isEmpty();
assert res == this.runningModules.containsKey(id.getID());
return res;
}
/**
* Start the passed modules. If this method is called outside of the EDT the modules will be
* actually started using {@link SwingUtilities#invokeLater(Runnable)}, thus code that needs the
* module to be actually started must also be called inside an invokeLater().
*
* @param ids which modules to start.
* @param noChoicePredicate which modules are allowed to be installed or removed.
* @param persistent <code>true</code> to start them the next time the application is launched,
* see {@link #startPreviouslyRunningModules()}.
* @return the not started modules.
* @throws Exception if an error occurs.
*/
public synchronized final Set<ModuleReference> startModules(final Collection<ModuleReference> ids, final NoChoicePredicate noChoicePredicate, final boolean persistent) throws Exception {
// since we ask to start ids, only the not created are not started
return this.createModules(ids, noChoicePredicate, ModuleState.STARTED, persistent).get1().getNotCreated();
}
// ATTN versions are ignored (this.runningModules is used)
synchronized Set<ModuleReference> setPersistentModules(final Collection<ModuleReference> ids) throws BackingStoreException {
Map<String, ModuleVersion> modulesInstalled = null;
final Set<ModuleReference> toSet = new HashSet<ModuleReference>();
for (final ModuleReference ref : ids) {
// use installedRef, since ids can contain null version
final ModuleReference installedRef;
// first cheap in-memory check with this.runningModules
final AbstractModule m = this.runningModules.get(ref.getID());
if (m != null) {
installedRef = m.getFactory().getReference();
} else {
// else check with local file system
if (modulesInstalled == null)
modulesInstalled = getModulesVersionInstalledLocally();
installedRef = new ModuleReference(ref.getID(), modulesInstalled.get(ref.getID()));
}
if (installedRef.getVersion() != null) {
toSet.add(installedRef);
}
}
putRefs(getRunningIDsPrefs(), toSet);
return toSet;
}
static public enum InvalidRef {
/**
* The reference has no available factory.
*/
NO_FACTORY,
/**
* The reference conflicts with another reference in the same call.
*/
SELF_CONFLICT,
/**
* The reference cannot be installed (e.g. missing dependency, cycle...).
*/
NO_SOLUTION
}
// the pool to use
// refs that could be installed
// refs that cannot be installed
// => instances of <code>refs</code> not returned are duplicates or references without version
private final Tuple3<FactoriesByID, List<ModuleReference>, SetMap<InvalidRef, ModuleReference>> resolveRefs(final Collection<ModuleReference> refs) {
// remove duplicates
final Set<ModuleReference> refsSet = new HashSet<ModuleReference>(refs);
// only keep references without version if no other specifies a version
final Set<String> nonNullVersions = new HashSet<String>();
for (final ModuleReference ref : refsSet) {
if (ref.getVersion() != null)
nonNullVersions.add(ref.getID());
}
// refs with only one factory (either specifying one version or with only one version
// available)
final List<ModuleFactory> factories = new ArrayList<ModuleFactory>();
final List<ModuleReference> atLeast1 = new ArrayList<ModuleReference>();
final Iterator<ModuleReference> iter = refsSet.iterator();
while (iter.hasNext()) {
final ModuleReference ref = iter.next();
if (ref.getVersion() == null && nonNullVersions.contains(ref.getID())) {
// only use the reference that specifies a version
iter.remove();
} else {
final List<ModuleFactory> factoriesForRef = this.factories.getFactories(ref);
final int size = factoriesForRef.size();
if (size > 0) {
iter.remove();
atLeast1.add(ref);
if (size == 1) {
factories.add(factoriesForRef.get(0));
}
}
}
}
final SetMap<InvalidRef, ModuleReference> invalidRefs = new SetMap<InvalidRef, ModuleReference>(Mode.NULL_FORBIDDEN);
invalidRefs.putCollection(InvalidRef.NO_FACTORY, refsSet);
final FactoriesByID fByID = this.copyFactories();
// conflicts with requested references
final Set<ModuleFactory> conflicts = fByID.getConflicts(factories);
final Collection<ModuleFactory> selfConflicts = CollectionUtils.intersection(factories, conflicts);
for (final ModuleFactory f : selfConflicts) {
invalidRefs.add(InvalidRef.SELF_CONFLICT, f.getReference());
// don't bother trying
atLeast1.remove(f.getReference());
}
fByID.removeAll(conflicts);
// make sure that the pool is coherent with the solving graph
fByID.addAll(this.dependencyGraph.vertexSet());
return Tuple3.create(fByID, atLeast1, invalidRefs);
}
/**
* Allow to create modules without user interaction.
*
* @author Sylvain
*/
static enum NoChoicePredicate {
/** No install, no uninstall */
NO_CHANGE,
/**
* No uninstall, only install passed modules.
*/
ONLY_INSTALL_ARGUMENTS,
/**
* No uninstall, only install passed modules and their dependencies.
*/
ONLY_INSTALL
}
synchronized private final DepSolver createSolver(final int maxCount, final NoChoicePredicate s, final Collection<ModuleReference> ids) throws Exception {
final InstallationState installState = new InstallationState(this);
final DepSolver depSolver = new DepSolver().setMaxSuccess(maxCount);
depSolver.setResultFactory(new Factory() {
@Override
public DepSolverResult create(DepSolverResult parent, int tryCount, String error, DepSolverGraph graph) {
final DepSolverResultMM res = new DepSolverResultMM((DepSolverResultMM) parent, tryCount, error, graph);
res.init(ModuleManager.this, installState, s, ids);
return res;
}
});
depSolver.setResultFilter(DepSolverResultMM.VALID_PRED);
return depSolver;
}
synchronized final Tuple2<Solutions, ModulesStateChangeResult> createModules(final Collection<ModuleReference> ids, final NoChoicePredicate s, final ModuleState targetState) throws Exception {
return this.createModules(ids, s, targetState, checkPersistentNeeded(targetState));
}
// allow to not pass unneeded argument
private boolean checkPersistentNeeded(final ModuleState targetState) {
if (targetState.compareTo(ModuleState.STARTED) >= 0)
throw new IllegalArgumentException("For STARTED the persistent parameter must be supplied");
return false;
}
// not public since it returns instance of AbstractModules
synchronized final Tuple2<Solutions, ModulesStateChangeResult> createModules(final Collection<ModuleReference> ids, final NoChoicePredicate s, final ModuleState targetState,
final boolean startPersistent) throws Exception {
// Don't uninstall automatically, use getSolutions() then applyChange()
if (s == null)
throw new NullPointerException();
if (ids.size() == 0 || targetState == ModuleState.NOT_CREATED)
return Tuple2.create(Solutions.EMPTY, ModulesStateChangeResult.empty());
final DepSolver depSolver = createSolver(1, s, ids);
final Solutions solutions = getSolutions(depSolver, ids);
final SetMap<InvalidRef, ModuleReference> cannotCreate = solutions.getNotSolvedReferences();
final ModulesStateChangeResult changeRes;
// don't partially install
if (cannotCreate != null && !cannotCreate.isEmpty()) {
changeRes = ModulesStateChangeResult.noneCreated(new HashSet<ModuleReference>(ids));
} else {
// at least one solution otherwise cannotCreate wouldn't be empty
changeRes = this.applyChange((DepSolverResultMM) solutions.getSolutions().get(0), targetState, startPersistent);
}
return Tuple2.create(solutions, changeRes);
}
synchronized final Solutions getSolutions(final Collection<ModuleReference> ids, final int maxCount) throws Exception {
return this.getSolutions(createSolver(maxCount, null, ids), ids);
}
synchronized private final Solutions getSolutions(final DepSolver depSolver, final Collection<ModuleReference> ids) throws Exception {
if (ids.size() == 0)
return Solutions.EMPTY;
final Tuple3<FactoriesByID, List<ModuleReference>, SetMap<InvalidRef, ModuleReference>> resolvedRefs = resolveRefs(ids);
final FactoriesByID pool = resolvedRefs.get0();
final List<ModuleReference> atLeast1 = resolvedRefs.get1();
final SetMap<InvalidRef, ModuleReference> invalidRefs = resolvedRefs.get2();
final List<DepSolverResult> solutions;
if (atLeast1.isEmpty()) {
// we were passed non empty references to install but no candidates remain. If we passed
// an empty list to DepSolver it will immediately return successfully.
solutions = Collections.emptyList();
} else {
solutions = depSolver.solve(pool, this.dependencyGraph, atLeast1);
}
if (solutions.size() == 0) {
invalidRefs.putCollection(InvalidRef.NO_SOLUTION, atLeast1);
}
invalidRefs.removeAllEmptyCollections();
return new Solutions(invalidRefs, solutions.size() == 0 ? Collections.<ModuleReference> emptyList() : atLeast1, solutions);
}
synchronized final ModulesStateChangeResult applyChange(final ModulesStateChange change, final ModuleState targetState) throws Exception {
return applyChange(change, targetState, checkPersistentNeeded(targetState));
}
// not public since it returns instances of AbstractModule
// @param targetState target state for modules in graph
// @param startPersistent only used if <code>targetState</code> is STARTED
synchronized final ModulesStateChangeResult applyChange(final ModulesStateChange change, final ModuleState targetState, final boolean startPersistent) throws Exception {
if (change == null || change.getError() != null) {
return null;
} else if (!new InstallationState(this).equals(change.getInstallState())) {
throw new IllegalStateException("Installation state has changed since getSolutions()");
}
// call it before stopping/uninstalling
final boolean exit = this.isExitAllowed() && this.needExit(change);
final DepSolverGraph graph = change.getGraph();
final Set<ModuleReference> toRemove = change.getReferencesToRemove();
final Set<ModuleReference> removed;
if (toRemove.size() > 0) {
final Set<String> idsToInstall = change.getIDsToInstall();
removed = new HashSet<ModuleReference>();
for (final ModuleReference ref : toRemove) {
// don't uninstall modules to upgrade but since this loop might uninstall modules
// needed by ref, at least stop it like uninstallUnsafe() does
if (idsToInstall.contains(ref.getID()))
this.stopModule(ref.getID(), false);
else if (this.uninstallUnsafe(ref, !change.forceRemove(), change.getInstallState()))
removed.add(ref);
}
} else {
removed = Collections.emptySet();
}
// MAYBE compare states with targetState to avoid going further (e.g ids are all started)
if (exit) {
// restart to make sure the uninstalled modules are really gone from the memory and
// none of its effects present. We could check that the class loader for the module
// is garbage collected, but
// 1. this cannot work if the module is in the class path
// 2. an ill-behaved modules might have modified a static value
assert noDisplayableFrame() : "A change needs to exit but there still a displayable frame : " + change;
final Set<ModuleReference> toInstall = change.getReferencesToInstall();
// don't use only getReferencesToInstall() as even if no modules need installing, their
// state might need to change (e.g. start)
if (toInstall.size() > 0 || (targetState.compareTo(ModuleState.INSTALLED) > 0 && change.getUserReferencesToInstall().size() > 0)) {
// record current time and actions
final File f = getToInstallFile();
try (final XMLEncoder xmlEncoder = new XMLEncoder(new FileOutputStream(f))) {
xmlEncoder.setExceptionListener(XMLCodecUtils.EXCEPTION_LISTENER);
xmlEncoder.setPersistenceDelegate(ModuleVersion.class, ModuleVersion.PERSIST_DELEGATE);
xmlEncoder.setPersistenceDelegate(ModuleReference.class, ModuleReference.PERSIST_DELEGATE);
xmlEncoder.writeObject(TO_INSTALL_VERSION);
xmlEncoder.writeObject(new Date());
xmlEncoder.writeObject(toInstall);
xmlEncoder.writeObject(change.getUserReferencesToInstall());
xmlEncoder.writeObject(targetState);
xmlEncoder.writeObject(startPersistent);
} catch (Exception e) {
// try to delete invalid file before throwing exception
// "any catch or finally block is run after the resources have been closed."
f.delete();
throw e;
}
}
return new ModulesStateChangeResult(removed, change.getReferencesToInstall(), graph, Collections.emptyMap());
}
// don't use getReferencesToInstall() as even if no modules need installing, their state
// might need to change (e.g. start)
if (targetState.compareTo(ModuleState.CREATED) < 0)
return ModulesStateChangeResult.onlyRemoved(removed);
if (graph == null)
throw new IllegalArgumentException("target state is " + targetState + " but no graph was provided by " + change);
// modules created by this method
final Map<ModuleReference, AbstractModule> modules = new LinkedHashMap<ModuleReference, AbstractModule>(graph.getFactories().size());
// MAYBE try to continue even if some modules couldn't be created
final Set<ModuleReference> cannotCreate = Collections.emptySet();
final List<AbstractModule> toStart = new ArrayList<AbstractModule>();
for (final ModuleFactory useableFactory : graph.flatten()) {
final String id = useableFactory.getID();
// already created
if (!this.dependencyGraph.containsVertex(useableFactory)) {
final Map<Object, ModuleFactory> dependenciesFactory = graph.getDependencies(useableFactory);
final Map<Object, AbstractModule> dependenciesModule = new HashMap<Object, AbstractModule>(dependenciesFactory.size());
for (final Entry<Object, ModuleFactory> e : dependenciesFactory.entrySet()) {
final AbstractModule module = this.createdModules.get(e.getValue());
assert module != null;
dependenciesModule.put(e.getKey(), module);
}
final AbstractModule createdModule = useableFactory.createModule(this.getLocalDataDirectory(id), Collections.unmodifiableMap(dependenciesModule));
modules.put(useableFactory.getReference(), createdModule);
this.createdModules.put(useableFactory, createdModule);
// update graph
final boolean added = this.dependencyGraph.addVertex(useableFactory);
assert added : "Module was already in graph : " + useableFactory;
for (final Entry<Object, ModuleFactory> e : dependenciesFactory.entrySet()) {
this.dependencyGraph.addEdge(useableFactory, e.getKey(), e.getValue());
}
}
// even if the module was created in a previous invocation, it might not have been
// started then
if (!this.runningModules.containsKey(id))
toStart.add(this.createdModules.get(useableFactory));
}
// don't test toStart emptiness as even if all modules were started, they might need to be
// made persistent
if (targetState.compareTo(ModuleState.INSTALLED) >= 0) {
// register each module just after install, so that the next module can use its elements
// in its install
for (final AbstractModule module : toStart) {
installAndRegister(module, graph);
}
if (targetState == ModuleState.STARTED) {
start(toStart);
if (startPersistent)
// only mark persistent passed modules (not their dependencies)
this.setPersistentModules(change.getUserReferencesToInstall());
}
}
// ATTN modules indexed by resolved references, not the ones passed
return new ModulesStateChangeResult(removed, cannotCreate, graph, modules);
}
synchronized final void startFactories(final List<ModuleFactory> toStart) throws Exception {
final List<AbstractModule> modules = new ArrayList<AbstractModule>(toStart.size());
for (final ModuleFactory f : toStart) {
final AbstractModule m = this.createdModules.get(f);
if (m == null)
throw new IllegalStateException("Not created : " + f);
else if (!this.isModuleRunning(f.getID()))
modules.add(m);
}
this.start(modules);
}
synchronized private final void start(final List<AbstractModule> toStart) throws Exception {
if (toStart.size() == 0)
return;
// check install state before starting
final Set<ModuleReference> registeredModules = this.getRegisteredModules();
for (final AbstractModule m : toStart) {
final ModuleReference ref = m.getFactory().getReference();
if (!registeredModules.contains(ref))
throw new IllegalStateException("Not installed and registered : " + ref);
}
// a module can always start if installed
final FutureTask<MenuAndActions> menuAndActions = new FutureTask<MenuAndActions>(new Callable<MenuAndActions>() {
@Override
public MenuAndActions call() throws Exception {
return MenuManager.getInstance().copyMenuAndActions();
}
});
SwingThreadUtils.invoke(menuAndActions);
for (final AbstractModule module : toStart) {
final ModuleFactory f = module.getFactory();
final String id = f.getID();
try {
// do the request here instead of in the EDT in setupComponents()
assert !this.runningModules.containsKey(id) : "Doing a request for nothing";
final Tuple2<Set<String>, Set<SQLName>> createdItems = getCreatedItems(id);
// execute right away if possible, allowing the caller to handle any exceptions
if (SwingUtilities.isEventDispatchThread()) {
startModule(module, createdItems, menuAndActions.get());
} else {
// keep the for outside to avoid halting the EDT too long
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
try {
startModule(module, createdItems, menuAndActions.get());
} catch (Exception e) {
ExceptionHandler.handle(MainFrame.getInstance(), "Unable to start " + f, e);
}
}
});
}
} catch (Exception e) {
throw new Exception("Couldn't start module " + module, e);
}
this.runningModules.put(id, module);
}
SwingThreadUtils.invoke(new Runnable() {
@Override
public void run() {
try {
MenuManager.getInstance().setMenuAndActions(menuAndActions.get());
} catch (Exception e) {
ExceptionHandler.handle(MainFrame.getInstance(), "Unable to update menu", e);
}
}
});
}
private final Set<SQLTable> loadTranslations(final SQLFieldTranslator trns, final AbstractModule module, final String mdVariant) throws IOException {
final Locale locale = getConf().getLocale();
final Control cntrl = TranslationManager.getControl();
final String baseName = "labels";
final Set<SQLTable> res = new HashSet<SQLTable>();
boolean found = false;
for (Locale targetLocale = locale; targetLocale != null && !found; targetLocale = cntrl.getFallbackLocale(baseName, targetLocale)) {
final List<Locale> langs = cntrl.getCandidateLocales(baseName, targetLocale);
// SQLFieldTranslator overwrite, so we need to load from general to specific
final ListIterator<Locale> listIterator = CollectionUtils.getListIterator(langs, true);
while (listIterator.hasNext()) {
final Locale lang = listIterator.next();
final SQLElementNamesFromXML elemNames = new SQLElementNamesFromXML(lang);
final String bundleName = cntrl.toBundleName(baseName, lang);
final String resourceName = cntrl.toResourceName(bundleName, "xml");
final InputStream ins = module.getClass().getResourceAsStream(resourceName);
// do not force to have one mapping for each locale
if (ins != null) {
L.config("module " + module.getName() + " loading translation from " + resourceName);
final Set<SQLTable> loadedTables;
try {
loadedTables = trns.load(getRoot(), mdVariant, ins, elemNames).get0();
} finally {
ins.close();
}
if (loadedTables.size() > 0) {
res.addAll(loadedTables);
found |= true;
}
// As in PropsConfiguration.loadTranslations(), perhaps load the class at
// module.getClass().getPackage().getName() + '.' + bundleName
// to allow more flexibility. Perhaps pass a ModuleSQLFieldTranslator to
// restrict what a module can do (and have SQLElementNamesFromXML, mdVariant).
}
}
}
return res;
}
private final void installAndRegister(final AbstractModule module, DepSolverGraph graph) throws Exception {
assert Thread.holdsLock(this);
assert !isModuleRunning(module.getFactory().getID());
// Snapshot now to allow install() to register and use its own elements
// Also needed for checks, since install() can do arbitrary changes to the directory
final Map<SQLTable, SQLElement> beforeElements = new HashMap<SQLTable, SQLElement>(getDirectory().getElementsMap());
try {
install(module, graph);
} catch (Exception e) {
throw new Exception("Couldn't install module " + module, e);
}
try {
this.registerSQLElements(module, beforeElements);
} catch (Exception e) {
throw new Exception("Couldn't register module " + module, e);
}
}
private final void startModule(final AbstractModule module, final Tuple2<Set<String>, Set<SQLName>> createdItems, final MenuAndActions menuAndActions) throws Exception {
assert SwingUtilities.isEventDispatchThread();
this.setupComponents(module, createdItems, menuAndActions);
module.start();
}
private final void setupMenu(final AbstractModule module, final MenuAndActions menuAndActions) {
module.setupMenu(new MenuContext(menuAndActions, module.getFactory().getID(), getDirectory(), getRoot()));
}
public synchronized final boolean isModuleRunning(final String id) {
return this.runningModules.containsKey(id);
}
/**
* The modules that are currently running. NOTE : if {@link #startModules(Collection, boolean)}
* or {@link #stopModule(String, boolean)} wasn't called from the EDT the modules will only be
* actually started/stopped when the EDT executes the invokeLater(). In other words a module can
* be in the result but not yet on screen, or module can no longer be in the result but still on
* screen.
*
* @return the started modules.
*/
public synchronized final Map<String, AbstractModule> getRunningModules() {
return new HashMap<String, AbstractModule>(this.runningModules);
}
/**
* The running modules depending on the passed one. E.g. if it isn't running returns an empty
* list.
*
* @param id a module.
* @return the running modules needing <code>id</code> (including itself), in stop order (i.e.
* the first item isn't depended on).
*/
public synchronized final List<ModuleReference> getRunningDependentModulesRecursively(final String id) {
if (!this.isModuleRunning(id))
return Collections.emptyList();
final ModuleFactory f = this.runningModules.get(id).getFactory();
return getRunningDependentModulesRecursively(f.getReference(), new LinkedList<ModuleReference>());
}
private synchronized final List<ModuleReference> getRunningDependentModulesRecursively(final ModuleReference ref, final List<ModuleReference> res) {
// can happen if a module depends on two others and they share a dependency, e.g.
// __ B
// A < > D
// __ C
if (!res.contains(ref) && this.isModuleRunning(ref.getID())) {
final ModuleFactory f = this.runningModules.get(ref.getID()).getFactory();
// the graph has no cycle, so we don't need to protected against infinite loop
final Set<ModuleReference> deps = new TreeSet<ModuleReference>(ModuleReference.COMP_ID_ASC_VERSION_DESC);
for (final DirectedEdge<ModuleFactory> e : this.dependencyGraph.incomingEdgesOf(f)) {
deps.add(e.getSource().getReference());
}
for (final ModuleReference dep : deps) {
this.getRunningDependentModulesRecursively(dep, res);
}
res.add(f.getReference());
}
return res;
}
public final Set<ModuleReference> stopAllModules() {
// this method is not synchronized, so don't just return getRunningModules()
final Set<ModuleReference> res = new HashSet<>();
for (final String id : this.getRunningModules().keySet()) {
res.addAll(this.stopModuleRecursively(id));
}
return res;
}
public synchronized final List<ModuleReference> stopModuleRecursively(final String id) {
final List<ModuleReference> res = getRunningDependentModulesRecursively(id);
for (final ModuleReference ref : res) {
this.stopModule(ref.getID());
}
return res;
}
public final boolean stopModule(final String id) {
return this.stopModule(id, true);
}
// TODO pass ModuleReference instead of ID (need to change this.runningModules)
public synchronized final boolean stopModule(final String id, final boolean persistent) {
if (!this.isModuleRunning(id))
return false;
final ModuleFactory f = this.runningModules.get(id).getFactory();
if (this.isAdminRequired(f.getReference()) && !currentUserIsAdmin())
throw new IllegalStateException("Not allowed to stop a module required by the administrator " + f);
final Set<DepLink> deps = this.dependencyGraph.incomingEdgesOf(f);
for (final DepLink l : deps) {
if (this.isModuleRunning(l.getSource().getID()))
throw new IllegalArgumentException("Some dependents still running : " + deps);
}
final AbstractModule m = this.runningModules.remove(id);
try {
// execute right away if possible, allowing the caller to handle any exceptions
if (SwingUtilities.isEventDispatchThread()) {
stopModule(m);
} else {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
try {
stopModule(m);
} catch (Exception e) {
ExceptionHandler.handle(MainFrame.getInstance(), "Unable to stop " + f, e);
}
}
});
}
} catch (Exception e) {
throw new IllegalStateException("Couldn't stop module " + m, e);
}
// we can't undo what the module has done, so just start from the base menu and re-apply all
// modifications
final MenuAndActions menuAndActions = MenuManager.getInstance().createBaseMenuAndActions();
final ArrayList<AbstractModule> modules = new ArrayList<AbstractModule>(this.runningModules.values());
SwingThreadUtils.invoke(new Runnable() {
@Override
public void run() {
for (final AbstractModule m : modules) {
setupMenu(m, menuAndActions);
}
MenuManager.getInstance().setMenuAndActions(menuAndActions);
}
});
if (persistent)
getRunningIDsPrefs().remove(m.getFactory().getID());
assert !this.isModuleRunning(id);
return true;
}
private final void stopModule(final AbstractModule m) {
// this must not attempt to lock this monitor, see uninstallUnsafe()
assert SwingUtilities.isEventDispatchThread();
m.stop();
this.tearDownComponents(m);
}
private void unregisterSQLElements(final AbstractModule module) {
final ModuleReference id = module.getFactory().getReference();
synchronized (this.modulesElements) {
if (this.modulesElements.containsKey(id)) {
final IdentityHashMap<SQLElement, SQLElement> elements = this.modulesElements.remove(id);
final SQLElementDirectory dir = getDirectory();
for (final Entry<SQLElement, SQLElement> e : elements.entrySet()) {
dir.removeSQLElement(e.getKey());
// restore replaced element if any
if (e.getValue() != null) {
dir.addSQLElement(e.getValue());
}
}
final String mdVariant = getMDVariant(module.getFactory());
// perhaps record which element this module modified in start()
for (final SQLElement elem : this.getDirectory().getElements()) {
elem.removeFromMDPath(mdVariant);
}
getConf().getTranslator().removeDescFor(null, null, mdVariant, null);
}
}
}
private void tearDownComponents(final AbstractModule module) {
assert SwingUtilities.isEventDispatchThread();
final String id = module.getFactory().getID();
if (this.modulesComponents.containsKey(id)) {
final ComponentsContext ctxt = this.modulesComponents.remove(id);
for (final Entry<SQLElement, ? extends Collection<String>> e : ctxt.getFields().entrySet())
for (final String fieldName : e.getValue())
e.getKey().removeAdditionalField(fieldName);
for (final Entry<SQLElement, ? extends Collection<IListeAction>> e : ctxt.getRowActions().entrySet())
e.getKey().getRowActions().removeAll(e.getValue());
TranslationManager.removeTranslationStreamFromClass(module.getClass());
// can't undo so menu is reset in stopModule()
}
}
private final List<ModuleReference> getDBDependentModules(final ModuleReference ref) throws Exception {
// dependencies are stored in the DB that way we can uninstall dependent modules even
// without their factories
final SQLTable installedTable = getInstalledTable(getRoot());
final TableRef needingModule = new AliasedTable(installedTable, "needingModule");
final SQLTable depT = getDepTable();
final SQLSelect sel = new SQLSelect();
sel.setWhere(getModuleRowWhere(installedTable).and(new Where(installedTable.getField(MODULE_COLNAME), "=", ref.getID())));
if (ref.getVersion() != null)
sel.andWhere(new Where(installedTable.getField(MODULE_VERSION_COLNAME), "=", ref.getVersion().getMerged()));
sel.addBackwardJoin("INNER", depT.getField(NEEDED_MODULE_COLNAME), null);
sel.addJoin("INNER", new Where(depT.getField(NEEDING_MODULE_COLNAME), "=", needingModule.getKey()));
sel.addSelect(needingModule.getKey());
sel.addSelect(needingModule.getField(MODULE_COLNAME));
sel.addSelect(needingModule.getField(MODULE_VERSION_COLNAME));
@SuppressWarnings("unchecked")
final List<Map<String, Object>> rows = installedTable.getDBSystemRoot().getDataSource().execute(sel.asString());
final List<ModuleReference> res = new ArrayList<ModuleReference>(rows.size());
for (final Map<String, Object> row : rows) {
res.add(getRef(new SQLRow(needingModule.getTable(), row)));
}
return res;
}
// ATTN the result is not in removal order since it might contain itself dependent modules, e.g.
// getDependentModules(C) will return A, B but the removal order is B, A :
// A
// ^
// |> C
// B
private synchronized final Set<ModuleReference> getDependentModules(final ModuleReference ref) throws Exception {
// predictable order
final Set<ModuleReference> res = new TreeSet<ModuleReference>(ModuleReference.COMP_ID_ASC_VERSION_DESC);
// ATTN if in the future we make local-only modules, we will have to record the dependencies
// in the local file system
res.addAll(getDBDependentModules(ref));
return res;
}
/**
* The list of installed modules depending on the passed one.
*
* @param ref the module.
* @return the modules needing <code>ref</code> (excluding it), in uninstallation order (i.e.
* the first item isn't depended on).
* @throws Exception if an error occurs.
*/
public final List<ModuleReference> getDependentModulesRecursively(final ModuleReference ref) throws Exception {
return getDependentModulesRecursively(ref, new ArrayList<ModuleReference>());
}
private synchronized final List<ModuleReference> getDependentModulesRecursively(final ModuleReference ref, final List<ModuleReference> res) throws Exception {
for (final ModuleReference depModule : getDependentModules(ref)) {
// can happen if a module depends on two others and they share a dependency, e.g.
// __ B
// A < > D
// __ C
if (!res.contains(depModule)) {
// the graph has no cycle, so we don't need to protected against infinite loop
final List<ModuleReference> depModules = this.getDependentModulesRecursively(depModule, res);
assert !depModules.contains(depModule) : "cycle with " + depModule;
res.add(depModule);
}
}
return res;
}
// ids + modules depending on them in uninstallation order
// ATTN return ids even if not installed
synchronized final LinkedHashSet<ModuleReference> getAllOrderedDependentModulesRecursively(final Set<ModuleReference> ids) throws Exception {
final LinkedHashSet<ModuleReference> depModules = new LinkedHashSet<ModuleReference>();
for (final ModuleReference id : ids) {
if (!depModules.contains(id)) {
depModules.addAll(getDependentModulesRecursively(id));
// even without this line the result could still contain some of ids if it contained
// a module and one of its dependencies
depModules.add(id);
}
}
return depModules;
}
public synchronized final Set<ModuleReference> uninstall(final Set<ModuleReference> ids, final boolean recurse) throws Exception {
return this.uninstall(ids, recurse, false);
}
public synchronized final Set<ModuleReference> uninstall(final Set<ModuleReference> ids, final boolean recurse, final boolean force) throws Exception {
return this.applyChange(this.getUninstallSolution(ids, recurse, force), ModuleState.NOT_CREATED).getRemoved();
}
// ATTN this doesn't use canCurrentUserInstall(), as (at least for now) there's one and only one
// solution. That way, the UI can list the modules that need to be uninstalled.
public synchronized final ModulesStateChange getUninstallSolution(final Set<ModuleReference> passedRefs, final boolean recurse, final boolean force) throws Exception {
// compute now, at the same time as the solution not in each
// ModulesStateChange.getInstallState()
final InstallationState installationState = new InstallationState(this);
final Set<ModuleReference> ids = new HashSet<ModuleReference>();
for (final ModuleReference ref : passedRefs) {
if (ref.getVersion() == null)
throw new UnsupportedOperationException("Version needed for " + ref);
if (installationState.getLocalOrRemote().contains(ref)) {
ids.add(ref);
}
}
final int size = ids.size();
final Set<ModuleReference> toRemove;
// optimize by not calling recursively getDependentModules()
if (!recurse && size == 1) {
final Set<ModuleReference> depModules = this.getDependentModules(ids.iterator().next());
if (depModules.size() > 0)
throw new IllegalStateException("Dependent modules not uninstalled : " + depModules);
toRemove = ids;
} else if (size > 0) {
toRemove = getAllOrderedDependentModulesRecursively(ids);
} else {
toRemove = Collections.emptySet();
}
// if size == 1, already tested
if (!recurse && size > 1) {
final Collection<ModuleReference> depModulesNotRequested = CollectionUtils.substract(toRemove, ids);
if (!depModulesNotRequested.isEmpty())
throw new IllegalStateException("Dependent modules not uninstalled : " + depModulesNotRequested);
}
return new ModulesStateChange() {
@Override
public String getError() {
return null;
}
@Override
public InstallationState getInstallState() {
return installationState;
}
@Override
public Set<ModuleReference> getUserReferencesToInstall() {
return Collections.emptySet();
}
@Override
public Set<ModuleReference> getReferencesToRemove() {
return toRemove;
}
@Override
public boolean forceRemove() {
return force;
}
@Override
public Set<ModuleReference> getReferencesToInstall() {
return Collections.emptySet();
}
@Override
public DepSolverGraph getGraph() {
return null;
}
@Override
public String toString() {
return "Uninstall solution for " + this.getReferencesToRemove();
}
};
}
public final void uninstall(final ModuleReference ref) throws Exception {
this.uninstall(ref, false);
}
public synchronized final Set<ModuleReference> uninstall(final ModuleReference id, final boolean recurse) throws Exception {
return this.uninstall(id, recurse, false);
}
public synchronized final Set<ModuleReference> uninstall(final ModuleReference id, final boolean recurse, final boolean force) throws Exception {
return this.uninstall(Collections.singleton(id), recurse, force);
}
// return the version in installed that matches ref
private final ModuleReference filter(final Set<ModuleReference> installed, final ModuleReference ref) {
for (final ModuleReference installedRef : installed) {
if (installedRef.getID().equals(ref.getID()) && (ref.getVersion() == null || installedRef.getVersion().equals(ref.getVersion())))
return installedRef;
}
return null;
}
// unsafe because this method doesn't check dependents
// dbVersions parameter to avoid requests to the DB
// return true if the mref was actually uninstalled (i.e. it was installed locally or remotely)
private boolean uninstallUnsafe(final ModuleReference mref, final boolean requireModule, final InstallationState installState) throws SQLException, Exception {
assert Thread.holdsLock(this);
final String id = mref.getID();
// versions to uninstall
final ModuleReference localRef = filter(installState.getLocal(), mref);
final ModuleReference dbRef = filter(installState.getRemote(), mref);
final ModuleVersion dbVersion = dbRef == null ? null : dbRef.getVersion();
// otherwise it will get re-installed the next launch
getRunningIDsPrefs().remove(id);
final Set<ModuleReference> refs = new HashSet<ModuleReference>(2);
if (localRef != null)
refs.add(localRef);
if (dbRef != null)
refs.add(dbRef);
setAdminRequiredModules(refs, false);
// only return after having cleared required, so that we don't need to install just to
// not require
if (localRef == null && dbRef == null)
return false;
if (dbRef != null && !currentUserIsAdmin())
throw new IllegalStateException("Not allowed to uninstall " + id + " from the database");
// DB module
final AbstractModule module;
if (!this.isModuleRunning(id)) {
if (dbRef == null) {
assert localRef != null;
// only installed locally
module = null;
} else {
final SortedMap<ModuleVersion, ModuleFactory> available = this.factories.getVersions(id);
final ModuleReference ref;
if (available.containsKey(dbVersion)) {
ref = dbRef;
} else {
// perhaps modules should specify which versions they can uninstall
final SortedMap<ModuleVersion, ModuleFactory> moreRecent = available.headMap(dbVersion);
if (moreRecent.size() == 0) {
ref = null;
} else {
// take the closest
ref = new ModuleReference(id, moreRecent.lastKey());
}
}
if (ref != null) {
assert ref.getVersion().compareTo(dbVersion) >= 0;
final ModuleFactory f = available.get(ref.getVersion());
assert f != null;
// only call expensive method if necessary
if (!this.createdModules.containsKey(f)) {
// * Don't use the result, instead use this.createdModules since the module
// might have been created before.
// * Cannot call directly applyChange(), we need DepSolver to create modules
// that ref depends on, as they might be required by
// AbstractModule.uninstall().
// * Cannot pass NoChoicePredicate.NO_CHANGE as ref won't be created if not
// already installed both locally and remotely. No installation will occur
// since we pass ModuleState.CREATED.
this.createModules(Collections.singleton(ref), NoChoicePredicate.ONLY_INSTALL, ModuleState.CREATED);
}
module = this.createdModules.get(f);
} else {
module = null;
}
if (module == null && requireModule) {
final String reason;
if (ref == null) {
reason = "No version recent enough to uninstall " + dbVersion + " : " + available.keySet();
} else {
// TODO include InvalidRef in ModulesStateChangeResult
reason = "Creation of " + ref + " failed (e.g. missing factory, dependency)";
}
throw new IllegalStateException("Couldn't get module " + id + " : " + reason);
}
}
} else {
final ModuleVersion localVersion = localRef.getVersion();
if (!localVersion.equals(dbVersion))
L.warning("Someone else has changed the database version while we were running :" + localVersion + " != " + dbVersion);
module = this.runningModules.get(id);
assert localVersion.equals(module.getFactory().getVersion());
this.stopModule(id, false);
// The module has to be stopped before we can proceed
// ATTN we hold this monitor, so stop() should never try to acquire it in the EDT
if (!SwingUtilities.isEventDispatchThread()) {
SwingUtilities.invokeAndWait(EMPTY_RUNNABLE);
}
}
assert (module == null) == (!requireModule || dbRef == null);
SQLUtils.executeAtomic(getDS(), new SQLFactory<Object>() {
@Override
public Object create() throws SQLException {
final DBRoot root = getRoot();
if (module != null) {
module.uninstall(root);
unregisterSQLElements(module);
}
if (localRef != null)
setModuleInstalledLocally(localRef, false);
// uninstall from DB
if (dbRef != null) {
final Tuple2<Set<String>, Set<SQLName>> createdItems = getCreatedItems(id);
final List<ChangeTable<?>> l = new ArrayList<ChangeTable<?>>();
final Set<String> tableNames = createdItems.get0();
for (final SQLName field : createdItems.get1()) {
final SQLField f = root.getDesc(field, SQLField.class);
// dropped by DROP TABLE
if (!tableNames.contains(f.getTable().getName())) {
// cascade needed since the module might have created constraints
// (e.g. on H2 a foreign column cannot be dropped)
l.add(new AlterTable(f.getTable()).dropColumnCascade(f.getName()));
}
}
for (final String table : tableNames) {
l.add(new DropTable(root.getTable(table)));
}
if (l.size() > 0) {
for (final String s : ChangeTable.cat(l, root.getName()))
root.getDBSystemRoot().getDataSource().execute(s);
root.getSchema().updateVersion();
root.refetch();
}
removeModuleFields(dbRef);
}
return null;
}
});
return true;
}
}