OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

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

import org.openconcerto.sql.Log;
import org.openconcerto.sql.model.graph.DatabaseGraph;
import org.openconcerto.sql.model.graph.Link;
import org.openconcerto.sql.model.graph.Link.Direction;
import org.openconcerto.sql.model.graph.Step;
import org.openconcerto.utils.NumberUtils;
import org.openconcerto.utils.Value;
import org.openconcerto.utils.cc.HashingStrategy;
import org.openconcerto.utils.convertor.StringClobConvertor;

import java.math.BigDecimal;
import java.sql.Clob;
import java.text.DateFormat;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;

/**
 * A class that represent a row of a table. The row might not acutally exists in the database, and
 * it might not define all the fields.
 * 
 * <table border="1">
 * <caption>Primary Key</caption> <thead>
 * <tr>
 * <th><code>ID</code> value</th>
 * <th>{@link #hasID()}</th>
 * <th>{@link #getIDNumber()}</th>
 * <th>{@link #isUndefined()}</th>
 * </tr>
 * </thead> <tbody>
 * <tr>
 * <th>∅</th>
 * <td><code>false</code></td>
 * <td><code>null</code></td>
 * <td><code>false</code></td>
 * </tr>
 * <tr>
 * <th><code>null</code></th>
 * <td><code>false</code> :<br/>
 * no row in the DB can have a <code>null</code> primary key</td>
 * <td><code>null</code></td>
 * <td><code>false</code><br/>
 * (even if getUndefinedIDNumber() is <code>null</code>, see method documentation)</td>
 * </tr>
 * <tr>
 * <th><code>instanceof Number</code></th>
 * <td><code>true</code></td>
 * <td><code>Number</code></td>
 * <td>if equals <code>getUndefinedID()</code></td>
 * </tr>
 * <tr>
 * <th><code>else</code></th>
 * <td><code>ClassCastException</code></td>
 * <td><code>ClassCastException</code></td>
 * <td><code>ClassCastException</code></td>
 * </tr>
 * </tbody>
 * </table>
 * <br/>
 * <table border="1">
 * <caption>Foreign Keys</caption> <thead>
 * <tr>
 * <th><code>ID</code> value</th>
 * <th>{@link #getForeignIDNumber(String)}</th>
 * <th>{@link #isForeignEmpty(String)}</th>
 * </tr>
 * </thead> <tbody>
 * <tr>
 * <th>∅</th>
 * <td><code>Exception</code></td>
 * <td><code>Exception</code></td>
 * </tr>
 * <tr>
 * <th><code>null</code></th>
 * <td><code>null</code></td>
 * <td>if equals <code>getUndefinedID()</code></td>
 * </tr>
 * <tr>
 * <th><code>instanceof Number</code></th>
 * <td><code>Number</code></td>
 * <td>if equals <code>getUndefinedID()</code></td>
 * </tr>
 * <tr>
 * <tr>
 * <th><code>instanceof SQLRowValues</code></th>
 * <td><code>getIDNumber()</code></td>
 * <td><code>isUndefined()</code></td>
 * </tr>
 * <th><code>else</code></th>
 * <td><code>ClassCastException</code></td>
 * <td><code>ClassCastException</code></td>
 * </tr>
 * </tbody>
 * </table>
 * 
 * @author Sylvain CUAZ
 */
public abstract class SQLRowAccessor implements SQLData {

    @Deprecated
    static public final String ACCESS_DB_IF_NEEDED_PROP = "SQLRowAccessor.accessDBIfNeeded";
    static private final boolean ACCESS_DB_IF_NEEDED = Boolean.parseBoolean(System.getProperty(ACCESS_DB_IF_NEEDED_PROP, "false"));

    public static boolean getAccessDBIfNeeded() {
        return ACCESS_DB_IF_NEEDED;
    }

    static private final HashingStrategy<SQLRowAccessor> ROW_STRATEGY = new HashingStrategy<SQLRowAccessor>() {
        @Override
        public int computeHashCode(SQLRowAccessor object) {
            return object.hashCodeAsRow();
        }

        @Override
        public boolean equals(SQLRowAccessor object1, SQLRowAccessor object2) {
            return object1.equalsAsRow(object2);
        }
    };

    /**
     * A strategy to compare instances as {@link SQLRow}, i.e. only {@link #getTable() table} and
     * {@link #getID() id}.
     * 
     * @return a strategy.
     * @see #equalsAsRow(SQLRowAccessor)
     */
    public static final HashingStrategy<SQLRowAccessor> getRowStrategy() {
        return ROW_STRATEGY;
    }

    static public final Set<Number> getIDs(final Collection<? extends SQLRowAccessor> rows) {
        return getIDs(rows, new HashSet<Number>());
    }

    static public final <C extends Collection<? super Number>> C getIDs(final Collection<? extends SQLRowAccessor> rows, final C res) {
        for (final SQLRowAccessor r : rows)
            res.add(r.getIDNumber());
        return res;
    }

    private final SQLTable table;

    protected SQLRowAccessor(SQLTable table) {
        super();
        if (table == null)
            throw new NullPointerException("null SQLTable");
        this.table = table;
    }

    public final SQLTable getTable() {
        return this.table;
    }

    /**
     * Whether this row has a Number for the primary key.
     * 
     * @return <code>true</code> if the value of the primary key is specified and is a non
     *         <code>null</code> number, <code>false</code> if the value isn't specified or if it's
     *         <code>null</code>.
     * @throws ClassCastException if value is not <code>null</code> and not a {@link Number}.
     */
    public final boolean hasID() throws ClassCastException {
        return this.getIDNumber() != null;
    }

    /**
     * Returns the ID of the represented row.
     * 
     * @return the ID, or {@link SQLRow#NONEXISTANT_ID} if this row is not linked to the DB.
     */
    public abstract int getID();

    public abstract Number getIDNumber();

    /**
     * Whether this row is the undefined row. Return <code>false</code> if both the
     * {@link #getIDNumber() ID} and {@link SQLTable#getUndefinedIDNumber()} are <code>null</code>
     * since no row can have <code>null</code> primary key in the database. IOW when
     * {@link SQLTable#getUndefinedIDNumber()} is <code>null</code> the empty
     * <strong>foreign</strong> keys are <code>null</code>.
     * 
     * @return <code>true</code> if the ID is specified, not <code>null</code> and is equal to the
     *         {@link SQLTable#getUndefinedIDNumber() undefined} ID.
     */
    public final boolean isUndefined() {
        final Number id = this.getIDNumber();
        return id != null && id.intValue() == this.getTable().getUndefinedID();
    }

    /**
     * Est ce que cette ligne est archivée.
     * 
     * @return <code>true</code> si la ligne était archivée lors de son instanciation.
     */
    public final boolean isArchived() {
        return this.isArchived(true);
    }

    protected final boolean isArchived(final boolean allowDBAccess) {
        // si il n'y a pas de champs archive, elle n'est pas archivée
        final SQLField archiveField = this.getTable().getArchiveField();
        if (archiveField == null)
            return false;
        final Object archiveVal = this.getRequiredObject(archiveField.getName(), allowDBAccess);
        if (archiveField.getType().getJavaType().equals(Boolean.class))
            return ((Boolean) archiveVal).booleanValue();
        else
            return ((Number) archiveVal).intValue() > 0;
    }

    /**
     * Creates an SQLRow from these values, without any DB access.
     * 
     * @return an SQLRow with the same values as this.
     */
    public abstract SQLRow asRow();

    /**
     * Creates an SQLRowValues from these values, without any DB access.
     * 
     * @return an SQLRowValues with the same values as this.
     */
    public abstract SQLRowValues asRowValues();

    /**
     * Creates an SQLRowValues with just this ID, and no other values.
     * 
     * @return an empty SQLRowValues.
     */
    public final SQLRowValues createEmptyUpdateRow() {
        return new SQLRowValues(this.getTable()).setID(this.getIDNumber());
    }

    /**
     * Return the fields defined by this instance.
     * 
     * @return a Set of field names.
     */
    public abstract Set<String> getFields();

    public boolean contains(final String fieldName) {
        return this.getFields().contains(fieldName);
    }

    public abstract Object getObject(String fieldName);

    /**
     * Return the value for the passed field only if already present in this instance.
     * 
     * @param fieldName a field name.
     * @return the existing value for the passed field.
     * @throws IllegalArgumentException if there's no value for the passed field.
     */
    public final Object getContainedObject(String fieldName) throws IllegalArgumentException {
        return this.getObject(fieldName, true);
    }

    protected final Object getRequiredObject(String fieldName, final boolean allowDBAccess) throws IllegalArgumentException {
        // SQLRowValues cannot add a field value, so required means mustBePresent
        // SQLRow.getOject() can add and also checks whether the passed field is in its table, i.e.
        // fields are always required.
        return this.getObject(fieldName, this instanceof SQLRowValues || !allowDBAccess);
    }

    // MAYBE change paramter to enum MissingMode = THROW_EXCEPTION, ADD, RETURN_NULL
    public final Object getObject(String fieldName, final boolean mustBePresent) throws IllegalArgumentException {
        if (mustBePresent && !this.contains(fieldName)) {
            final String msg;
            if (this.getTable().contains(fieldName))
                msg = "Field " + fieldName + " not present in this : " + this.getFields() + " but exists in " + this.getTable();
            else
                msg = "Field " + fieldName + " neither present in this : " + this.getFields() + " nor " + this.getTable();
            throw new IllegalArgumentException(msg);
        }
        return this.getObject(fieldName);
    }

    /**
     * All objects in this row.
     * 
     * @return an immutable map.
     */
    public abstract Map<String, Object> getAbsolutelyAll();

    public final Map<String, Object> getValues(final SQLTable.VirtualFields vFields) {
        return this.getValues(this.getTable().getFieldsNames(vFields));
    }

    public final Map<String, Object> getValues(final Collection<String> fields) {
        return this.getValues(fields, false);
    }

    /**
     * Return the values of this row for the passed fields.
     * 
     * @param fields the keys.
     * @param includeMissingKeys <code>true</code> if a field only in the parameter should be
     *        returned with a <code>null</code> value (i.e. the result might contains fields not in
     *        {@link #getFields()}), <code>false</code> to not include it in the result (i.e. the
     *        fields of the result will be a subset of {@link #getFields()}).
     * @return the values of the passed fields.
     */
    public final Map<String, Object> getValues(final Collection<String> fields, final boolean includeMissingKeys) {
        this.initValues();
        final Map<String, Object> res = new LinkedHashMap<String, Object>();
        final Set<String> thisFields = this.getFields();
        for (final String f : fields) {
            if (includeMissingKeys || thisFields.contains(f))
                res.put(f, this.getObject(f));
        }
        return res;
    }

    protected void initValues() {
    }

    /**
     * Retourne le champ nommé <code>field</code> de cette ligne. Cette méthode formate la valeur en
     * fonction de son type, par exemple une date sera localisée.
     * 
     * @param field le nom du champ que l'on veut.
     * @return la valeur du champ sous forme de chaine, ou <code>null</code> si la valeur est NULL.
     */
    public final String getString(String field) {
        String result = null;
        Object obj = this.getObject(field);
        if (obj == null) {
            result = null;
        } else if (obj instanceof Date) {
            DateFormat df = DateFormat.getDateInstance(DateFormat.SHORT, Locale.getDefault());
            result = df.format((Date) obj);
        } else if (obj instanceof Clob) {
            try {
                result = StringClobConvertor.INSTANCE.unconvert((Clob) obj);
            } catch (Exception e) {
                e.printStackTrace();
                result = obj.toString();
            }
        } else {
            result = obj.toString();
        }
        return result;
    }

    /**
     * Retourne le champ nommé <code>field</code> de cette ligne.
     * 
     * @param field le nom du champ que l'on veut.
     * @return la valeur du champ sous forme d'int, ou <code>0</code> si la valeur est NULL.
     */
    public final int getInt(String field) {
        return getObjectAs(field, Number.class).intValue();
    }

    public final long getLong(String field) {
        return getObjectAs(field, Number.class).longValue();
    }

    public final float getFloat(String field) {
        return getObjectAs(field, Number.class).floatValue();
    }

    public final Boolean getBoolean(String field) {
        return getObjectAs(field, Boolean.class);
    }

    public final BigDecimal getBigDecimal(String field) {
        return getObjectAs(field, BigDecimal.class);
    }

    public final Calendar getDate(String field) {
        final Date d = this.getObjectAs(field, Date.class);
        if (d == null)
            return null;

        final Calendar cal = Calendar.getInstance();
        cal.setTime(d);
        return cal;
    }

    public final <T> T getObjectAs(String field, Class<T> clazz) {
        return this.getObjectAs(field, false, clazz);
    }

    public final <T> T getObjectAs(final String field, final boolean mustBePresent, final Class<T> clazz) {
        T res = null;
        try {
            res = clazz.cast(this.getObject(field, mustBePresent));
        } catch (ClassCastException e) {
            throw new IllegalArgumentException("Impossible d'accéder au champ " + field + " de la ligne " + this + " en tant que " + clazz.getSimpleName(), e);
        }
        return res;
    }

    /**
     * Returns the foreign table of <i>fieldName</i>.
     * 
     * @param fieldName the name of a foreign field, e.g. "ID_ARTICLE_2".
     * @return the table the field points to (never <code>null</code>), e.g. |ARTICLE|.
     * @throws IllegalArgumentException if <i>fieldName</i> is not a foreign field.
     */
    protected final SQLTable getForeignTable(String fieldName) throws IllegalArgumentException {
        return this.getForeignLink(Collections.singletonList(fieldName)).getTarget();
    }

    protected final Link getForeignLink(final List<String> fieldsNames) throws IllegalArgumentException {
        final DatabaseGraph graph = this.getTable().getDBSystemRoot().getGraph();
        final Link foreignLink = graph.getForeignLink(this.getTable(), fieldsNames);
        if (foreignLink == null)
            throw new IllegalArgumentException(fieldsNames + " are not a foreign key of " + this.getTable());
        return foreignLink;
    }

    /**
     * Return the foreign row, if any, for the passed field.
     * 
     * @param fieldName name of the foreign field.
     * @return <code>null</code> if the value of <code>fieldName</code> is <code>null</code>,
     *         otherwise a SQLRowAccessor with the value of <code>fieldName</code> as its ID.
     * @throws IllegalArgumentException if fieldName is not a foreign field.
     */
    public abstract SQLRowAccessor getForeign(String fieldName);

    /**
     * Return the non empty foreign row, if any, for the passed field.
     * 
     * @param fieldName name of the foreign field.
     * @return <code>null</code> if the value of <code>fieldName</code> is
     *         {@link #isForeignEmpty(String) empty}, otherwise a SQLRowAccessor with the value of
     *         <code>fieldName</code> as its ID.
     * @throws IllegalArgumentException if fieldName is not a foreign field or if the field isn't
     *         specified.
     * @see #getNonEmptyForeignIDNumber(String)
     */
    public final SQLRowAccessor getNonEmptyForeign(String fieldName) {
        if (this.isForeignEmpty(fieldName)) {
            return null;
        } else {
            final SQLRowAccessor res = this.getForeign(fieldName);
            assert res != null;
            return res;
        }
    }

    /**
     * Return the ID of a foreign row.
     * 
     * @param fieldName name of the foreign field.
     * @return the value of <code>fieldName</code>, {@link SQLRow#NONEXISTANT_ID} if
     *         <code>null</code>.
     * @throws IllegalArgumentException if fieldName is not a foreign field.
     */
    public final int getForeignID(String fieldName) throws IllegalArgumentException {
        final Number res = this.getForeignIDNumber(fieldName);
        return res == null ? SQLRow.NONEXISTANT_ID : res.intValue();
    }

    /**
     * Return the ID of a foreign row. NOTE : there's two cases when the result can be
     * <code>null</code> :
     * <ol>
     * <li><code>field</code> is defined and has the value <code>null</code></li>
     * <li><code>field</code> is defined and has an SQLRowValues value {@link #hasID() without an
     * ID} (i.e. field not defined or <code>null</code>)</li>
     * </ol>
     * In the second case, <code>field</code> is *not* {@link #isForeignEmpty(String) empty}, an ID
     * is just missing.
     * 
     * @param fieldName name of the foreign field.
     * @return the value of <code>fieldName</code> or {@link #getIDNumber()} if the value is a
     *         {@link SQLRowValues}, <code>null</code> if the actual value is.
     * @throws IllegalArgumentException if fieldName is not a foreign field or if the field isn't
     *         specified.
     */
    public final Number getForeignIDNumber(String fieldName) throws IllegalArgumentException {
        final Value<Number> res = getForeignIDNumberValue(fieldName);
        return res.hasValue() ? res.getValue() : null;
    }

    /**
     * Return the non empty foreign ID, if any, for the passed field.
     * 
     * @param fieldName name of the foreign field.
     * @return <code>null</code> if the value of <code>fieldName</code> is
     *         {@link #isForeignEmpty(String) empty}, otherwise the foreign ID for the passed field.
     * @throws IllegalArgumentException if fieldName is not a foreign field or if the field isn't
     *         specified.
     * @throws IllegalStateException if fieldName is not empty but lacks an ID (i.e. has a
     *         SQLRowValues without an ID) as by definition a <code>null</code> result means empty.
     * @see #getNonEmptyForeign(String)
     */
    public final Number getNonEmptyForeignIDNumber(String fieldName) {
        if (this.isForeignEmpty(fieldName)) {
            return null;
        } else {
            final Value<Number> res = this.getForeignIDNumberValue(fieldName);
            if (!res.hasValue())
                throw new IllegalStateException("Foreign row has no ID");
            assert res.getValue() != null;
            return res.getValue();
        }
    }

    /**
     * Return the ID of a foreign row.
     * 
     * @param fieldName name of the foreign field.
     * @return {@link Value#getNone()} if there's a {@link SQLRowValues} without
     *         {@link SQLRowValues#hasID() ID}, otherwise the value of <code>fieldName</code> or
     *         {@link #getIDNumber()} if the value is a {@link SQLRowValues}, never
     *         <code>null</code> (the {@link Value#getValue()} is <code>null</code> when
     *         <code>fieldName</code> is).
     * @throws IllegalArgumentException if fieldName is not a foreign field or if the field isn't
     *         specified.
     */
    public final Value<Number> getForeignIDNumberValue(final String fieldName) throws IllegalArgumentException {
        fetchIfNeeded(fieldName);
        // don't use getForeign() to avoid creating a SQLRow
        final Object val = this.getContainedObject(fieldName);
        if (val instanceof SQLRowValues) {
            final SQLRowValues vals = (SQLRowValues) val;
            return vals.hasID() ? Value.getSome(vals.getIDNumber()) : Value.<Number> getNone();
        } else {
            if (!this.getTable().getField(fieldName).isForeignKey())
                throw new IllegalArgumentException(fieldName + "is not a foreign key of " + this.getTable());
            return Value.getSome((Number) val);
        }
    }

    private void fetchIfNeeded(String fieldName) {
        if (getAccessDBIfNeeded() && (this instanceof SQLRow) && !contains(fieldName)) {
            assert false : "Missing " + fieldName + " in " + this;
            Log.get().log(Level.WARNING, "Missing " + fieldName + " in " + this, new IllegalStateException());
            ((SQLRow) this).fetchValues();
        }
    }

    /**
     * Whether the passed field is empty.
     * 
     * @param fieldName name of the foreign field.
     * @return <code>true</code> if {@link #getForeignIDNumber(String)} is the
     *         {@link SQLTable#getUndefinedIDNumber()}.
     * @throws IllegalArgumentException if fieldName is not a foreign field or if the field isn't
     *         specified.
     * @throws IllegalStateException if <code>fieldName</code> is <code>null</code> but the foreign
     *         table has an undefined ID.
     */
    public final boolean isForeignEmpty(String fieldName) {
        final Value<Number> fID = this.getForeignIDNumberValue(fieldName);
        if (!fID.hasValue()) {
            // a foreign row values without ID is *not* undefined
            return false;
        } else {
            // keep getForeignTable at the 1st line since it does the check
            final SQLTable foreignTable = this.getForeignTable(fieldName);
            final Number undefID = foreignTable.getUndefinedIDNumber();
            if (undefID != null && fID.getValue() == null) {
                // since a foreign row with ID=null !hasID() and thus getForeignIDNumberValue() is
                // none and this method returns false above
                assert this.getObject(fieldName) == null;
                throw new IllegalStateException("Null isn't a valid foreign key value when pointing to a table with undefined ID : " + undefID);
            }
            return NumberUtils.areNumericallyEqual(fID.getValue(), undefID);
        }
    }

    public abstract Collection<? extends SQLRowAccessor> getReferentRows();

    public abstract Collection<? extends SQLRowAccessor> getReferentRows(final SQLField refField);

    public abstract Collection<? extends SQLRowAccessor> getReferentRows(final SQLTable refTable);

    public final Collection<? extends SQLRowAccessor> followLink(final Link l) {
        return this.followLink(l, Direction.ANY);
    }

    /**
     * Return the rows linked to this one by <code>l</code>.
     * 
     * @param l the link to follow.
     * @param direction which way, one can pass {@link Direction#ANY} to infer it except for self
     *        references.
     * @return the rows linked to this one.
     * @see Step#create(SQLTable, SQLField, Direction)
     */
    public abstract Collection<? extends SQLRowAccessor> followLink(final Link l, final Direction direction);

    public final BigDecimal getOrder() {
        return (BigDecimal) this.getObject(this.getTable().getOrderField().getName());
    }

    public final Calendar getCreationDate() {
        final SQLField f = getTable().getCreationDateField();
        return f == null ? null : this.getDate(f.getName());
    }

    public final Calendar getModificationDate() {
        final SQLField f = getTable().getModifDateField();
        return f == null ? null : this.getDate(f.getName());
    }

    // avoid costly asRow()
    public final boolean equalsAsRow(SQLRowAccessor o) {
        return this.getTable() == o.getTable() && this.getID() == o.getID();
    }

    // avoid costly asRow()
    public final int hashCodeAsRow() {
        return this.getTable().hashCode() + this.getID();
    }

    // return the all current field values
    public abstract String mapToString();
}