Dépôt officiel du code source de l'ERP OpenConcerto
Rev 142 | 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.
*/
/*
* Created on 7 mai 03
*/
package org.openconcerto.sql.model;
import org.openconcerto.sql.model.LoadingListener.StructureLoadingEvent;
import org.openconcerto.sql.model.graph.TablesMap;
import org.openconcerto.sql.utils.SQL_URL;
import org.openconcerto.utils.CollectionUtils;
import org.openconcerto.utils.NetUtils;
import org.openconcerto.utils.Tuple2.List2;
import org.openconcerto.utils.cc.CopyOnWriteMap;
import org.openconcerto.utils.cc.IClosure;
import org.openconcerto.utils.cc.ITransformer;
import org.openconcerto.utils.change.CollectionChangeEventCreator;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import net.jcip.annotations.GuardedBy;
/**
* Un serveur de base de donnée SQL. Meaning a system (eg mysql) on a certain host and port. Un
* serveur permet d'accéder aux bases qui le composent (une base par défaut peut être spécifiée). De
* plus il permet de spécifier un login/pass par défaut.
*
* @author ilm
* @see #getOrCreateBase(String)
*/
public final class SQLServer extends DBStructureItemJDBC {
private static final IClosure<SQLDataSource> DSINIT_ERROR = new IClosure<SQLDataSource>() {
@Override
public void executeChecked(SQLDataSource input) {
throw new IllegalStateException("Datasource should already be created");
}
};
static private final <T> IClosure<? super T> coalesce(IClosure<? super T> o1, IClosure<? super T> o2) {
// ternary operator makes Eclipse fail
if (o1 != null)
return o1;
else
return o2;
}
public static final DBSystemRoot create(final SQL_URL url) {
return create(url, Collections.<String> emptySet(), null);
}
/**
* Create a system root from the passed URL.
*
* @param url an SQL URL.
* @param rootsToMap the collection of {@link DBSystemRoot#setRootsToMap(Collection) roots to
* map}, in addition to <code>url.{@link SQL_URL#getRootName() getRootName()}</code>.
* @param dsInit to initialize the datasource before any request (e.g. setting JDBC properties),
* can be <code>null</code>.
* @return the new system root.
*/
public static final DBSystemRoot create(final SQL_URL url, final Collection<String> rootsToMap, IClosure<SQLDataSource> dsInit) {
return create(url, rootsToMap, false, dsInit);
}
public static final DBSystemRoot create(final SQL_URL url, final Collection<String> roots, final boolean setPath, IClosure<SQLDataSource> dsInit) {
final DBSystemRoot res = create(url, new IClosure<DBSystemRoot>() {
@Override
public void executeChecked(DBSystemRoot input) {
if (url.getRootName() != null) {
input.setRootToMap(url.getRootName());
input.addRootsToMap(roots);
} else {
input.setRootsToMap(roots);
}
}
}, dsInit);
if (setPath) {
final List<String> path = new ArrayList<String>(roots);
if (url.getRootName() != null)
path.add(0, url.getRootName());
path.retainAll(res.getChildrenNames());
if (path.size() > 0)
res.setRootPath(path);
}
return res;
}
public static final DBSystemRoot create(final SQL_URL url, final IClosure<? super DBSystemRoot> systemRootInit, final IClosure<? super SQLDataSource> dsInit) {
return new SQLServer(url.getSystem(), url.getServerName(), null, url.getLogin(), url.getPass(), systemRootInit, dsInit).getSystemRoot(url.getSystemRootName());
}
// *** Instance members
// eg mysql, derby
private final SQLSystem system;
private final String login;
private final String pass;
private final IClosure<? super DBSystemRoot> systemRootInit;
@GuardedBy("baseMutex")
private CopyOnWriteMap<String, SQLBase> bases;
private Object baseMutex = new String("base mutex");
// linked to bases
@GuardedBy("this")
private String defaultBase;
// linked to dsSet
@GuardedBy("this")
private SQLDataSource ds;
@GuardedBy("this")
private boolean dsSet;
private final IClosure<? super SQLDataSource> dsInit;
private final ITransformer<String, String> urlTransf;
@GuardedBy("this")
private String id;
public SQLServer(SQLSystem system, String host) {
this(system, host, null);
}
public SQLServer(SQLSystem system, String host, String port) {
this(system, host, port, null, null);
}
public SQLServer(SQLSystem system, String host, String port, String login, String pass) {
this(system, host, port, login, pass, null, null);
}
/**
* Creates a new server.
*
* @param system the database system.
* @param host an IP address or DNS name.
* @param port the port to connect to can be <code>null</code> to pick the system default.
* @param login the default login to access database of this server, can be <code>null</code>.
* @param pass the default password to access database of this server, can be <code>null</code>.
* @param systemRootInit to initialize the system root in its constructor, can be
* <code>null</code>.
* @param dsInit to initialize the datasource before any request (e.g. setting JDBC properties),
* must be thread-safe, can be <code>null</code>.
*/
public SQLServer(SQLSystem system, String host, String port, String login, String pass, IClosure<? super DBSystemRoot> systemRootInit, IClosure<? super SQLDataSource> dsInit) {
super(null, host);
this.ds = null;
this.dsSet = false;
this.dsInit = dsInit;
this.system = system;
this.login = login;
this.pass = pass;
this.bases = null;
this.systemRootInit = systemRootInit;
this.urlTransf = this.getSQLSystem().getURLTransf(this);
this.id = this.getName();
// cannot refetch now as we don't have any datasource yet (see createSystemRoot())
}
private final CopyOnWriteMap<String, SQLBase> getBases() {
synchronized (this.getTreeMutex()) {
synchronized (this.baseMutex) {
if (this.bases == null) {
this.checkDropped();
this.bases = new CopyOnWriteMap<String, SQLBase>();
this.refresh(null, true, true);
}
return this.bases;
}
}
}
/**
* Signal that this server and its descendants will not be used anymore.
*/
public final void destroy() {
synchronized (this.getTreeMutex()) {
if (!this.isDropped())
this.dropped();
}
}
@Override
protected void onDrop() {
synchronized (this) {
if (this.ds != null)
try {
this.ds.close();
} catch (SQLException e) {
// tant pis
e.printStackTrace();
}
}
// allow SQLBase to be gc'd even if someone holds on to us
synchronized (this.baseMutex) {
this.bases = null;
}
super.onDrop();
}
private final Object getTreeMutex() {
final DBSystemRoot sysRoot = this.getDBSystemRoot();
final Object res = sysRoot == null ? this : sysRoot.getTreeMutex();
assert Thread.holdsLock(res) || !Thread.holdsLock(this);
return res;
}
Map<String, TablesMap> refresh(final Map<String, TablesMap> namesToRefresh, final boolean readCache) {
return this.refresh(namesToRefresh, readCache, false);
}
// return null if this cannot be refreshed (i.e. this is above DBSystemRoot)
private Map<String, TablesMap> refresh(final Map<String, TablesMap> tablesToRefresh, final boolean readCache, final boolean init) {
if (this.getDS() != null) {
if (Collections.emptyMap().equals(tablesToRefresh))
return tablesToRefresh;
final Set<String> namesToRefresh = tablesToRefresh == null ? null : tablesToRefresh.keySet();
// for mysql we must know our children, since they can reference each other and thus the
// graph needs them
final StructureLoadingEvent evt = new StructureLoadingEvent(this, namesToRefresh);
try {
this.getDBSystemRoot().fireLoading(evt);
synchronized (this.getTreeMutex()) {
final Set<String> childrenToRefresh = CollectionUtils.inter(namesToRefresh, this.getChildrenNames());
// don't save the result in files since getCatalogs() is at least as quick as
// executing a request to check if the cache is obsolete
final Set<String> allCats;
final Connection conn = this.getDS().getNewConnection();
try {
final List<String> allCatsList = ColumnListHandlerGeneric.create(String.class).handle(conn.getMetaData().getCatalogs());
allCats = new HashSet<String>(allCatsList);
} finally {
this.getDS().returnConnection(conn);
}
final Set<String> cats = CollectionUtils.inter(namesToRefresh, allCats);
this.getDBSystemRoot().filterNodes(this, cats);
SQLBase.mustContain(this, cats, childrenToRefresh, "bases");
for (final String base : CollectionUtils.substract(childrenToRefresh, cats)) {
final CollectionChangeEventCreator c = this.createChildrenCreator();
final SQLBase existingBase = this.getBases().remove(base);
this.fireChildrenChanged(c);
// null if it was never created
if (existingBase != null)
existingBase.dropped();
}
// delete the saved bases that we could have fetched, but haven't
// (bases that are not in scope are simply ignored, NOT deleted)
final DBFileCache cache = this.getFileCache();
if (cache != null) {
for (final DBItemFileCache savedBase : cache.getServerCache().getSavedDesc(SQLBase.class)) {
final String savedBaseName = savedBase.getName();
if (!cats.contains(savedBaseName) && (namesToRefresh == null || namesToRefresh.contains(savedBaseName)) && this.getDBSystemRoot().createNode(this, savedBaseName)) {
AccessController.doPrivileged(new PrivilegedAction<Object>() {
@Override
public Object run() {
savedBase.delete();
return null;
}
});
}
}
}
// fire once all the bases are loaded so that the graph is coherent
return this.getDBSystemRoot().getGraph().atomicRefresh(new Callable<Map<String, TablesMap>>() {
@Override
public Map<String, TablesMap> call() throws Exception {
final Map<String, TablesMap> res = new HashMap<String, TablesMap>();
// add or refresh
for (final String cat : cats) {
final TablesMap jdbcTables;
final SQLBase existing = getBase(cat);
if (existing != null) {
jdbcTables = existing.refresh(tablesToRefresh == null ? null : tablesToRefresh.get(cat), readCache);
} else {
// ignore tablesToRefresh if the base didn't exist (see
// SQLBase.assureAllTables())
// we already have the datasource, so login/pass aren't used
jdbcTables = createBase(cat, null, "", "", DSINIT_ERROR, readCache);
}
if (!jdbcTables.isEmpty())
res.put(cat, jdbcTables);
}
return res;
}
});
}
} catch (SQLException e) {
throw new IllegalStateException("could not get children names", e);
} finally {
this.getDBSystemRoot().fireLoading(evt.createFinishingEvent());
}
} else if (!init) {
throw new IllegalArgumentException("Cannot create bases since this server cannot have a connection");
}
return null;
}
/**
* Copy constructor. The new instance is in the same state <code>s</code> was, when it was
* created (no SQLBase, no default base).
*
* @param s the server to copy from.
*/
public SQLServer(SQLServer s) {
this(s.system, s.getName(), null, s.login, s.pass);
}
// tries to get a ds without any db
private synchronized final SQLDataSource getDS() {
if (!this.dsSet) {
this.checkDropped();
final DBSystemRoot sysRoot = this.getDBSystemRoot();
if (sysRoot == null) {
this.ds = null;
} else {
// should not succeed if pb otherwise with dsSet
// it will never be called again
this.ds = sysRoot.getDataSource();
}
this.dsSet = true;
}
return this.ds;
}
final String getURL(String base) {
return this.urlTransf.transformChecked(base);
}
/**
* Retourne la base par défaut.
*
* @return la base par défaut.
* @see #setDefaultBase(String)
*/
public SQLBase getBase() {
final String def;
synchronized (this) {
def = this.defaultBase;
}
if (def == null) {
throw new IllegalStateException("default base unset");
}
return this.getBase(def);
}
public SQLBase getBase(String baseName) {
return this.getBases().get(baseName);
}
/**
* Return the specified base using default login/pass.
*
* @param baseName the name of base to be returned.
* @return the SQLBase named <i>baseName</i>.
* @see #getBase(String, String, String, IClosure)
*/
public SQLBase getOrCreateBase(String baseName) {
return this.getBase(baseName, null, null);
}
public SQLBase getBase(String baseName, String login, String pass) {
return this.getBase(baseName, login, pass, null);
}
/**
* Return the specified base using provided login/pass. Does nothing if there's already a base
* with this name.
*
* @param baseName the name of the base.
* @param login the login, <code>null</code> means default.
* @param pass the password, <code>null</code> means default.
* @param dsInit to initialize the datasource before any request (eg setting jdbc properties),
* <code>null</code> meaning take the server one.
* @return the corresponding base.
*/
public SQLBase getBase(String baseName, String login, String pass, IClosure<? super SQLDataSource> dsInit) {
return this.getBase(baseName, null, login, pass, dsInit, true);
}
public SQLBase getBase(String baseName, final IClosure<? super DBSystemRoot> systemRootInit, String login, String pass, IClosure<? super SQLDataSource> dsInit, boolean readCache) {
if (this.getDBSystemRoot() != null)
throw new IllegalStateException("getBase(name, login, pass) should only be used for systems where SQLBase is DBSystemRoot");
synchronized (this.getTreeMutex()) {
SQLBase base = this.getBase(baseName);
if (base == null) {
this.createBase(baseName, systemRootInit, login, pass, dsInit, readCache);
base = this.getBase(baseName);
}
return base;
}
}
private final TablesMap createBase(String baseName, IClosure<? super DBSystemRoot> systemRootInit, String login, String pass, IClosure<? super SQLDataSource> dsInit, boolean readCache) {
final DBSystemRoot sysRoot = this.getDBSystemRoot();
if (sysRoot != null && !sysRoot.createNode(this, baseName))
throw new IllegalStateException(baseName + " is filtered, you must add it to rootsToMap");
final SQLBase base = new SQLBase(this, baseName, coalesce(systemRootInit, this.systemRootInit), login == null ? this.login : login, pass == null ? this.pass : pass,
coalesce(dsInit, this.dsInit));
return this.putBase(baseName, base, readCache);
}
public final DBSystemRoot getSystemRoot(String name) {
return this.getSystemRoot(name, null, null, null, null);
}
/**
* Return the specified systemRoot using provided login/pass. Does nothing if there's already a
* systemRoot with this name.
*
* @param name name of the system root, NOTE: for some systems the server is the systemRoot so
* <code>name</code> will be silently ignored.
* @param systemRootInit to initialize the {@link DBSystemRoot} before setting the data source.
* @param login the login, <code>null</code> means default.
* @param pass the password, <code>null</code> means default.
* @param dsInit to initialize the datasource before any request (eg setting jdbc properties),
* <code>null</code> meaning take the server one.
* @return the corresponding systemRoot.
* @see #isSystemRootCreated(String)
*/
public final DBSystemRoot getSystemRoot(String name, final IClosure<? super DBSystemRoot> systemRootInit, String login, String pass, IClosure<? super SQLDataSource> dsInit) {
synchronized (this.getTreeMutex()) {
if (!this.isSystemRootCreated(name)) {
return this.createSystemRoot(name, systemRootInit, login, pass, dsInit);
} else {
final DBSystemRoot res;
final DBSystemRoot sysRoot = this.getDBSystemRoot();
if (sysRoot != null)
res = sysRoot;
else {
res = this.getBase(name).getDBSystemRoot();
}
return res;
}
}
}
private final DBSystemRoot createSystemRoot(String name, IClosure<? super DBSystemRoot> systemRootInit, String login, String pass, IClosure<? super SQLDataSource> dsInit) {
final DBSystemRoot res;
synchronized (this.getTreeMutex()) {
final DBSystemRoot sysRoot = this.getDBSystemRoot();
if (sysRoot != null) {
res = sysRoot;
res.setDS(coalesce(systemRootInit, this.systemRootInit), login == null ? this.login : login, pass == null ? this.pass : pass, coalesce(dsInit, this.dsInit));
} else {
res = this.getBase(name, coalesce(systemRootInit, this.systemRootInit), login, pass, dsInit, true).getDBSystemRoot();
}
}
return res;
}
/**
* Whether the system root is created and has a datasource.
*
* @param name the system root name.
* @return <code>true</code> if the system root has a datasource.
*/
public final boolean isSystemRootCreated(String name) {
synchronized (this.getTreeMutex()) {
final DBSystemRoot sysRoot = this.getDBSystemRoot();
if (sysRoot != null)
return sysRoot.hasDataSource();
else
return this.isCreated(name) && this.getBase(name).getDBSystemRoot().hasDataSource();
}
}
private TablesMap putBase(String baseName, SQLBase base, boolean readCache) {
assert Thread.holdsLock(getTreeMutex());
final CollectionChangeEventCreator c = this.createChildrenCreator();
this.getBases().put(baseName, base);
final TablesMap res = base.init(readCache);
this.fireChildrenChanged(c);
// if base is null, no new tables (furthermore descendantsChanged() would create our
// children)
if (base != null)
if (this.getDBSystemRoot() != null)
this.getDBSystemRoot().descendantsChanged(this, Collections.singleton(baseName), readCache);
// defaultBase must be null, otherwise the user has already expressed his choice
synchronized (this) {
final boolean setDef = this.defaultBase == null && base != null;
if (setDef) {
this.setDefaultBase(baseName);
}
}
return res;
}
@Override
public Map<String, SQLBase> getChildrenMap() {
return this.getBases().getImmutable();
}
/**
* Has the passed base already been created. Useful as when this returns <code>true</code>,
* {@link #getBase(String, String, String, IClosure)} won't do anything but return the already
* created base, in particular the closure won't be used.
*
* @param baseName the name of the base.
* @return <code>true</code> if an instance of SQLBase already exists.
*/
public boolean isCreated(String baseName) {
return this.getBase(baseName) != null;
}
/**
* Met la base par défaut. Note: la première base ajoutée devient automatiquement la base par
* défaut.
*
* @param defaultBase le nom de la base par défaut, can be <code>null</code>.
* @see #getBase()
*/
public void setDefaultBase(String defaultBase) {
synchronized (this.getTreeMutex()) {
if (defaultBase != null && !this.contains(defaultBase))
throw new IllegalArgumentException(defaultBase + " unknown");
synchronized (this) {
this.defaultBase = defaultBase;
}
}
}
public String toString() {
return this.getName();
}
/**
* Return the name of the system of this server.
*
* @return the name of the system.
* @deprecated use {@link #getSQLSystem()}
*/
public final String getSystem() {
return this.getSQLSystem().getJDBCName();
}
public final SQLSystem getSQLSystem() {
return this.system;
}
public final synchronized void setID(final String id) {
this.id = id;
}
// needed since the host doesn't always identify one server (e.g. 127.0.0.1:1234 might be a
// tunnel to different servers)
public final synchronized String getID() {
return this.id;
}
public final DBFileCache getFileCache() {
return DBFileCache.create(this);
}
/**
* The host name of this server.
*
* @return the host name, <code>null</code> if not accessed through network.
*/
public final String getHostname() {
return this.getHostnameAndPath().get0();
}
public final List2<String> getHostnameAndPath() {
return this.getSQLSystem().getHostnameAndPath(this.getName());
}
public final boolean isPermanent() {
return this.getSQLSystem().isPermanent(this.getName());
}
/**
* Whether this server runs on the local machine.
*
* @return <code>true</code> if the JVM runs on the same machine than <code>this</code>.
*/
protected final boolean isLocalhost() {
// this method cannot be in SQLSyntax since it is used in the constructor of SQLDataSource
// (which is needed by SQLSyntax.create())
final String host = this.getHostname();
if (host == null)
return true;
final int colonIndex = host.indexOf(':');
final String hostWOPort = colonIndex < 0 ? host : host.substring(0, colonIndex);
return NetUtils.isSelfAddr(hostWOPort);
}
public final boolean isPersistent() {
return this.getSQLSystem() != SQLSystem.H2 || !this.getName().equals(SQLSystem.H2_IN_MEMORY);
}
}