OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

Rev 80 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | RSS feed

/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 * 
 * Copyright 2011 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.
 */
 
 /*
 * Field created on 4 mai 2004
 */
package org.openconcerto.sql.model;

import org.openconcerto.utils.CompareUtils;
import org.openconcerto.utils.StringUtils;
import org.openconcerto.xml.JDOMUtils;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.sql.Blob;
import java.sql.Clob;
import java.sql.Date;
import java.sql.Time;
import java.sql.Timestamp;
import java.sql.Types;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import net.jcip.annotations.GuardedBy;
import net.jcip.annotations.ThreadSafe;

import org.jdom2.Element;

/**
 * The type of a SQL field. Allow one to convert a Java object to its SQL serialization.
 * 
 * @see #toString(Object)
 * @see #check(Object)
 * @author Sylvain
 */
@ThreadSafe
public abstract class SQLType {

    private static Class<?> getClass(int type, final int size) {
        switch (type) {
        case Types.BIT:
            // As of MySQL 5.0.3, BIT is for storing bit-field values
            // As of Connector/J 3.1.9, transformedBitIsBoolean can be used
            // MAYBE remove Boolean after testing it works against 4.1 servers
            if (size == 1) {
                return Boolean.class;
            } else if (size <= Integer.SIZE) {
                return Integer.class;
            } else if (size <= Long.SIZE) {
                return Long.class;
            } else
                return BigInteger.class;
        case Types.BOOLEAN:
            return Boolean.class;
        case Types.DOUBLE:
            return Double.class;
        case Types.FLOAT:
        case Types.REAL:
            return Float.class;
        case Types.TIMESTAMP:
            return Timestamp.class;
        case Types.DATE:
            return java.util.Date.class;
        case Types.TIME:
            return java.sql.Time.class;
        case Types.INTEGER:
        case Types.SMALLINT:
        case Types.TINYINT:
            return Integer.class;
        case Types.BINARY:
        case Types.VARBINARY:
        case Types.LONGVARBINARY:
        case Types.BLOB:
            return Blob.class;
        case Types.CLOB:
            return Clob.class;
        case Types.BIGINT:
            // 8 bytes
            return Long.class;
        case Types.DECIMAL:
        case Types.NUMERIC:
            return BigDecimal.class;
        case Types.CHAR:
        case Types.VARCHAR:
        case Types.LONGVARCHAR:
            return String.class;
        default:
            // eg view columns are OTHER
            return Object.class;
        }
    }

    @GuardedBy("instances")
    static private final Map<List<String>, SQLType> instances = new HashMap<List<String>, SQLType>();

    // useful when no SQLBase is known
    public static SQLType getBoolean(final SQLSyntax s) {
        // TODO use get() once it accepts a SQLSyntax
        final SQLType res = new BooleanType(Types.BOOLEAN, 1, null, Boolean.class);
        res.setSyntax(s);
        return res;
    }

    public static SQLType get(final int type, final int size) {
        return get(null, type, size, null, null);
    }

    /**
     * Get the corresponding type.
     * 
     * @param base the base of this type, can be <code>null</code> but {@link #toString(Object)}
     *        will have to use standard SQL which might not be valid for all bases (eg escapes).
     * @param type a value from java.sql.Types.
     * @param size the size as COLUMN_SIZE is defined in
     *        {@link java.sql.DatabaseMetaData#getColumns(java.lang.String, java.lang.String, java.lang.String, java.lang.String)}
     * @param decDigits the number of fractional digits, can be <code>null</code>.
     * @param typeName data source dependent type name.
     * @return the corresponding instance.
     * @throws IllegalStateException if type is unknown.
     */
    public static SQLType get(final SQLBase base, final int type, final int size, Integer decDigits, final String typeName) {
        final List<String> typeID = Arrays.asList(base == null ? null : base.getURL(), type + "", size + "", String.valueOf(decDigits), typeName);
        synchronized (instances) {
            SQLType res = instances.get(typeID);
            if (res == null) {
                final Class<?> clazz = getClass(type, size);
                if (Boolean.class.isAssignableFrom(clazz))
                    res = new BooleanType(type, size, typeName, clazz);
                else if (Number.class.isAssignableFrom(clazz))
                    res = new NumberType(type, size, decDigits, typeName, clazz);
                else if (Time.class.isAssignableFrom(clazz))
                    res = new TimeType(type, size, decDigits, typeName, clazz);
                else if (Timestamp.class.isAssignableFrom(clazz))
                    res = new TimestampType(type, size, decDigits, typeName, clazz);
                // Date en dernier surclasse des autres
                else if (java.util.Date.class.isAssignableFrom(clazz))
                    res = new DateType(type, size, decDigits, typeName, clazz);
                else if (String.class.isAssignableFrom(clazz))
                    res = new StringType(type, size, typeName, clazz);
                else
                    // BLOB & CLOB and the rest
                    res = new UnknownType(type, size, typeName, clazz);
                res.setBase(base);
                instances.put(typeID, res);
            }
            return res;
        }
    }

    public static SQLType get(final SQLBase base, Element typeElement) {
        int type = Integer.valueOf(typeElement.getAttributeValue("type")).intValue();
        int size = Integer.valueOf(typeElement.getAttributeValue("size")).intValue();
        String typeName = typeElement.getAttributeValue("typeName");
        final String decDigitsS = typeElement.getAttributeValue("decimalDigits");
        final Integer decDigits = decDigitsS == null ? null : Integer.valueOf(decDigitsS);
        return get(base, type, size, decDigits, typeName);
    }

    static void remove(final SQLBase base) {
        synchronized (instances) {
            final Iterator<Entry<List<String>, SQLType>> iter = instances.entrySet().iterator();
            while (iter.hasNext()) {
                final Entry<List<String>, SQLType> e = iter.next();
                if (e.getValue().getBase() == base)
                    iter.remove();
            }
        }
    }

    // *** instance

    // a value from java.sql.Types
    private final int type;
    // COLUMN_SIZE
    private final int size;
    // DECIMAL_DIGITS
    private final Integer decimalDigits;
    // TYPE_NAME
    private final String typeName;
    // the class this type accepts
    private final Class<?> javaType;

    @GuardedBy("this")
    private SQLBase base;
    @GuardedBy("this")
    private SQLSyntax syntax;

    @GuardedBy("this")
    private String xml;

    private SQLType(int type, int size, Integer decDigits, String typeName, Class<?> javaType) {
        this.type = type;
        this.size = size;
        this.decimalDigits = decDigits;
        this.typeName = typeName;
        this.javaType = javaType;
        this.xml = null;
    }

    /**
     * The SQL type.
     * 
     * @return a value from java.sql.Types.
     */
    public int getType() {
        return this.type;
    }

    private final boolean isNumeric() {
        return this.getType() == Types.DECIMAL || this.getType() == Types.NUMERIC;
    }

    public final int getSize() {
        return this.size;
    }

    public final Integer getDecimalDigits() {
        return this.decimalDigits;
    }

    public Class<?> getJavaType() {
        return this.javaType;
    }

    /**
     * Data source dependent type name.
     * 
     * @return the data source dependent type name.
     */
    public final String getTypeName() {
        return this.typeName;
    }

    // TODO remove once quoteString() is in SQLSyntax
    private synchronized final void setBase(SQLBase base) {
        // set only once
        assert this.base == null;
        if (base != null) {
            this.base = base;
            this.setSyntax(this.base.getServer().getSQLSystem().getSyntax());
        }
    }

    private synchronized final void setSyntax(SQLSyntax s) {
        // set only once
        assert this.syntax == null;
        if (s != null) {
            this.syntax = s;
        }
    }

    private synchronized final SQLBase getBase() {
        return this.base;
    }

    public synchronized final SQLSyntax getSyntax() {
        return this.syntax;
    }

    protected final String quoteString(String s) {
        return SQLBase.quoteString(this.getBase(), s);
    }

    @Override
    public boolean equals(Object obj) {
        return equals(obj, null);
    }

    public boolean equals(Object obj, SQLSystem otherSystem) {
        if (obj instanceof SQLType) {
            final SQLType o = (SQLType) obj;
            final boolean javaTypeOK = this.getJavaType().equals(o.getJavaType());
            if (!javaTypeOK) {
                return false;
            } else if (this.getJavaType() == Boolean.class) {
                // can take many forms (e.g. BIT(1) or BOOLEAN) but type and size are meaningless
                return true;
            }

            // for all intents and purposes NUMERIC == DECIMAL
            final boolean typeOK = this.isNumeric() ? o.isNumeric() : this.getType() == o.getType();
            if (!typeOK)
                return false;

            final boolean sizeOK;
            // date has no precision so size is meaningless
            // Floating-Point Types have no precision (apart from single or double precision, but
            // this is handled by typeOK)
            if (this.getType() == Types.DATE || this.getJavaType() == Float.class || this.getJavaType() == Double.class) {
                sizeOK = true;
            } else {
                if (otherSystem == null && o.getSyntax() != null)
                    otherSystem = o.getSyntax().getSystem();
                final SQLSystem thisSystem = this.getSyntax() == null ? null : this.getSyntax().getSystem();
                final boolean isTime = this.getType() == Types.TIME || this.getType() == Types.TIMESTAMP;
                final boolean decDigitsOK;
                // only TIME and NUMERIC use DECIMAL_DIGITS, others like integer use only size
                if (!this.isNumeric() && !isTime) {
                    decDigitsOK = true;
                } else if (this.isNumeric() ||
                // isTime() : if we don't know the system, play it safe and compare
                        thisSystem == null || otherSystem == null || thisSystem.isFractionalSecondsSupported() && otherSystem.isFractionalSecondsSupported()) {
                    decDigitsOK = CompareUtils.equals(this.getDecimalDigits(), o.getDecimalDigits());
                } else {
                    decDigitsOK = true;
                }
                // not all systems return the same size for TIME but only DECIMAL DIGITS matters
                sizeOK = decDigitsOK && (isTime || this.getSize() == o.getSize());
            }
            return sizeOK;
        } else {
            return super.equals(obj);
        }
    }

    @Override
    public int hashCode() {
        return this.getType() + this.getSize() + this.getJavaType().hashCode();
    }

    @Override
    public final String toString() {
        return "SQLType #" + this.getType() + "(" + this.getSize() + "," + this.getDecimalDigits() + "): " + this.getJavaType();
    }

    public synchronized final String toXML() {
        // this class is immutable and its instances shared so cache its XML
        if (this.xml == null) {
            final StringBuilder sb = new StringBuilder(128);
            sb.append("<type type=\"");
            sb.append(this.type);
            sb.append("\" size=\"");
            sb.append(this.size);
            if (this.decimalDigits != null) {
                sb.append("\" decimalDigits=\"");
                sb.append(this.decimalDigits);
            }
            sb.append("\" typeName=\"");
            sb.append(JDOMUtils.OUTPUTTER.escapeAttributeEntities(this.typeName));
            sb.append("\"/>");
            this.xml = sb.toString();
        }
        return this.xml;
    }

    /**
     * Serialize an object to its SQL string.
     * 
     * @param o an instance of getJavaType(), e.g. "it's".
     * @return the SQL representation, e.g. "'it''s'".
     * @throws IllegalArgumentException if o is not valid.
     * @see #isValid(Object)
     */
    public final String toString(Object o) {
        this.check(o);
        if (o == null)
            return "NULL";
        else
            return this.toStringRaw(o);
    }

    /**
     * Serialize an object to its CSV string. <code>null</code> is \N, other values are always
     * quoted.
     * 
     * @param o an instance of getJavaType(), e.g. "it's" or 12.
     * @return the CSV representation, e.g. "it's" or "12".
     * @throws IllegalArgumentException if o is not valid.
     * @see #isValid(Object)
     */
    public final String toCSV(Object o) {
        this.check(o);
        if (o == null)
            return "\\N";
        else
            return StringUtils.doubleQuote(this.toCSVRaw(o));
    }

    // fromString(String s) is too complicated, e.g. see SQLBase#unquoteStringStd()

    /**
     * Check if o is valid, do nothing if it is else throw an exception.
     * 
     * @param o the object to check.
     * @throws IllegalArgumentException if o is not valid.
     */
    public final void check(Object o) {
        if (!isValid(o)) {
            final String className = o == null ? "" : "(" + o.getClass() + ")";
            throw new IllegalArgumentException(o + className + " is not valid for " + this);
        }
    }

    /**
     * Test whether o is valid for this type. Ie does o is an instance of getJavaType().
     * 
     * @param o the object to test.
     * @return <code>true</code> if o can be passed to {@link #toString(Object)}.
     * @see #check(Object)
     */
    public boolean isValid(Object o) {
        return o == null || this.getJavaType().isInstance(o);
    }

    abstract protected String toStringRaw(Object o);

    abstract protected String toCSVRaw(Object o);

    // ** static subclasses

    private static final class UnknownType extends SQLType {
        public UnknownType(int type, int size, String typeName, Class<?> clazz) {
            super(type, size, null, typeName, clazz);
        }

        @Override
        protected String toStringRaw(Object o) {
            throw new IllegalStateException("not implemented");
        }

        @Override
        protected String toCSVRaw(Object o) {
            throw new IllegalStateException("not implemented");
        }
    }

    private abstract static class ToStringType extends SQLType {
        public ToStringType(int type, int size, String typeName, Class<?> clazz) {
            this(type, size, null, typeName, clazz);
        }

        public ToStringType(int type, int size, Integer decDigits, String typeName, Class<?> clazz) {
            super(type, size, decDigits, typeName, clazz);
        }

        @Override
        protected String toStringRaw(Object o) {
            return o.toString();
        }

        @Override
        protected String toCSVRaw(Object o) {
            return toStringRaw(o);
        }
    }

    private static class BooleanType extends ToStringType {
        public BooleanType(int type, int size, String typeName, Class<?> clazz) {
            super(type, size, typeName, clazz);
        }

        @Override
        protected String toStringRaw(Object o) {
            if (this.getSyntax().getSystem() == SQLSystem.MSSQL) {
                // 'true'
                return this.quoteString(o.toString());
            } else
                return super.toStringRaw(o);
        }

        @Override
        protected String toCSVRaw(Object o) {
            // see SQLSyntaxPG.selectAll
            return Boolean.TRUE.equals(o) ? "1" : "0";
        }
    }

    private static class NumberType extends ToStringType {
        public NumberType(int type, int size, Integer decDigits, String typeName, Class<?> clazz) {
            super(type, size, decDigits, typeName, clazz);
        }

        @Override
        public boolean isValid(Object o) {
            return super.isValid(o) || o instanceof Number;
        }
    }

    private static abstract class DateOrTimeType extends SQLType {
        public DateOrTimeType(int type, int size, Integer decDigits, String typeName, Class<?> clazz) {
            super(type, size, decDigits, typeName, clazz);
        }

        @Override
        public final boolean isValid(Object o) {
            return super.isValid(o) || o instanceof java.util.Date || o instanceof java.util.Calendar || o instanceof Number;
        }

        @Override
        public final String toStringRaw(Object o) {
            // time has no special characters to escape
            return "'" + toCSVRaw(o) + "'";
        }

        static protected long getTime(Object o) {
            if (o instanceof java.util.Date)
                return ((java.util.Date) o).getTime();
            else if (o instanceof java.util.Calendar)
                return ((java.util.Calendar) o).getTimeInMillis();
            else
                return ((Number) o).longValue();
        }
    }

    private static class DateType extends DateOrTimeType {
        public DateType(int type, int size, Integer decDigits, String typeName, Class<?> clazz) {
            super(type, size, decDigits, typeName, clazz);
        }

        @Override
        protected String toCSVRaw(Object o) {
            final Date ts;
            if (o instanceof Date)
                ts = (Date) o;
            else
                ts = new Date(getTime(o));
            return ts.toString();
        }
    }

    private static class TimestampType extends DateOrTimeType {
        public TimestampType(int type, int size, Integer decDigits, String typeName, Class<?> clazz) {
            super(type, size, decDigits, typeName, clazz);
        }

        @Override
        protected String toCSVRaw(Object o) {
            final Timestamp ts;
            if (o instanceof Timestamp)
                ts = (Timestamp) o;
            else
                ts = new Timestamp(getTime(o));
            return ts.toString();
        }
    }

    private static class TimeType extends DateOrTimeType {
        public TimeType(int type, int size, Integer decDigits, String typeName, Class<?> clazz) {
            super(type, size, decDigits, typeName, clazz);
        }

        @Override
        protected String toCSVRaw(Object o) {
            final Time ts;
            if (o instanceof Time)
                ts = (Time) o;
            else
                ts = new Time(getTime(o));
            return ts.toString();
        }
    }

    private static class StringType extends SQLType {
        public StringType(int type, int size, String typeName, Class<?> clazz) {
            super(type, size, null, typeName, clazz);
        }

        @Override
        protected String toStringRaw(Object o) {
            return this.quoteString((String) o);
        }

        @Override
        protected String toCSVRaw(Object o) {
            return (String) o;
        }
    }

}