OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

Rev 144 | 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.element;

import org.openconcerto.sql.element.SQLElement.ReferenceAction;
import org.openconcerto.sql.model.SQLField;
import org.openconcerto.sql.model.SQLFieldRowProcessor;
import org.openconcerto.sql.model.SQLResultSet;
import org.openconcerto.sql.model.SQLRow;
import org.openconcerto.sql.model.SQLSelect;
import org.openconcerto.sql.model.SQLSyntax;
import org.openconcerto.sql.model.SQLTable;
import org.openconcerto.sql.model.SQLType;
import org.openconcerto.sql.model.Where;
import org.openconcerto.sql.model.graph.Link;
import org.openconcerto.sql.model.graph.Link.Direction;
import org.openconcerto.sql.model.graph.Path;
import org.openconcerto.sql.model.graph.Step;
import org.openconcerto.utils.CollectionUtils;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import org.apache.commons.dbutils.ResultSetHandler;

import net.jcip.annotations.Immutable;

/**
 * A logical link between two elements. It can be a direct foreign {@link Link} or two links through
 * a {@link JoinSQLElement join}. The {@link #getOwner()} needs the {@link #getOwned()}, i.e. if the
 * owner is unarchived the owned will also be unarchived. The owner is responsible for
 * {@link SQLElement#setupLinks(org.openconcerto.sql.element.SQLElement.LinksSetup) setting up} the properties of
 * the link.
 * 
 * @author Sylvain
 */
public final class SQLElementLink {

    public static enum LinkType {
        /** One element is the parent of the other */
        PARENT,
        /** One element is part of the other */
        COMPOSITION,
        /** One element references the other */
        ASSOCIATION
    }

    private final SQLElement owner, owned;
    private final Path path;
    private final LinkType type;
    private final String name;
    // TODO final (see #setAction())
    private ReferenceAction action;

    protected SQLElementLink(SQLElement owner, Path path, SQLElement owned, final LinkType type, final String name, final ReferenceAction action) {
        super();
        final int length = path.length();
        if (length == 0)
            throw new IllegalArgumentException("Empty path");
        if (owner != null && owner.getTable() != path.getFirst() || owned.getTable() != path.getLast())
            throw new IllegalArgumentException("Wrong path : " + path + " not from owner : " + owner + " to owned : " + owned);
        if (!path.isSingleLink())
            throw new IllegalArgumentException("Isn't single link : " + path);
        // either foreign key or join
        if (length > 2)
            throw new IllegalArgumentException("Path too long : " + path);
        final Step lastStep = path.getStep(-1);
        final boolean endsWithForeign = lastStep.getDirection().equals(Direction.FOREIGN);
        if (length == 1) {
            if (!endsWithForeign)
                throw new IllegalArgumentException("Single step path isn't foreign : " + path);
        } else {
            assert length == 2;
            if (!endsWithForeign || !path.getStep(0).getDirection().equals(Direction.REFERENT))
                throw new IllegalArgumentException("Two steps path isn't a join : " + path);
        }
        if (lastStep.getSingleField() == null)
            throw new IllegalArgumentException("Multi-field not yet supported : " + lastStep);
        this.path = path;
        this.owner = owner;
        this.owned = owned;
        if (type == null || action == null)
            throw new NullPointerException();
        this.type = type;
        if (name != null) {
            this.name = name;
        } else if (length == 1) {
            final SQLField singleField = lastStep.getSingleField();
            this.name = singleField != null ? singleField.getName() : lastStep.getSingleLink().getName();
        } else {
            this.name = lastStep.getFrom().getName();
        }
        assert this.getName() != null;
        this.action = action;
    }

    /**
     * The path from the {@link #getOwner() owner} to the {@link #getOwned() owned}. NOTE : the last
     * step is always {@link Direction#FOREIGN}.
     * 
     * @return the path of this link, its {@link Path#length() length} is 1 or 2 for a join.
     */
    public final Path getPath() {
        return this.path;
    }

    public final boolean isJoin() {
        return this.path.length() == 2;
    }

    /**
     * Return the single link.
     * 
     * @return the single foreign link, <code>null</code> if and only if this is a {@link #isJoin()
     *         join} as multi-link paths are invalid.
     */
    public final Link getSingleLink() {
        if (this.isJoin())
            return null;
        final Link res = this.path.getStep(0).getSingleLink();
        // checked in the constructor
        assert res != null;
        return res;
    }

    /**
     * Return the single field of this link.
     * 
     * @return the foreign field of this link, <code>null</code> if and only if this is a
     *         {@link #isJoin() join} as multi-field link are not yet supported.
     */
    public final SQLField getSingleField() {
        final Link l = this.getSingleLink();
        if (l == null)
            return null;
        final SQLField res = l.getSingleField();
        // checked in the constructor
        assert res != null;
        return res;
    }

    public final SQLElement getOwner() {
        return this.owner;
    }

    public final SQLElement getOwned() {
        return this.owned;
    }

    public final JoinSQLElement getJoinElement() {
        if (!this.isJoin())
            return null;
        return (JoinSQLElement) this.getOwner().getElement(this.getPath().getTable(1));
    }

    public final boolean isOwnerTheParent() {
        final boolean owner;
        if (this.getLinkType().equals(LinkType.COMPOSITION))
            owner = true;
        else if (this.getLinkType().equals(LinkType.PARENT))
            owner = false;
        else
            throw new IllegalStateException("Invalid type : " + this.getLinkType());
        return owner;
    }

    public final SQLElement getParent() {
        return this.getParentOrChild(true);
    }

    private final SQLElement getParentOrChild(final boolean parent) {
        return parent == isOwnerTheParent() ? this.getOwner() : this.getOwned();
    }

    public final SQLElement getChild() {
        return this.getParentOrChild(false);
    }

    public final Path getPathToParent() {
        return this.getPathToParentOrChild(true);
    }

    public final Step getStepToParent() {
        return this.getPathToParent().getStep(-1);
    }

    private final Path getPathToParentOrChild(final boolean toParent) {
        return toParent == isOwnerTheParent() ? this.getPath().reverse() : this.getPath();
    }

    public final Path getPathToChild() {
        return this.getPathToParentOrChild(false);
    }

    public final Step getStepToChild() {
        return this.getPathToChild().getStep(-1);
    }

    public final List<SQLRow> getRowsUntilRoot(final Number id) {
        return this.getRowsUntilRoot(id, null).getRows();
    }

    /**
     * Get all rows above the passed row (including it).
     * 
     * @param id the first row.
     * @param fields which fields to fetch, <code>null</code> to fetch all.
     * @return all rows from the passed one to the root.
     */
    public final RecursiveRows getRowsUntilRoot(final Number id, Set<String> fields) {
        return getRecursiveRows(true, id, fields, -1, null);
    }

    public final List<SQLRow> getSubTreeRows(final Number id) {
        return this.getSubTreeRows(id, null).getRows();
    }

    public final RecursiveRows getSubTreeRows(final Number id, final Set<String> fields) {
        return getSubTreeRows(id, fields, -1);
    }

    /**
     * Get all rows beneath the passed root (including it).
     * 
     * @param id the root row.
     * @param fields which fields to fetch, <code>null</code> to fetch all.
     * @param maxLevel the max number of times to go through the link.
     * @return all rows are deterministically ordered (by level, parent order, order ; i.e. root
     *         first).
     */
    public final RecursiveRows getSubTreeRows(final Number id, final Set<String> fields, final int maxLevel) {
        return getRecursiveRows(false, id, fields, maxLevel, getOwned().getTable().getOrderField());
    }

    static private final String findUnusedName(final Collection<String> usedNames, final String base) {
        String res = base;
        int i = 0;
        while (usedNames.contains(res)) {
            res = base + i++;
        }
        return res;
    }

    @Immutable
    static public final class RecursiveRows {

        static public final RecursiveRows ZERO_LEVEL = new RecursiveRows(0, Collections.emptyList(), Collections.emptyMap());

        private final int maxLevel;
        private final List<SQLRow> rows;
        private final Map<SQLRow, List<Number>> cycles;

        RecursiveRows(final int maxLevel, final List<SQLRow> rows, final Map<SQLRow, List<Number>> cycles) {
            super();
            this.maxLevel = maxLevel;
            this.rows = Collections.unmodifiableList(rows);
            // OK since List<Number> are already immutable
            this.cycles = Collections.unmodifiableMap(cycles);
        }

        public final int getMaxLevelRequested() {
            return this.maxLevel;
        }

        public final List<SQLRow> getRows() {
            if (this.getCycles().isEmpty())
                return this.getPartialRows();
            else
                throw new IllegalStateException("Cycle detected : " + this.getCycles());
        }

        public final List<SQLRow> getPartialRows() {
            return this.rows;
        }

        public final Map<SQLRow, List<Number>> getCycles() {
            return this.cycles;
        }
    }

    private final RecursiveRows getRecursiveRows(final boolean foreign, final Number id, Set<String> fields, final int maxLevel, final SQLField orderField) {
        if (this.getOwner() != this.getOwned() || this.isJoin())
            throw new IllegalStateException("Not a recurive link : " + this);
        final SQLTable t = getOwned().getTable();
        final Link singleLink = this.getSingleLink();
        final SQLField singleField = singleLink.getSingleField();
        if (singleField == null)
            throw new UnsupportedOperationException("Multiple fields not yet supported : " + singleLink);
        Objects.requireNonNull(id, "id is null");

        if (maxLevel == 0)
            return RecursiveRows.ZERO_LEVEL;

        final SQLSyntax syntax = t.getDBSystemRoot().getSyntax();

        if (fields == null)
            fields = t.getFieldsName();
        final String recursiveT = "recT";
        // use array to prevent infinite loop
        final String visitedIDsF = findUnusedName(fields, "visitedIDs");
        final String visitedIDsRef = recursiveT + '.' + visitedIDsF;
        final String visitedIDsCount = syntax.getSQLArrayLength(visitedIDsRef);
        // boolean to know about endless loops : we don't stop before visiting a row a second time,
        // but just after
        final String loopF = findUnusedName(fields, "loop");

        // firstly visitedIDsF, secondly optional order, then the asked fields
        final SQLSelect selNonRec = new SQLSelect();
        selNonRec.addRawSelect(syntax.getSQLArray(Collections.singletonList(t.getKey().getFieldRef())), visitedIDsF);
        selNonRec.addRawSelect(SQLType.getBoolean(syntax).toString(Boolean.FALSE), loopF);
        final boolean useOrder = !foreign && orderField != null;
        if (useOrder)
            selNonRec.addRawSelect(syntax.cast("null", orderField.getType().getJavaType()), "parentOrder");
        selNonRec.addAllSelect(t, fields);
        if (!fields.contains(singleField.getName()))
            selNonRec.addSelect(singleField);
        // need PK for SQLRow
        if (!fields.contains(t.getKey().getName()))
            selNonRec.addSelect(t.getKey());
        selNonRec.setWhere(new Where(t.getKey(), "=", id));

        // recursive SELECT
        final StringBuilder recSelect = new StringBuilder("SELECT ");
        recSelect.append(syntax.getSQLArrayAppend(visitedIDsRef, t.getKey().getFieldRef())).append(", ");
        recSelect.append(syntax.getSQLArrayContains(visitedIDsRef, t.getKey().getFieldRef())).append(", ");
        final int index;
        if (useOrder) {
            recSelect.append(recursiveT).append('.').append(orderField.getName()).append(", ");
            index = 3;
        } else {
            index = 2;
        }
        recSelect.append(CollectionUtils.join(selNonRec.getSelect().subList(index, selNonRec.getSelect().size()), ", "));
        recSelect.append("\nFROM ").append(t.getSQLName().quote()).append(", ").append(recursiveT);
        recSelect.append("\nWHERE ");
        if (foreign) {
            recSelect.append(t.getKey().getFieldRef()).append(" = ").append(recursiveT + '.' + singleField.getName());
        } else {
            recSelect.append(singleField.getFieldRef()).append(" = ").append(recursiveT + '.' + t.getKey().getName());
        }
        // avoid infinite loop
        recSelect.append(" and not (").append(recursiveT).append('.').append(loopF).append(')');
        if (t.getUndefinedIDNumber() != null) {
            recSelect.append(" and ").append(new Where(t.getKey(), "!=", t.getUndefinedID()).getClause());
        }
        if (maxLevel > 0) {
            recSelect.append(" and ").append(visitedIDsCount).append(" < ").append(maxLevel);
        }

        String cte = "with recursive " + recursiveT + "(" + CollectionUtils.join(selNonRec.getSelectNames(), ", ") + ") as (\n" + selNonRec.asString() + "\nunion all\n" + recSelect
                + ")\nSELECT * from " + recursiveT + " ORDER BY " + visitedIDsCount;
        if (useOrder) {
            cte += ", 2, " + recursiveT + '.' + orderField.getName();
        }

        final List<String> rsNames = new ArrayList<>(selNonRec.getSelectNames());
        // int[] visited IDs
        rsNames.set(0, null);
        // boolean loop
        rsNames.set(1, null);
        if (useOrder)
            rsNames.set(2, null);

        final List<SQLRow> res = new ArrayList<>();
        final Map<SQLRow, List<Number>> cycleRows = new HashMap<>();
        final Class<? extends Number> keyType = t.getKey().getType().getJavaType().asSubclass(Number.class);
        t.getDBSystemRoot().getDataSource().execute(cte, new ResultSetHandler() {
            @Override
            public Object handle(ResultSet rs) throws SQLException {
                final SQLFieldRowProcessor rowProc = new SQLFieldRowProcessor(t, rsNames);
                while (rs.next()) {
                    final SQLRow row = SQLRow.createFromRS(t, rs, rowProc, true);
                    final boolean looped = rs.getBoolean(2);
                    if (looped) {
                        cycleRows.put(row, SQLResultSet.getList(rs, 1, keyType));
                    } else {
                        res.add(row);
                    }
                }
                return null;
            }
        });
        return new RecursiveRows(maxLevel, res, cycleRows);
    }

    public final LinkType getLinkType() {
        return this.type;
    }

    public final String getName() {
        return this.name;
    }

    public final ReferenceAction getAction() {
        return this.action;
    }

    // use SQLElementLinkSetup
    @Deprecated
    public final void setAction(ReferenceAction action) {
        final List<ReferenceAction> possibleActions = getOwner().getPossibleActions(this.getLinkType(), this.getOwned());
        if (!possibleActions.contains(action))
            throw new IllegalArgumentException("Not allowed : " + action);
        this.action = action;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + this.action.hashCode();
        result = prime * result + this.path.hashCode();
        result = prime * result + this.type.hashCode();
        return result;
    }

    // don't use SQLElement to avoid walking the graph
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        final SQLElementLink other = (SQLElementLink) obj;
        return this.action.equals(other.action) && this.path.equals(other.path) && this.type.equals(other.type);
    }

    @Override
    public String toString() {
        return this.getClass().getSimpleName() + " '" + this.getName() + "' " + this.getLinkType() + " " + this.getPath();
    }
}