Dépôt officiel du code source de l'ERP OpenConcerto
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();
}
}