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.
 */
 
 package org.openconcerto.sql.model;

import org.openconcerto.sql.utils.SQL_URL;
import org.openconcerto.utils.EnumOrderedSet;
import org.openconcerto.utils.Tuple2;
import org.openconcerto.utils.Tuple2.List2;
import org.openconcerto.utils.cc.ITransformer;

import java.sql.Statement;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import org.h2.engine.ConnectionInfo;
import org.h2.engine.Constants;
import org.h2.util.StringUtils;

/**
 * A RDBMS like PostgreSQL or MySQL.
 * 
 * @author Sylvain
 */
public enum SQLSystem {

    /**
     * The PostgreSQL database. The required version is 8.2 since "drop schema if exists" and
     * "insert returning" are needed.
     * 
     * @see <a href="http://www.postgresql.org/">PostgreSQL site</a>
     */
    POSTGRESQL("PostgreSQL") {
        @Override
        void removeRootsToIgnore(Set<String> s) {
            super.removeRootsToIgnore(s);
            final Iterator<String> iter = s.iterator();
            while (iter.hasNext()) {
                final String r = iter.next();
                if (r.startsWith("pg_"))
                    iter.remove();
            }
        }

        @Override
        public boolean isClearingPathSupported() {
            return true;
        }

        @Override
        public boolean autoCreatesFKIndex() {
            return false;
        }

        @Override
        public boolean isIndexFilterConditionSupported() {
            return true;
        }

        @Override
        public boolean isSequencesSupported() {
            return true;
        }
    },

    /**
     * The MySQL database. Necessary server configuration :
     * <dl>
     * <dt>sql_mode = 'ANSI'</dt>
     * <dd>to allow standard SQL (syntax like " and || ; real as float)</dd>
     * <dt>lower_case_table_names = 0</dt>
     * <dd>to allow tables with mixed case</dd>
     * </dl>
     * 
     * @see <a href="http://www.mysql.com/">MySQL site</a>
     */
    MYSQL("MySQL") {
        @Override
        EnumSet<HierarchyLevel> createLevels() {
            // mysql has no schema
            return EnumSet.complementOf(EnumSet.of(HierarchyLevel.SQLSCHEMA));
        }

        @Override
        void removeRootsToIgnore(Set<String> s) {
            super.removeRootsToIgnore(s);
            s.remove("mysql");
            s.remove("performance_schema");
            // before 5.5.8
            s.remove("PERFORMANCE_SCHEMA");
        }

        @Override
        public boolean isInterBaseSupported() {
            // since jdbc://127.0.0.1/Ideation_2007 can reach jdbc://127.0.0.1/Gestion
            return true;
        }

        @Override
        public boolean isDBPathEmpty() {
            // since ds is now on SystemRoot ie jdbc://127.0.0.1/
            return true;
        }

        @Override
        public boolean isFractionalSecondsSupported() {
            // see http://forge.mysql.com/worklog/task.php?id=946
            return false;
        }

        @Override
        public boolean isTablesCommentSupported() {
            // comments are supported in MySQL but JDBC doesn't return them
            // (for now it uses "show tables" although they are in information_schema."TABLES")
            return false;
        }
    },

    /**
     * The H2 database.
     * 
     * @see <a href="http://www.h2database.com/">H2 site</a>
     */
    H2("H2") {

        private static final String TCP_PREFIX = "tcp://";
        private static final String ARBITRARY_BASE_NAME = "foo";

        ITransformer<String, String> getURLTransf(final SQLServer s) {
            if (s.getSQLSystem() != this)
                throw new IllegalArgumentException(s + " is not " + this);

            return new ITransformer<String, String>() {
                @Override
                public String transformChecked(String base) {
                    // by default h2 convert database name to upper case (we used to work around it
                    // with SQLSystem.getMDName() but in r2251 an equalsIgnoreCase() was replaced by
                    // equals()) see http://code.google.com/p/h2database/issues/detail?id=204
                    return s.getName() + base + ";DATABASE_TO_UPPER=false";
                }
            };
        }

        @Override
        public boolean isClearingPathSupported() {
            // TODO see if SCHEMA_SEARCH_PATH can be passed an empty list
            // (in addition to merge with SCHEMA)
            return false;
        }

        @Override
        public boolean isMultipleResultSetsSupported() {
            // https://groups.google.com/d/msg/h2-database/Is91FqarxDw/5x-xW3_IPwUJ
            return false;
        }

        @Override
        public boolean isSequencesSupported() {
            return true;
        }

        @Override
        public String getServerName(final String host) {
            return TCP_PREFIX + host + "/";
        }

        @Override
        public List2<String> getHostnameAndPath(final String server) {
            // append base name to server name to get a valid value
            final ConnectionInfo info = new ConnectionInfo(server + ARBITRARY_BASE_NAME);
            final String name = info.getName();
            final String hostName;
            final int pathIndex;
            if (info.isRemote()) {
                // tcp:// or ssl://server/path
                assert name.startsWith("//");
                final int slashIndex = name.indexOf('/', 2);
                hostName = name.substring(2, slashIndex);
                pathIndex = slashIndex + 1;
            } else {
                // mem: or file:/data/sample or ~/test
                hostName = null;
                pathIndex = 0;
            }

            return new List2<>(hostName, name.substring(pathIndex, name.length() - ARBITRARY_BASE_NAME.length()));
        }

        @Override
        public boolean isPermanent(final String server) {
            return !server.startsWith(H2_IN_MEMORY);
        }

        @Override
        public Map<String, String> getConnectionInfo(final String url) {
            final Tuple2<String, Map<String, String>> settings = readSettingsFromURL(url);
            final Map<String, String> res = new HashMap<String, String>();
            // TODO other settings are ignored
            res.put("root", settings.get1().get(StringUtils.toUpperEnglish("SCHEMA")));
            res.put("table", settings.get1().get(StringUtils.toUpperEnglish("TABLE")));
            res.put("login", settings.get1().get(StringUtils.toUpperEnglish("USER")));
            res.put("pass", settings.get1().get(StringUtils.toUpperEnglish("PASSWORD")));

            // remove mem:, tcp:, etc
            final String name = settings.get0();
            final int prefix = name.indexOf(':');
            final int lastSlash = name.lastIndexOf('/');
            final String sysRoot = lastSlash < 0 ? name.substring(prefix + 1) : name.substring(lastSlash + 1);
            res.put("systemRoot", sysRoot);
            res.put("name", name.substring(0, name.length() - sysRoot.length()));

            return res;
        }

        // pasted from org.h2.engine.ConnectionInfo
        private Tuple2<String, Map<String, String>> readSettingsFromURL(final String origURL) throws IllegalArgumentException {
            String url = origURL;
            final Map<String, String> prop = new HashMap<String, String>();
            final int idx = url.indexOf(';');
            if (idx >= 0) {
                String settings = url.substring(idx + 1);
                url = url.substring(0, idx);
                String[] list = StringUtils.arraySplit(settings, ';', false);
                for (String setting : list) {
                    int equal = setting.indexOf('=');
                    if (equal < 0) {
                        throw new IllegalArgumentException("format error, missing =" + url);
                    }
                    String value = setting.substring(equal + 1);
                    String key = setting.substring(0, equal);
                    key = StringUtils.toUpperEnglish(key);

                    final String old = prop.get(key);
                    if (old != null && !old.equals(value)) {
                        throw new IllegalArgumentException("DUPLICATE_PROPERTY " + key + " in " + url);
                    }
                    prop.put(key, value);
                }
            }
            return Tuple2.create(url.substring(Constants.START_URL.length()), prop);
        }

    },
    MSSQL("Microsoft SQL Server") {
        @Override
        public String getJDBCName() {
            return "sqlserver";
        }

        @Override
        ITransformer<String, String> getURLTransf(final SQLServer s) {
            return new ITransformer<String, String>() {
                @Override
                public String transformChecked(String base) {
                    return "//" + s.getName() + ";databaseName=" + base;
                }
            };
        }

        @Override
        void removeRootsToIgnore(Set<String> s) {
            super.removeRootsToIgnore(s);
            final Iterator<String> iter = s.iterator();
            while (iter.hasNext()) {
                final String r = iter.next();
                if (r.startsWith("db_") || r.equals("sys"))
                    iter.remove();
            }
        }

        @Override
        public boolean autoCreatesFKIndex() {
            return false;
        }

        @Override
        public boolean isIndexFilterConditionSupported() {
            return true;
        }

        @Override
        public boolean isTablesCommentSupported() {
            // comments are not directly supported in MS, see sp_addextendedproperty
            // 'MS_Description' :
            // http://stackoverflow.com/questions/378700/is-it-possible-to-add-a-description-comment-to-a-table-in-microsoft-sql-2000
            return false;
        }
    },
    DERBY("Apache Derby");

    public static SQLSystem get(String name) {
        final String normalized = name.toUpperCase();
        try {
            return SQLSystem.valueOf(normalized);
        } catch (IllegalArgumentException e) {
            // synonyms
            if (normalized.equals("PSQL"))
                return POSTGRESQL;
            else
                throw e;
        }
    }

    public static final String H2_IN_MEMORY = "mem:";

    private final String label;
    private final EnumOrderedSet<HierarchyLevel> levels;

    private SQLSystem(final String label) {
        this.label = label;
        this.levels = new EnumOrderedSet<HierarchyLevel>(this.createLevels());
    }

    /**
     * The string to use in jdbc urls.
     * 
     * @return the jdbc string for this.
     */
    public String getJDBCName() {
        return this.name().toLowerCase();
    }

    public final String getLabel() {
        return this.label;
    }

    EnumSet<HierarchyLevel> createLevels() {
        return EnumSet.allOf(HierarchyLevel.class);
    }

    public final EnumOrderedSet<HierarchyLevel> getLevels() {
        return this.levels;
    }

    /**
     * The number of levels between the parameters.
     * 
     * @param clazz1 the start structure item class, e.g. {@link SQLTable}.
     * @param clazz2 the destination structure item class, e.g. {@link SQLSchema} or {@link DBRoot}.
     * @return the distance between parameters, e.g. -1.
     */
    public final int getHops(Class<? extends DBStructureItem<?>> clazz1, Class<? extends DBStructureItem<?>> clazz2) {
        final EnumOrderedSet<HierarchyLevel> levels;
        if (DBStructureItemDB.class.isAssignableFrom(clazz1) || DBStructureItemDB.class.isAssignableFrom(clazz2))
            levels = this.getLevels();
        else
            levels = HierarchyLevel.getAll();
        return levels.getHops(this.getLevel(clazz1), this.getLevel(clazz2));
    }

    /**
     * The level of the root for this system, ie the level above {@link HierarchyLevel#SQLTABLE}.
     * 
     * @return level of the root.
     */
    public final HierarchyLevel getDBRootLevel() {
        return this.getLevels().getPrevious(HierarchyLevel.SQLTABLE);
    }

    public final HierarchyLevel getDBLevel(Class<? extends DBStructureItemDB> clazz) {
        if (clazz.equals(DBRoot.class))
            return this.getDBRootLevel();
        else if (clazz.equals(DBSystemRoot.class))
            return this.getLevels().getPrevious(this.getDBRootLevel());
        else
            throw new IllegalArgumentException(clazz + " should be either DBRoot or DBSystemRoot");
    }

    public final HierarchyLevel getLevel(Class<? extends DBStructureItem<?>> clazz) {
        if (DBStructureItemDB.class.isAssignableFrom(clazz))
            return this.getDBLevel(clazz.asSubclass(DBStructureItemDB.class));
        else
            return HierarchyLevel.get(clazz.asSubclass(DBStructureItemJDBC.class));
    }

    /**
     * Remove from <code>s</code> the database private roots, eg "information_schema".
     * 
     * @param s a set of roots names, that will be modified.
     */
    void removeRootsToIgnore(Set<String> s) {
        s.remove("information_schema");
        s.remove("INFORMATION_SCHEMA");
    }

    // result must be thread-safe
    ITransformer<String, String> getURLTransf(final SQLServer s) {
        if (s.getSQLSystem() != this)
            throw new IllegalArgumentException(s + " is not " + this);

        return new ITransformer<String, String>() {
            @Override
            public String transformChecked(String base) {
                return "//" + s.getName() + "/" + base;
            }
        };
    }

    /**
     * Return the server name for the passed host.
     * 
     * @param host an ip address or dns name, eg "foo".
     * @return the name of the server, eg "tcp://foo/".
     */
    public String getServerName(String host) {
        return host;
    }

    /**
     * The host name for the passed server.
     * 
     * @param server the name of an {@link SQLServer}, e.g. tcp://127.0.0.1/dir/.
     * @return its host and its path, both can be <code>null</code> (but not at the same time), e.g.
     *         [127.0.0.1, dir].
     */
    public List2<String> getHostnameAndPath(String server) {
        Objects.requireNonNull(server, "Null server");
        return new List2<>(server, null);
    }

    /**
     * Whether the passed server runs inside the VM.
     * 
     * @param server a server, e.g. {@link SQLServer#getName()}.
     * @return <code>true</code> if <code>server</code> runs inside the VM.
     */
    public final boolean isEmbedded(final String server) {
        return getHostnameAndPath(server).get0() == null;
    }

    /**
     * Whether the passed server survives when all connections to it are closed.
     * 
     * @param server a server, e.g. {@link SQLServer#getName()}.
     * @return <code>true</code> if <code>server</code> survives when all connections to it are
     *         closed.
     */
    public boolean isPermanent(String server) {
        return true;
    }

    /**
     * Parse <code>url</code> to find info needed by {@link SQL_URL}.
     * 
     * @param url a jdbc url, eg
     *        "jdbc:h2:mem:Controle;USER=maillard;PASSWORD=pass;SCHEMA=Ideation_2007;TABLE=TENSION".
     * @return a map containing login, pass, server name, and systemRoot, root, table.
     */
    public Map<String, String> getConnectionInfo(final String url) {
        throw new UnsupportedOperationException();
    }

    public final boolean isNoDefaultSchemaSupported() {
        return this.isClearingPathSupported() || this.isDBPathEmpty();
    }

    /**
     * Whether this can clear the path of an existing connection.
     * 
     * @return <code>true</code> if this can.
     */
    public boolean isClearingPathSupported() {
        return false;
    }

    /**
     * Whether a connection has an empty path by default, eg MySQL when connecting to 127.0.0.1.
     * 
     * @return <code>true</code> if it does.
     */
    public boolean isDBPathEmpty() {
        return false;
    }

    /**
     * Whether a table in one base can reference a table in another one.
     * 
     * @return <code>true</code> if eg base1.schema1.RENDEZVOUS can point to base2.schema1.CLIENT.
     */
    public boolean isInterBaseSupported() {
        return false;
    }

    /**
     * Whether this system automatically creates an index for each foreign key constraint.
     * 
     * @return <code>true</code> for this implementation.
     */
    public boolean autoCreatesFKIndex() {
        return true;
    }

    public boolean isIndexFilterConditionSupported() {
        return false;
    }

    public boolean isFractionalSecondsSupported() {
        return true;
    }

    public boolean isTablesCommentSupported() {
        return true;
    }

    /**
     * Whether more than one result set can be retrieved.
     * 
     * @return <code>true</code> if {@link Statement#getMoreResults()} is functional.
     */
    public boolean isMultipleResultSetsSupported() {
        return true;
    }

    public boolean isSequencesSupported() {
        return false;
    }

    public String getMDName(String name) {
        return name;
    }

    /**
     * The default syntax for this system. NOTE : when needing a syntax for a system currently
     * accessible, {@link DBSystemRoot#getSyntax()} should be used so that server options can be
     * read. Otherwise constructors of {@link SQLSyntax} subclasses should be used to specify
     * options.
     * 
     * @return the syntax for this system, or <code>null</code> if none exists.
     */
    public final SQLSyntax getSyntax() {
        try {
            return SQLSyntax.get(this);
        } catch (IllegalArgumentException e) {
            return null;
        }
    }
}