OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

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);
    }
}