Dépôt officiel du code source de l'ERP OpenConcerto
Rev 151 | 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.model.SQLRowValues.CreateMode;
import org.openconcerto.sql.model.SQLRowValuesCluster.State;
import org.openconcerto.sql.model.SQLRowValuesCluster.WalkOptions;
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.CollectionMap2Itf.ListMapItf;
import org.openconcerto.utils.CollectionUtils;
import org.openconcerto.utils.CompareUtils;
import org.openconcerto.utils.CopyUtils;
import org.openconcerto.utils.ListMap;
import org.openconcerto.utils.RTInterruptedException;
import org.openconcerto.utils.RecursionType;
import org.openconcerto.utils.SetMap;
import org.openconcerto.utils.Tuple2;
import org.openconcerto.utils.cc.ITransformer;
import org.openconcerto.utils.cc.LinkedIdentitySet;
import org.openconcerto.utils.cc.Transformer;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.commons.dbutils.ResultSetHandler;
import net.jcip.annotations.GuardedBy;
import net.jcip.annotations.ThreadSafe;
/**
* Construct a list of linked SQLRowValues from one request.
*
* @author Sylvain
*/
@ThreadSafe
public class SQLRowValuesListFetcher {
/**
* Create an ordered fetcher with the necessary grafts to fetch the passed graph.
*
* @param graph what to fetch, can be any tree.
* @return the fetcher.
*/
public static SQLRowValuesListFetcher create(final SQLRowValues graph) {
// ORDER shouldn't slow down the query and it makes the result predictable and repeatable
return create(graph, true);
}
public static SQLRowValuesListFetcher create(final SQLRowValues graph, final boolean ordered) {
// path -> longest referent only path
// i.e. map each path to the main fetcher or a referent graft
final Map<Path, Path> handledPaths = new HashMap<Path, Path>();
final Path emptyPath = Path.get(graph.getTable());
handledPaths.put(emptyPath, emptyPath);
// find out referent only paths (yellow in the diagram)
graph.getGraph().walk(graph, null, new ITransformer<State<Object>, Object>() {
@Override
public Path transformChecked(State<Object> input) {
final Path p = input.getPath();
for (int i = p.length(); i > 0; i--) {
final Path subPath = p.subPath(0, i);
if (handledPaths.containsKey(subPath))
break;
handledPaths.put(subPath, p);
}
return null;
}
}, RecursionType.DEPTH_FIRST, Direction.REFERENT);
// find out needed grafts
final ListMap<Path, SQLRowValuesListFetcher> grafts = new ListMap<Path, SQLRowValuesListFetcher>();
graph.getGraph().walk(graph, null, new ITransformer<State<Object>, Object>() {
@Override
public Path transformChecked(State<Object> input) {
final Path p = input.getPath();
if (!handledPaths.containsKey(p)) {
final Path pMinusLast = p.minusLast();
if (!input.isBackwards()) {
// Forwards can be fetched by existing fetcher (blue in the diagram)
final Path existingRefPath = handledPaths.get(pMinusLast);
assert existingRefPath != null;
handledPaths.put(p, existingRefPath);
} else {
// Backwards needs another fetcher
if (!grafts.containsKey(pMinusLast)) {
final SQLRowValues copy = graph.deepCopy();
final SQLRowValues graftNode = copy.followPath(pMinusLast);
graftNode.clear();
final SQLRowValues previous = copy.followPath(pMinusLast.minusLast());
assert p.getStep(-2).isForeign();
previous.remove(p.getStep(-2).getSingleField().getName());
// don't recurse forever
if (previous.getGraph() == graftNode.getGraph())
throw new IllegalArgumentException("Graph is not a tree");
// ATTN pMinusLast might not be on the main fetcher so don't graft now
// also we can only graft non empty descendant path fetchers (plus
// removing a fetcher saves one request)
final SQLRowValuesListFetcher rec = create(graftNode, ordered);
final Collection<SQLRowValuesListFetcher> ungrafted = rec.ungraft();
if (ungrafted == null || ungrafted.size() == 0) {
// i.e. only one referent and thus graft not necessary
assert rec.descendantPath.length() > 0;
grafts.add(pMinusLast, rec);
} else {
grafts.addAll(pMinusLast, ungrafted);
}
}
throw new SQLRowValuesCluster.StopRecurseException().setCompletely(false);
}
}
return null;
}
}, new WalkOptions(Direction.ANY).setRecursionType(RecursionType.BREADTH_FIRST).setStartIncluded(false));
final Set<Path> refPaths = new HashSet<Path>(handledPaths.values());
// remove the main fetcher
refPaths.remove(emptyPath);
// fetchers for the referent paths (yellow part)
final Map<Path, SQLRowValuesListFetcher> graftedFetchers;
// create the main fetcher and grafts
final SQLRowValuesListFetcher res;
if (refPaths.size() == 1) {
res = new SQLRowValuesListFetcher(graph, refPaths.iterator().next());
graftedFetchers = Collections.emptyMap();
} else {
res = new SQLRowValuesListFetcher(graph, false);
graftedFetchers = new HashMap<Path, SQLRowValuesListFetcher>();
if (refPaths.size() > 0) {
final Path graftPath = new Path(graph.getTable());
final SQLRowValues copy = graph.deepCopy();
copy.clear();
for (final Path refPath : refPaths) {
final SQLRowValuesListFetcher f = new SQLRowValuesListFetcher(copy, refPath, true).setOrdered(ordered);
res.graft(f, graftPath);
graftedFetchers.put(refPath, f);
}
}
}
res.setOrdered(ordered);
// now graft recursively created grafts
for (final Entry<Path, ? extends Collection<SQLRowValuesListFetcher>> e : grafts.entrySet()) {
final Path graftPath = e.getKey();
final Path refPath = handledPaths.get(graftPath);
// can be grafted on the main fetcher or on the referent fetchers
final SQLRowValuesListFetcher f = graftedFetchers.containsKey(refPath) ? graftedFetchers.get(refPath) : res;
for (final SQLRowValuesListFetcher recFetcher : e.getValue())
f.graft(recFetcher, graftPath);
}
return res;
}
// return the referent single link path starting from graph
private static Path computePath(SQLRowValues graph) {
// check that there's only one referent for each row
// (otherwise huge joins, e.g. LOCAL<-CPI,SOURCE,RECEPTEUR,etc.)
final AtomicReference<Path> res = new AtomicReference<Path>(null);
graph.getGraph().walk(graph, null, new ITransformer<State<Path>, Path>() {
@Override
public Path transformChecked(State<Path> input) {
final Collection<SQLRowValues> referentRows = input.getCurrent().getReferentRows();
final int size = referentRows.size();
if (size > 1) {
// remove the foreign rows which are all the same (since they point to
// current) so the exn is more legible
final List<SQLRowValues> toPrint = SQLRowValues.trim(referentRows);
throw new IllegalArgumentException(input.getCurrent() + " is referenced by " + toPrint + "\nat " + input.getPath());
} else if (size == 0) {
if (res.get() == null)
res.set(input.getPath());
else
throw new IllegalStateException();
}
return input.getAcc();
}
}, RecursionType.BREADTH_FIRST, Direction.REFERENT);
// since includeStart=true
assert res.get() != null;
return res.get();
}
static private final ListMap<Tuple2<Path, Number>, SQLRowValues> createCollectionMap() {
// we need a List in merge()
return new ListMap<Tuple2<Path, Number>, SQLRowValues>() {
@Override
public List<SQLRowValues> createCollection(Collection<? extends SQLRowValues> v) {
final List<SQLRowValues> res = new ArrayList<SQLRowValues>(8);
res.addAll(v);
return res;
}
};
}
// unmodifiable
private final SQLRowValues graph;
private final Path descendantPath;
@GuardedBy("this")
private List<ITransformer<SQLSelect, SQLSelect>> selTransf;
@GuardedBy("this")
private Number selID;
@GuardedBy("this")
private Set<Path> ordered;
@GuardedBy("this")
private boolean descendantsOrdered;
@GuardedBy("this")
private SQLRowValues minGraph;
@GuardedBy("this")
private boolean includeForeignUndef;
@GuardedBy("this")
private SQLSelect frozen;
@GuardedBy("this")
private boolean freezeRows;
// graftPlace -> {referent path -> fetcher}, unmodifiable
@GuardedBy("this")
private Map<Path, Map<Path, SQLRowValuesListFetcher>> grafts;
// {pathToAdd, existingPath}, unmodifiable
@GuardedBy("this")
private Map<Path, Path> postFetchLinks;
/**
* Construct a new instance with the passed graph of SQLRowValues.
*
* @param graph what SQLRowValues should be returned by {@link #fetch()}, eg <code>new
* SQLRowValues("SITE").setAllToNull().put("ID_CONTACT", new SQLRowValues("CONTACT"))</code>
* to return all sites (with all their fields) with their associated contacts.
*/
public SQLRowValuesListFetcher(SQLRowValues graph) {
this(graph, false);
}
/**
* Construct a new instance with the passed graph of SQLRowValues. Eg if <code>graph</code> is a
* BATIMENT which points to SITE, is pointed by LOCAL, CPI_BT and <code>referents</code> is
* <code>true</code>, {@link #fetch()} could return
*
* <pre>
* SITE[2] BATIMENT[2] LOCAL[2] CPI_BT[3]
* CPI_BT[2]
* LOCAL[3]
* LOCAL[5] CPI_BT[5]
* SITE[7] BATIMENT[3] LOCAL[4] CPI_BT[4]
* SITE[7] BATIMENT[4]
* </pre>
*
* @param graph what SQLRowValues should be returned by {@link #fetch()}, eg <code>new
* SQLRowValues("SITE").setAllToNull().put("ID_CONTACT", new SQLRowValues("CONTACT"))</code>
* to return all sites (with all their fields) with their associated contacts.
* @param referents <code>true</code> if referents to <code>graph</code> should also be fetched.
*/
public SQLRowValuesListFetcher(SQLRowValues graph, final boolean referents) {
this(graph, referents ? computePath(graph) : null);
}
/**
* Construct a new instance.
*
* @param graph what SQLRowValues should be returned by {@link #fetch()}.
* @param referentPath a {@link Path#isSingleField() single link} path from the primary table,
* <code>null</code> meaning don't fetch referent rows.
*/
public SQLRowValuesListFetcher(SQLRowValues graph, final Path referentPath) {
this(graph, referentPath, true);
}
/**
* Construct a new instance.
*
* @param graph what SQLRowValues should be returned by {@link #fetch()}.
* @param referentPath a {@link Path#isSingleField() single link} path from the primary table,
* <code>null</code> meaning don't fetch referent rows.
* @param prune if <code>true</code> the graph will be pruned to only contain
* <code>referentPath</code>. If <code>false</code> the graph will be kept as is, which
* can produce undefined results if there exist more than one referent row outside of
* <code>referentPath</code>.
*/
SQLRowValuesListFetcher(final SQLRowValues graph, final Path referentPath, final boolean prune) {
super();
this.graph = graph.deepCopy();
this.descendantPath = referentPath == null ? Path.get(graph.getTable()) : referentPath;
if (!this.descendantPath.isDirection(Direction.REFERENT))
throw new IllegalArgumentException("path is not (exclusively) referent : " + this.descendantPath);
final SQLRowValues descRow = this.graph.followPath(this.descendantPath);
if (descRow == null)
throw new IllegalArgumentException("path is not contained in the passed rowValues : " + referentPath + "\n" + this.graph.printTree());
// followPath() do the following check
assert this.descendantPath.getFirst() == this.graph.getTable() && this.descendantPath.isSingleField();
if (prune) {
this.graph.getGraph().walk(descRow, null, new ITransformer<State<Object>, Object>() {
@Override
public Object transformChecked(State<Object> input) {
if (input.getFrom() == null) {
input.getCurrent().clearReferents();
} else {
input.getCurrent().retainReferent(input.getPrevious());
}
return null;
}
}, RecursionType.BREADTH_FIRST, Direction.FOREIGN);
}
// always need IDs
for (final SQLRowValues curr : this.graph.getGraph().getItems()) {
// don't overwrite existing values
if (!curr.hasID())
curr.setID(null);
}
this.graph.getGraph().freeze();
synchronized (this) {
this.selTransf = Collections.emptyList();
this.selID = null;
this.ordered = Collections.<Path> emptySet();
this.descendantsOrdered = false;
this.minGraph = null;
this.includeForeignUndef = false;
this.frozen = null;
this.freezeRows = false;
this.grafts = Collections.emptyMap();
this.postFetchLinks = Collections.emptyMap();
}
}
// be aware that the new instance will share the same selTransf, and if it doesn't directly
// (with copyTransf) some state can still be shared
private SQLRowValuesListFetcher(SQLRowValuesListFetcher f, final boolean copyTransf) {
synchronized (f) {
this.graph = f.getGraph().toImmutable();
this.descendantPath = f.getReferentPath();
// can't deadlock since this hasn't been published
synchronized (this) {
this.selTransf = copyTransf ? CopyUtils.copy(f.selTransf) : f.selTransf;
this.selID = f.getSelID();
this.ordered = f.getOrder();
this.descendantsOrdered = f.areReferentsOrdered();
this.minGraph = f.minGraph == null ? null : f.minGraph.toImmutable();
this.includeForeignUndef = f.includeForeignUndef;
// a new instance is always mutable
this.frozen = null;
this.freezeRows = f.freezeRows;
// Recursively copy grafts
final Map<Path, Map<Path, SQLRowValuesListFetcher>> outerMutable = new HashMap<Path, Map<Path, SQLRowValuesListFetcher>>(f.grafts);
for (final Entry<Path, Map<Path, SQLRowValuesListFetcher>> e : outerMutable.entrySet()) {
final Map<Path, SQLRowValuesListFetcher> innerMutable = new HashMap<Path, SQLRowValuesListFetcher>(e.getValue());
for (final Entry<Path, SQLRowValuesListFetcher> innerEntry : innerMutable.entrySet()) {
innerEntry.setValue(new SQLRowValuesListFetcher(innerEntry.getValue(), copyTransf));
}
e.setValue(Collections.unmodifiableMap(innerMutable));
}
this.grafts = Collections.unmodifiableMap(outerMutable);
this.postFetchLinks = f.postFetchLinks;
}
}
}
/**
* Get a frozen version of this. If not already {@link #isFrozen() frozen}, copy this and its
* grafts and {@link #freeze()} the copy.
*
* @return <code>this</code> if already frozen, otherwise a frozen copy of <code>this</code>.
*/
public final SQLRowValuesListFetcher toUnmodifiable() {
synchronized (this) {
if (this.isFrozen())
return this;
// no need to try to deep copy since we freeze before releasing the lock
return new SQLRowValuesListFetcher(this, false).freeze();
}
}
/**
* Make this instance immutable. Ie all setters will now throw {@link IllegalStateException}.
* Furthermore the request will be computed now once and for all, so as not to be subject to
* outside modification by {@link #getSelectTransformers()}.
*
* @return this.
*/
public synchronized final SQLRowValuesListFetcher freeze() {
if (!this.isFrozen()) {
this.frozen = new SQLSelect(this.getReq());
for (final Map<Path, SQLRowValuesListFetcher> m : this.grafts.values()) {
for (final SQLRowValuesListFetcher f : m.values())
f.freeze();
}
}
return this;
}
public synchronized final boolean isFrozen() {
return this.frozen != null;
}
private final void checkFrozen() {
if (this.isFrozen())
throw new IllegalStateException("this has been frozen: " + this);
}
/**
* Whether the rows returned by {@link #fetch()} are {@link SQLRowValues#isFrozen()
* unmodifiable}.
*
* @param b <code>true</code> to make all rows unmodifiable.
*/
public synchronized final void setReturnedRowsUnmodifiable(final boolean b) {
this.checkFrozen();
this.freezeRows = b;
}
/**
* Whether the rows returned by {@link #fetch()} are {@link SQLRowValues#isFrozen()
* unmodifiable}.
*
* @return <code>true</code> if all rows are returned unmodifiable.
*/
public synchronized boolean areReturnedRowsUnmodifiable() {
return this.freezeRows;
}
public SQLRowValues getGraph() {
return this.graph;
}
public final Path getReferentPath() {
return this.descendantPath;
}
/**
* Whether to include undefined rows (of tables other than the graph's).
*
* @param includeForeignUndef <code>true</code> to include undefined rows.
*/
public synchronized final void setIncludeForeignUndef(boolean includeForeignUndef) {
this.checkFrozen();
this.includeForeignUndef = includeForeignUndef;
}
/**
* Require that only rows with values for the full graph are returned. Eg if the graph is CPI ->
* OBS, setting this to <code>true</code> will excludes CPI without OBS.
*
* @param b <code>true</code> if only full rows should be fetched.
*/
public synchronized final void setFullOnly(boolean b) {
this.checkFrozen();
if (b)
this.minGraph = this.getGraph().deepCopy();
else
this.minGraph = null;
}
// MAYBE allow to remove by changing to
// addRequiredPath(Path)
// removeRequiredPath(Path)
// -> just a Set of Path, reduced at the start of fetch()
public synchronized final void requirePath(final Path p) {
this.checkFrozen();
if (this.getGraph().followPath(p) == null)
throw new IllegalArgumentException("Path not included in this graph : " + p + "\n" + this.getGraph().printGraph());
if (this.minGraph == null)
this.minGraph = new SQLRowValues(getGraph().getTable());
this.minGraph.assurePath(p);
}
private synchronized final boolean isPathRequired(final Path p) {
return this.minGraph != null && this.minGraph.followPath(p) != null;
}
private boolean fetchReferents() {
return this.descendantPath.length() > 0;
}
/**
* To modify the query before execution.
*
* @param selTransf will be passed the query which has been constructed, and the return value
* will be actually executed, can be <code>null</code>.
*/
public synchronized void setSelTransf(ITransformer<SQLSelect, SQLSelect> selTransf) {
this.checkFrozen();
this.selTransf = selTransf != null ? Collections.singletonList(selTransf) : Collections.<ITransformer<SQLSelect, SQLSelect>> emptyList();
}
public void clearSelTransf() {
this.setSelTransf(null);
}
public void appendSelTransf(ITransformer<SQLSelect, SQLSelect> selTransf) {
this.addSelTransf(selTransf, -1);
}
public void prependSelTransf(ITransformer<SQLSelect, SQLSelect> selTransf) {
this.addSelTransf(selTransf, 0);
}
public synchronized void addSelTransf(ITransformer<SQLSelect, SQLSelect> selTransf, final int index) {
this.checkFrozen();
if (selTransf != null) {
final List<ITransformer<SQLSelect, SQLSelect>> copy = new ArrayList<ITransformer<SQLSelect, SQLSelect>>(this.selTransf);
final int size = copy.size();
final int realIndex = index < 0 ? size + index + 1 : index;
copy.add(realIndex, selTransf);
this.selTransf = Collections.unmodifiableList(copy);
}
}
public synchronized boolean removeSelTransf(ITransformer<SQLSelect, SQLSelect> selTransf) {
this.checkFrozen();
if (selTransf != null) {
final List<ITransformer<SQLSelect, SQLSelect>> copy = new ArrayList<ITransformer<SQLSelect, SQLSelect>>(this.selTransf);
if (copy.remove(selTransf)) {
this.selTransf = Collections.unmodifiableList(copy);
return true;
}
}
return false;
}
public synchronized final ITransformer<SQLSelect, SQLSelect> getSelTransf() {
if (this.selTransf.size() > 1)
throw new IllegalStateException("More than one transformer");
return CollectionUtils.getFirst(this.selTransf);
}
public synchronized final List<ITransformer<SQLSelect, SQLSelect>> getSelectTransformers() {
return this.selTransf;
}
/**
* Add a where in {@link #getReq()} to restrict the primary key.
*
* @param selID an ID for the primary key, <code>null</code> to not filter.
*/
public synchronized void setSelID(Number selID) {
this.checkFrozen();
this.selID = selID;
}
public synchronized final Number getSelID() {
return this.selID;
}
/**
* Whether to add ORDER BY in {@link #getReq()}.
*
* @param b <code>true</code> if the query should be ordered.
* @return this.
*/
public synchronized final SQLRowValuesListFetcher setOrdered(final boolean b) {
this.setOrder(b ? Collections.singleton(Path.get(getGraph().getTable())) : Collections.<Path> emptySet(), true);
this.setReferentsOrdered(b, false);
return this;
}
public final SQLRowValuesListFetcher setOrder(final List<Path> order) {
return this.setOrder(order, false);
}
private synchronized final SQLRowValuesListFetcher setOrder(final Collection<Path> order, final boolean safeVal) {
this.checkFrozen();
for (final Path p : order)
if (this.getGraph().followPath(p) == null)
throw new IllegalArgumentException("Path not in this " + p);
this.ordered = safeVal ? (Set<Path>) order : Collections.unmodifiableSet(new LinkedHashSet<Path>(order));
return this;
}
public synchronized final Set<Path> getOrder() {
return this.ordered;
}
/**
* Whether to order referent rows in this fetcher.
*
* @param b <code>true</code> to order referent rows starting from the primary node, e.g. if the
* graph is
*
* <pre>
* *SITE* <- BATIMENT <- LOCAL
* </pre>
*
* then this will cause ORDER BY BATIMENT.ORDRE, LOCAL.ORDRE.
* @param rec if grafts should also be changed.
* @return this.
*/
public synchronized final SQLRowValuesListFetcher setReferentsOrdered(final boolean b, final boolean rec) {
this.descendantsOrdered = b;
if (rec) {
for (final Map<Path, SQLRowValuesListFetcher> m : this.grafts.values()) {
for (final SQLRowValuesListFetcher f : m.values())
f.setReferentsOrdered(b, rec);
}
}
return this;
}
public synchronized final boolean areReferentsOrdered() {
return this.descendantsOrdered;
}
public final void addPostFetchLink(final Path toAdd, final Path existingDestination) {
this.addPostFetchLink(toAdd, existingDestination, false);
}
/**
* Add a link to be added at the end of fetch(). This is needed when the graph to be fetched
* isn't a tree.
*
* @param toAdd the last step of this parameter will be added at the end of {@link #fetch()},
* e.g. /SOURCE/ --[ID_MOST_SERIOUS_OBS]--> /OBSERVATION/.
* @param existingDestination where the destination rows of <code>toAdd</code> are, e.g.
* /SOURCE/ <--[ID_SOURCE]-- /SOURCE_OBSERVATION/ --[ID_OBSERVATION]--> /OBSERVATION/.
* @param ignoreIfMissing what to do if a passed path isn't in this, <code>true</code> to do
* nothing, <code>false</code> to throw an exception.
* @return <code>true</code> if the link was added.
*/
public synchronized final boolean addPostFetchLink(final Path toAdd, final Path existingDestination, final boolean ignoreIfMissing) {
checkFrozen();
if (toAdd.getLast() != existingDestination.getLast())
throw new IllegalArgumentException("Different destination tables");
if (!toAdd.isSingleField())
throw new IllegalArgumentException("Path to add isn't composed of single fields");
final Step lastStep = toAdd.getStep(-1);
if (lastStep.getDirection() != Direction.FOREIGN)
throw new IllegalArgumentException("Last step isn't foreign : " + lastStep);
if (!getFetchers(toAdd).isEmpty())
throw new IllegalArgumentException("Path to add already fetched");
final Path pathToFK = toAdd.minusLast();
final ListMap<Path, SQLRowValuesListFetcher> fkFetchers = getFetchers(pathToFK);
if (fkFetchers.isEmpty()) {
if (ignoreIfMissing)
return false;
else
throw new IllegalArgumentException("Path to add should only have the last step missing");
}
final String lastFieldName = lastStep.getSingleField().getName();
for (final Entry<Path, List<SQLRowValuesListFetcher>> e : fkFetchers.entrySet()) {
final int pathLength = e.getKey().length();
for (final SQLRowValuesListFetcher fkFetcher : e.getValue()) {
if (!fkFetcher.getGraph().followPath(pathToFK.subPath(pathLength)).contains(lastFieldName)) {
if (ignoreIfMissing)
return false;
else
throw new IllegalArgumentException("Foreign key " + lastFieldName + " isn't fetched");
}
}
}
if (getFetchers(existingDestination).isEmpty()) {
if (ignoreIfMissing)
return false;
else
throw new IllegalArgumentException("Destination won't be fetched : " + existingDestination);
}
final Map<Path, Path> copy = new HashMap<>(this.postFetchLinks);
copy.put(toAdd, existingDestination);
this.postFetchLinks = Collections.unmodifiableMap(copy);
return true;
}
public synchronized Map<Path, Path> getPostFetchLinks() {
return this.postFetchLinks;
}
public final SQLRowValuesListFetcher graft(final SQLRowValuesListFetcher other) {
return this.graft(other, Path.get(getGraph().getTable()));
}
public final SQLRowValuesListFetcher graft(final SQLRowValues other, Path graftPath) {
// with referents otherwise it's useless
return this.graft(new SQLRowValuesListFetcher(other, true), graftPath);
}
/**
* Graft a fetcher on this graph.
*
* @param other another instance fetching rows of the table at <code>graftPath</code>.
* @param graftPath a path from this values to where <code>other</code> rows should be grafted.
* @return the previous fetcher.
*/
public synchronized final SQLRowValuesListFetcher graft(final SQLRowValuesListFetcher other, Path graftPath) {
checkFrozen();
if (this == other)
throw new IllegalArgumentException("trying to graft onto itself");
if (other.getGraph().getTable() != graftPath.getLast())
throw new IllegalArgumentException("trying to graft " + other.getGraph().getTable() + " at " + graftPath);
final SQLRowValues graftPlace = this.getGraph().followPath(graftPath);
if (graftPlace == null)
throw new IllegalArgumentException("path doesn't exist: " + graftPath);
assert graftPath.getLast() == graftPlace.getTable();
if (other.getGraph().hasForeigns())
throw new IllegalArgumentException("shouldn't have foreign rows");
final Path descendantPath = other.getReferentPath();
final int descendantPathLength = descendantPath.length();
if (descendantPathLength == 0)
throw new IllegalArgumentException("empty path");
// checked by computePath
assert descendantPath.isSingleField();
// we used to disallow that :
// this is LOCAL* -> BATIMENT -> SITE and CPI -> LOCAL -> BATIMENT* is being grafted
// but this is sometimes desirable, e.g. for each LOCAL find all of its siblings with the
// same capacity (or any other predicate)
// shallow copy : all values are still immutable
final Map<Path, Map<Path, SQLRowValuesListFetcher>> outerMutable = new HashMap<Path, Map<Path, SQLRowValuesListFetcher>>(this.grafts);
final Map<Path, SQLRowValuesListFetcher> innerMutable;
if (!this.grafts.containsKey(graftPath)) {
// allow getFetchers() to use a list, easing tests and avoiding using equals()
innerMutable = new LinkedHashMap<Path, SQLRowValuesListFetcher>(4);
} else {
final Map<Path, SQLRowValuesListFetcher> map = this.grafts.get(graftPath);
innerMutable = new LinkedHashMap<Path, SQLRowValuesListFetcher>(map);
// e.g. fetching *BATIMENT* <- LOCAL and *BATIMENT* <- LOCAL <- CPI (with different
// WHERE) and LOCAL have different fields. This isn't supported since we would have to
// merge fields in merge() and it would be quite long
for (Entry<Path, SQLRowValuesListFetcher> e : map.entrySet()) {
final Path fetcherPath = e.getKey();
final SQLRowValuesListFetcher fetcher = e.getValue();
for (int i = 1; i <= descendantPathLength; i++) {
final Path subPath = descendantPath.subPath(0, i);
if (fetcherPath.startsWith(subPath)) {
if (!fetcher.getGraph().followPath(subPath).getFields().equals(other.getGraph().followPath(subPath).getFields()))
throw new IllegalArgumentException("The same node have different fields in different fetcher\n" + graftPath + "\n" + subPath);
} else {
break;
}
}
}
}
final SQLRowValuesListFetcher res = innerMutable.put(descendantPath, other);
outerMutable.put(graftPath, Collections.unmodifiableMap(innerMutable));
this.grafts = Collections.unmodifiableMap(outerMutable);
return res;
}
public final Collection<SQLRowValuesListFetcher> ungraft() {
return this.ungraft(Path.get(getGraph().getTable()));
}
public synchronized final Collection<SQLRowValuesListFetcher> ungraft(final Path graftPath) {
checkFrozen();
if (!this.grafts.containsKey(graftPath))
return null;
final Map<Path, Map<Path, SQLRowValuesListFetcher>> outerMutable = new HashMap<Path, Map<Path, SQLRowValuesListFetcher>>(this.grafts);
final Map<Path, SQLRowValuesListFetcher> res = outerMutable.remove(graftPath);
this.grafts = Collections.unmodifiableMap(outerMutable);
return res == null ? null : res.values();
}
private synchronized final Map<Path, Map<Path, SQLRowValuesListFetcher>> getGrafts() {
return this.grafts;
}
/**
* The fetchers grafted at the passed path.
*
* @param graftPath where the fetchers are grafted, e.g. MISSION, DOSSIER, SITE.
* @return the grafts by their path to fetch, e.g. SITE, BATIMENT, LOCAL, CPI_BT.
*/
public final Map<Path, SQLRowValuesListFetcher> getGrafts(final Path graftPath) {
return this.getGrafts().get(graftPath);
}
/**
* Get all fetchers.
*
* @param includeSelf <code>true</code> to include <code>this</code> (with a <code>null</code>
* key).
* @return all instances indexed by the graft path.
*/
public final ListMapItf<Path, SQLRowValuesListFetcher> getFetchers(final boolean includeSelf) {
final ListMap<Path, SQLRowValuesListFetcher> res = new ListMap<Path, SQLRowValuesListFetcher>();
for (final Entry<Path, Map<Path, SQLRowValuesListFetcher>> e : this.getGrafts().entrySet()) {
assert e.getKey() != null;
res.putCollection(e.getKey(), e.getValue().values());
}
if (includeSelf)
res.add(null, this);
return ListMap.unmodifiableMap(res);
}
/**
* Get instances which fetch the {@link Path#getLast() last table} of the passed path. E.g.
* useful if you want to add a where to a join. This method is recursively called on
* {@link #getGrafts(Path) grafts} thus the returned paths may be fetched by grafts.
*
* @param fetchedPath a path starting by this table.
* @return all instances indexed by the graft path, i.e. <code>fetchedPath</code> is between
* with it and (it+fetchers.{@link #getReferentPath()}).
*/
public final ListMap<Path, SQLRowValuesListFetcher> getFetchers(final Path fetchedPath) {
final ListMap<Path, SQLRowValuesListFetcher> res = new ListMap<Path, SQLRowValuesListFetcher>();
if (this.getGraph().followPath(fetchedPath) != null)
res.add(Path.get(getGraph().getTable()), this);
// search grafts
for (final Entry<Path, Map<Path, SQLRowValuesListFetcher>> e : this.getGrafts().entrySet()) {
final Path graftPlace = e.getKey();
if (fetchedPath.startsWith(graftPlace) && fetchedPath.length() > graftPlace.length()) {
final Path rest = fetchedPath.subPath(graftPlace.length());
// we want requests that use the last step of fetchedPath
assert rest.length() > 0;
for (final Entry<Path, SQLRowValuesListFetcher> e2 : e.getValue().entrySet()) {
final Path refPath = e2.getKey();
final SQLRowValuesListFetcher graft = e2.getValue();
if (refPath.startsWith(rest)) {
res.add(graftPlace, graft);
} else if (rest.startsWith(refPath)) {
// otherwise rest == refPath and the above if would have been executed
assert rest.length() > refPath.length();
for (final Entry<Path, List<SQLRowValuesListFetcher>> e3 : graft.getFetchers(rest).entrySet()) {
res.addAll(graftPlace.append(e3.getKey()), e3.getValue());
}
}
}
}
}
return res;
}
private final void addFields(final SQLSelect sel, final SQLRowValues vals, final String alias) {
// put key first
final SQLField key = vals.getTable().getKey();
sel.addSelect(new AliasedField(key, alias));
for (final String fieldName : vals.getFields()) {
if (!fieldName.equals(key.getName()))
sel.addSelect(new AliasedField(vals.getTable().getField(fieldName), alias));
}
}
public final SQLSelect getReq() {
return this.getReq(null, null);
}
static private final SQLSelect checkTr(final List<String> origSelect, final SQLSelect tr) {
if (!origSelect.equals(tr.getSelect()))
throw new IllegalArgumentException("Select clause cannot be modified");
return tr;
}
public synchronized final SQLSelect getReq(final Where w, final ITransformer<SQLSelect, SQLSelect> selTransf) {
checkTable(w);
final boolean isNopTransf = selTransf == null || selTransf == Transformer.<SQLSelect> nopTransformer();
if (this.isFrozen()) {
if (w == null && isNopTransf) {
return this.frozen;
} else {
final SQLSelect copy = new SQLSelect(this.frozen);
final SQLSelect res = isNopTransf ? copy : checkTr(copy.getSelect(), selTransf.transformChecked(copy));
return res.andWhere(w);
}
}
final SQLTable t = this.getGraph().getTable();
final SQLSelect sel = new SQLSelect();
if (this.includeForeignUndef) {
sel.setExcludeUndefined(false);
sel.setExcludeUndefined(true, t);
}
walk(null, new ITransformer<State<String>, String>() {
@Override
public String transformChecked(State<String> input) {
final String alias;
if (input.getFrom() != null) {
alias = getAlias(sel, input.getPath());
final String aliasPrev = input.getAcc();
// MAYBE use "INNER" for first step of a referent graft since the first node is
// ignored (the node from the parent graph is used)
// SITE <-- BATIMENT and graft is BATIMENT <-- LOCAL, empty BATIMENT are just
// discarded so best not to fetch them for nothing
final String joinType = isPathRequired(input.getPath()) ? "INNER" : "LEFT";
sel.addJoin(joinType, aliasPrev, input.getPath().getStep(-1), alias);
} else {
alias = null;
}
addFields(sel, input.getCurrent(), alias);
return alias;
}
});
for (final Path p : this.getOrder())
sel.addOrder(sel.followPath(t.getName(), p), false);
// after getOrder() since it can specify more precise order
if (this.areReferentsOrdered()) {
final int descSize = this.descendantPath.length();
for (int i = 1; i <= descSize; i++) {
sel.addOrder(sel.followPath(t.getName(), this.descendantPath.subPath(0, i)), false);
}
}
if (this.getSelID() != null)
sel.andWhere(getIDWhere(this.getSelID()));
final List<String> origSel = new ArrayList<String>(sel.getSelect());
SQLSelect res = sel;
for (final ITransformer<SQLSelect, SQLSelect> tr : this.getSelectTransformers()) {
res = tr.transformChecked(res);
}
if (!isNopTransf)
res = selTransf.transformChecked(res);
return checkTr(origSel, res).andWhere(w);
}
public final Where getIDWhere(final Number id) {
if (id == null)
return null;
return new Where(getGraph().getTable().getKey(), "=", id);
}
static String getAlias(final SQLSelect sel, final Path path) {
String res = "tAlias";
final int stop = path.length();
for (int i = 0; i < stop; i++) {
res += "__" + path.getSingleField(i).getName();
}
// needed for backward, otherwise tableAlias__ID_BATIMENT for LOCAL
res += "__" + path.getTable(stop).getName();
return sel.getUniqueAlias(res);
}
// assure that the graph is explored the same way for the construction of the request
// and the reading of the resultSet
private <S> void walk(final S sel, final ITransformer<State<S>, S> transf) {
// walk through foreign keys and never walk back (use graft())
this.getGraph().getGraph().walk(this.getGraph(), sel, transf, RecursionType.BREADTH_FIRST, Direction.FOREIGN);
// walk starting backwards but allowing forwards
this.getGraph().getGraph().walk(this.getGraph(), sel, new ITransformer<State<S>, S>() {
@Override
public S transformChecked(State<S> input) {
final Path p = input.getPath();
if (p.getStep(0).isForeign())
throw new SQLRowValuesCluster.StopRecurseException().setCompletely(false);
final Step lastStep = p.getStep(p.length() - 1);
// if we go backwards it should be from the start (i.e. we can't go backwards, then
// forwards and backwards again)
if (!lastStep.isForeign() && p.getDirection() != Direction.REFERENT)
throw new SQLRowValuesCluster.StopRecurseException().setCompletely(false);
return transf.transformChecked(input);
}
}, new WalkOptions(Direction.ANY).setRecursionType(RecursionType.BREADTH_FIRST).setStartIncluded(false));
}
// models the graph, so that we don't have to walk it for each row
private static final class GraphNode {
private final SQLTable t;
private final int fieldCount;
private final int foreignCount;
private final int linkIndex;
private final Step from;
private GraphNode(final State<Integer> input) {
super();
this.t = input.getCurrent().getTable();
this.fieldCount = input.getCurrent().size();
this.foreignCount = input.getCurrent().getForeigns().size();
this.linkIndex = input.getAcc();
final int length = input.getPath().length();
this.from = length == 0 ? null : input.getPath().getStep(length - 1);
}
public final SQLTable getTable() {
return this.t;
}
public final int getFieldCount() {
return this.fieldCount;
}
public final int getForeignCount() {
return this.foreignCount;
}
public final int getLinkIndex() {
return this.linkIndex;
}
public final String getFromName() {
return this.from.getSingleField().getName();
}
public final boolean isBackwards() {
return !this.from.isForeign();
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + this.fieldCount;
result = prime * result + ((this.from == null) ? 0 : this.from.hashCode());
result = prime * result + this.linkIndex;
result = prime * result + this.t.hashCode();
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
final GraphNode other = (GraphNode) obj;
return this.fieldCount == other.fieldCount && this.linkIndex == other.linkIndex && this.t.equals(other.t) && CompareUtils.equals(this.from, other.from);
}
@Override
public String toString() {
final String link = this.from == null ? "" : " linked to " + getLinkIndex() + " by " + this.getFromName() + (this.isBackwards() ? " backwards" : " forewards");
return this.getFieldCount() + " fields of " + this.getTable() + link;
}
}
static private final class RSH implements ResultSetHandler {
private final List<String> selectFields;
private final List<GraphNode> graphNodes;
private final boolean freezeRows;
private RSH(List<String> selectFields, List<GraphNode> l, boolean freezeRows) {
this.selectFields = selectFields;
this.graphNodes = l;
this.freezeRows = freezeRows;
}
@Override
public Object handle(final ResultSet rs) throws SQLException {
final List<GraphNode> l = this.graphNodes;
final int graphSize = l.size();
int nextToLink = 0;
final List<Future<?>> futures = new ArrayList<Future<?>>();
final List<SQLRowValues> res = new ArrayList<SQLRowValues>(64);
final List<List<SQLRowValues>> rows = Collections.synchronizedList(new ArrayList<List<SQLRowValues>>(64));
// for each rs row, create all SQLRowValues without linking them together
// if we're multi-threaded, link them in another thread
while (rs.next()) {
int rsIndex = 1;
// MAYBE cancel() futures
if (Thread.currentThread().isInterrupted())
throw new RTInterruptedException("interrupted while fetching");
final List<SQLRowValues> row = new ArrayList<SQLRowValues>(graphSize);
for (int i = 0; i < graphSize; i++) {
final GraphNode node = l.get(i);
final int stop = rsIndex + node.getFieldCount();
final SQLRowValues creatingVals;
// the PK is always first and it can only be null if there was no row, i.e. all
// other fields will be null.
final Object first = rs.getObject(rsIndex);
if (first == null) {
creatingVals = null;
// don't bother reading all nulls
rsIndex = stop;
} else {
// don't pass referent count as it can be fetched by a graft, or else
// several rows might later be merged (e.g. *BATIMENT* <- LOCAL has only one
// referent but all locals of a batiment will point to the same row)
creatingVals = new SQLRowValues(node.getTable(), node.getFieldCount(), node.getForeignCount(), -1);
put(creatingVals, rsIndex, first);
rsIndex++;
}
if (i == 0) {
if (creatingVals == null)
throw new IllegalStateException("Null primary row");
res.add(creatingVals);
}
for (; rsIndex < stop; rsIndex++) {
try {
put(creatingVals, rsIndex, rs.getObject(rsIndex));
} catch (SQLException e) {
throw new IllegalStateException("unable to fill " + creatingVals, e);
}
}
row.add(creatingVals);
}
rows.add(row);
// become multi-threaded only for large values
final int currentCount = rows.size();
if (currentCount % 1000 == 0) {
futures.add(exec.submit(new Linker(l, rows, nextToLink, currentCount, this.freezeRows)));
nextToLink = currentCount;
}
}
final int rowSize = rows.size();
assert nextToLink > 0 == futures.size() > 0;
if (nextToLink > 0)
futures.add(exec.submit(new Linker(l, rows, nextToLink, rowSize, this.freezeRows)));
// either link all rows, or...
if (nextToLink == 0)
link(l, rows, 0, rowSize, this.freezeRows);
else {
// ...wait for every one and most importantly check for any exceptions
try {
for (final Future<?> f : futures)
f.get();
} catch (Exception e) {
throw new IllegalStateException("couldn't link", e);
}
}
return res;
}
protected void put(final SQLRowValues creatingVals, int rsIndex, final Object obj) {
// -1 since rs starts at 1
// field names checked only once when nodes are created
creatingVals.put(this.selectFields.get(rsIndex - 1), obj, false);
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + this.graphNodes.hashCode();
result = prime * result + this.selectFields.hashCode();
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
final RSH other = (RSH) obj;
return this.graphNodes.equals(other.graphNodes) && this.selectFields.equals(other.selectFields);
}
}
/**
* Execute the request transformed by <code>selTransf</code> and return the result as a list of
* SQLRowValues. NOTE: this method doesn't use the cache of SQLDataSource.
*
* @return a list of SQLRowValues, one item per row, each item having the same structure as the
* SQLRowValues passed to the constructor.
*/
public final List<SQLRowValues> fetch() {
return this.fetch(null);
}
private void checkTable(final Where w) throws IllegalArgumentException {
if (w == null)
return;
final SQLTable t = this.getGraph().getTable();
for (final FieldRef f : w.getFields()) {
if (!f.getTableRef().equals(t))
throw new IllegalArgumentException("Not all from the primary table " + t + " : " + w);
}
}
public final SQLRowValues fetchOne(final Number id) {
return this.fetchOne(id, null);
}
public final SQLRowValues fetchOne(final Number id, final Boolean unmodifiableRows) {
if (id == null)
throw new NullPointerException("Null ID");
if (this.getSelID() != null)
throw new IllegalStateException("ID already set to " + getSelID());
final List<SQLRowValues> res = this.fetch(getIDWhere(id), unmodifiableRows);
if (res.size() > 1)
throw new IllegalStateException("More than one row for ID " + id + " : " + res);
return CollectionUtils.getFirst(res);
}
/**
* Execute the request transformed by <code>selTransf</code> and with the passed where (even if
* {@link #isFrozen()}) and return the result as a list of SQLRowValues. NOTE: this method
* doesn't use the cache of SQLDataSource.
*
* @param w a where to {@link SQLSelect#andWhere(Where) restrict} the result, can only uses the
* primary table (others have unspecified aliases), can be <code>null</code>.
* @return a list of SQLRowValues, one item per row, each item having the same structure as the
* SQLRowValues passed to the constructor.
* @throws IllegalArgumentException if the passed where doesn't refer exclusively to the primary
* table.
*/
public final List<SQLRowValues> fetch(final Where w) throws IllegalArgumentException {
return this.fetch(w, null);
}
/**
* Execute the request transformed by <code>selTransf</code> and with the passed where (even if
* {@link #isFrozen()}) and return the result as a list of SQLRowValues. NOTE: this method
* doesn't use the cache of SQLDataSource.
*
* @param w a where to {@link SQLSelect#andWhere(Where) restrict} the result, can only uses the
* primary table (others have unspecified aliases), can be <code>null</code>.
* @param unmodifiableRows whether to return unmodifiable rows, <code>null</code> to use
* {@link #areReturnedRowsUnmodifiable() the default}.
* @return a list of SQLRowValues, one item per row, each item having the same structure as the
* SQLRowValues passed to the constructor.
* @throws IllegalArgumentException if the passed where doesn't refer exclusively to the primary
* table.
*/
public final List<SQLRowValues> fetch(final Where w, final Boolean unmodifiableRows) throws IllegalArgumentException {
return this.fetch(w, null, unmodifiableRows);
}
public final List<SQLRowValues> fetch(final Where w, final ITransformer<SQLSelect, SQLSelect> selTransf, final Boolean unmodifiableRows) throws IllegalArgumentException {
return this.fetch(null, w, selTransf, unmodifiableRows);
}
// same object passed to all recursive calls
static private final class MainResult {
private final Deque<GraftState> graftStates = new LinkedList<>();
private MainResult() {
super();
}
private GraftState getLastGraftState() {
return this.graftStates.peekLast();
}
private void push(final List<SQLRowValues> merged, final Path graftPlace) {
final GraftState graftState = this.getLastGraftState();
final Path recPath = graftState == null ? null : graftState.pathFromMain;
final GraftState recGraftState = new GraftState(merged, recPath, graftPlace);
this.graftStates.addLast(recGraftState);
}
private void pop() {
this.graftStates.removeLast();
}
}
static private final class GraftState {
private final Path pathFromMain;
// list of BATIMENT to only fetch what's necessary
private final Set<Number> ids = new HashSet<Number>();
// CollectionMap since the same row can be in multiple index of merged, e.g. when
// fetching *BATIMENT* -> SITE each site will be repeated as many times as it has
// children and if we want their DOSSIER they must be grafted on each line.
private final ListMap<Tuple2<Path, Number>, SQLRowValues> byRows = createCollectionMap();
private GraftState(final List<SQLRowValues> parentRows, final Path pathFromMain, final Path graftPlace) {
this.pathFromMain = pathFromMain == null ? graftPlace : pathFromMain.append(graftPlace);
final Path mapPath = Path.get(graftPlace.getLast());
for (final SQLRowValues vals : parentRows) {
// can be empty when grafting on optional row
for (final SQLRowValues graftPlaceVals : vals.followPath(graftPlace, CreateMode.CREATE_NONE, false)) {
this.ids.add(graftPlaceVals.getIDNumber());
this.byRows.add(Tuple2.create(mapPath, graftPlaceVals.getIDNumber()), graftPlaceVals);
}
}
assert this.ids.size() == this.byRows.size();
}
private Where createWhere() {
return new Where(this.pathFromMain.getLast().getKey(), this.ids);
}
}
private final List<SQLRowValues> fetch(MainResult mainRes, Where w, final ITransformer<SQLSelect, SQLSelect> selTransf, final Boolean unmodifiableRows) throws IllegalArgumentException {
final GraftState graftState = mainRes == null ? null : mainRes.getLastGraftState();
if (graftState != null) {
final Where graftWhere = graftState.createWhere();
if (graftWhere.equals(Where.FALSE))
return Collections.emptyList();
w = Where.and(w, graftWhere);
}
final SQLSelect req;
final Map<Path, Map<Path, SQLRowValuesListFetcher>> grafts;
final Map<Path, Path> postLinks;
final boolean freezeRows;
// the only other internal state used is this.descendantPath which is final immutable
synchronized (this) {
req = this.getReq(w, selTransf);
grafts = this.getGrafts();
postLinks = this.postFetchLinks;
freezeRows = unmodifiableRows == null ? this.areReturnedRowsUnmodifiable() : unmodifiableRows.booleanValue();
}
// getName() would take 5% of ResultSetHandler.handle()
final List<FieldRef> selectFields = req.getSelectFields();
final int selectFieldsSize = selectFields.size();
final List<String> selectFieldsNames = req.getSelectNames();
final SQLTable table = getGraph().getTable();
// create a flat list of the graph nodes, we just need the table, field count and the index
// in this list of its linked table, eg for CPI -> LOCAL -> BATIMENT -> SITE :
// <LOCAL,2,0>, <BATIMENT,2,0>, <SITE,5,1>, <CPI,4,0>
final int graphSize = this.getGraph().getGraph().size();
final List<GraphNode> l = new ArrayList<GraphNode>(graphSize);
// check field names only once since each row has the same fields
final AtomicInteger fieldIndex = new AtomicInteger(0);
walk(0, new ITransformer<State<Integer>, Integer>() {
@Override
public Integer transformChecked(State<Integer> input) {
final int index = l.size();
final GraphNode node = new GraphNode(input);
final int stop = fieldIndex.get() + node.getFieldCount();
for (int i = fieldIndex.get(); i < stop; i++) {
if (i >= selectFieldsSize)
throw new IllegalStateException("Fields were removed from the select");
final FieldRef field = selectFields.get(i);
if (!node.getTable().equals(field.getTableRef().getTable()))
throw new IllegalStateException("Select field not in " + node + " : " + field);
}
fieldIndex.set(stop);
l.add(node);
// used by link index of GraphNode
return index;
}
});
// otherwise walk() would already have thrown an exception
assert fieldIndex.get() <= selectFieldsSize;
if (fieldIndex.get() != selectFieldsSize) {
throw new IllegalStateException("Items have been added to the select (which is useless, since only fields specified by rows are returned and WHERE cannot access SELECT columns) : "
+ selectFields.subList(fieldIndex.get(), selectFieldsSize));
}
assert l.size() == graphSize : "All nodes weren't explored once : " + l.size() + " != " + graphSize + "\n" + this.getGraph().printGraph();
final boolean mergeReferents = this.fetchReferents();
final boolean mergeGrafts = grafts.size() > 0;
final boolean addPostLinks = !postLinks.isEmpty();
// if it is possible let the handler do the freeze, avoid another loop and further is
// multi-threaded
final boolean handlerCanFreeze = !mergeReferents && !mergeGrafts && !addPostLinks;
// if we wanted to use the cache, we'd need to copy the returned list and its items (i.e.
// deepCopy()), since we modify them afterwards. Or perhaps include the code after this line
// into the result set handler.
final IResultSetHandler rsh = new IResultSetHandler(new RSH(selectFieldsNames, l, freezeRows && handlerCanFreeze), false);
@SuppressWarnings("unchecked")
final List<SQLRowValues> res = (List<SQLRowValues>) table.getBase().getDataSource().execute(req.asString(), rsh, false);
// e.g. list of batiment pointing to site
final List<SQLRowValues> merged;
if (!mergeReferents) {
merged = res;
} else if (graftState == null) {
merged = merge(res);
} else {
// merge before recursive call, so it can access the main graph
merged = mergeGraft(res, graftState.byRows);
}
if (mergeGrafts) {
if (mainRes == null) {
mainRes = new MainResult();
}
for (final Entry<Path, Map<Path, SQLRowValuesListFetcher>> graftPlaceEntry : grafts.entrySet()) {
// e.g. BATIMENT
final Path graftPlace = graftPlaceEntry.getKey();
// common to all grafts to support CPI -> LOCAL -> BATIMENT and RECEPTEUR
// -> LOCAL -> BATIMENT (ie avoid duplicate LOCAL)
mainRes.push(merged, graftPlace);
for (final Entry<Path, SQLRowValuesListFetcher> e : graftPlaceEntry.getValue().entrySet()) {
// e.g BATIMENT <- LOCAL <- CPI
final Path descendantPath = e.getKey();
assert descendantPath.getFirst() == graftPlace.getLast() : descendantPath + " != " + graftPlace;
final SQLRowValuesListFetcher graft = e.getValue();
graft.fetch(mainRes, null, null, false);
}
mainRes.pop();
}
}
if (addPostLinks) {
// group by destinationPath to index only once for all links to add
// SetMap<Tuple2<commonPath, destinationPath>, toAddPath>
final SetMap<Tuple2<Path, Path>, Path> byCommonPath = new SetMap<>();
for (final Entry<Path, Path> e : postLinks.entrySet()) {
final Path toAdd = e.getKey();
final Path existingPath = e.getValue();
final Path commonPath = toAdd.getCommonPath(existingPath);
byCommonPath.add(Tuple2.create(commonPath, existingPath.subPath(commonPath.length())), toAdd.subPath(commonPath.length()));
}
/**
* <pre>
* LOCAL <-- SRC <-- JOIN --> OBSERVATION
* \--ID_OLDEST_OBS--/
* \--ID_MOST_SERIOUS_OBS--/
* </pre>
*/
for (final Entry<Tuple2<Path, Path>, Set<Path>> e : byCommonPath.entrySet()) {
// LOCAL <-- SRC
final Path commonPath = e.getKey().get0();
// SRC <-- JOIN --> OBSERVATION
final Path throughTargetRows = e.getKey().get1();
// SRC -- ID_OLDEST_OBS --> OBSERVATION
// SRC -- ID_MOST_SERIOUS_OBS --> OBSERVATION
final Set<Path> pathsToAdd = e.getValue();
for (final SQLRowValues v : merged) {
for (final SQLRowValues commonRow : v.getDistantRows(commonPath)) {
// index target rows
final Map<Number, SQLRowValues> byIDs = new HashMap<>();
for (final SQLRowValues target : commonRow.getDistantRows(throughTargetRows)) {
byIDs.put(target.getIDNumber(), target);
}
// add links
for (final Path toAdd : pathsToAdd) {
final String fkName = toAdd.getStep(-1).getSingleField().getName();
// SRC
final Path throughRowToUpdate = toAdd.minusLast();
for (final SQLRowValues toUpdate : commonRow.getDistantRows(throughRowToUpdate)) {
final Number foreignID = toUpdate.getNonEmptyForeignIDNumber(fkName);
if (foreignID != null) {
final SQLRowValues target = byIDs.get(foreignID);
if (target == null)
throw new IllegalStateException("Missing row for " + foreignID + " at " + throughTargetRows);
toUpdate.put(fkName, target);
}
}
}
}
}
}
}
if (freezeRows && !handlerCanFreeze) {
for (final SQLRowValues r : merged) {
r.getGraph().freeze();
}
}
return merged;
}
// no need to set keep-alive too low, since on finalize() the pool shutdowns itself
private static final ExecutorService exec = new ThreadPoolExecutor(0, 2, 10L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
private static final class Linker implements Callable<Object> {
private final List<GraphNode> l;
private final List<List<SQLRowValues>> rows;
private final int fromIndex;
private final int toIndex;
private final boolean freezeRows;
public Linker(final List<GraphNode> l, final List<List<SQLRowValues>> rows, final int first, final int last, final boolean freezeRows) {
super();
this.l = l;
this.rows = rows;
this.fromIndex = first;
this.toIndex = last;
this.freezeRows = freezeRows;
}
@Override
public Object call() throws Exception {
link(this.l, this.rows, this.fromIndex, this.toIndex, this.freezeRows);
return null;
}
}
private static void link(final List<GraphNode> l, final List<List<SQLRowValues>> rows, final int start, final int stop, final boolean freezeRows) {
final int graphSize = l.size();
for (int nodeIndex = 1; nodeIndex < graphSize; nodeIndex++) {
final GraphNode node = l.get(nodeIndex);
final String fromName = node.getFromName();
final int linkIndex = node.getLinkIndex();
final boolean backwards = node.isBackwards();
// freeze after the last put()
final boolean freeze = freezeRows && nodeIndex == graphSize - 1;
for (int i = start; i < stop; i++) {
final List<SQLRowValues> row = rows.get(i);
final SQLRowValues creatingVals = row.get(nodeIndex);
// don't link empty values (LEFT JOIN produces rowValues filled with
// nulls) to the graph
if (creatingVals != null) {
final SQLRowValues valsToFill;
final SQLRowValues valsToPut;
if (backwards) {
valsToFill = creatingVals;
valsToPut = row.get(linkIndex);
} else {
valsToFill = row.get(linkIndex);
valsToPut = creatingVals;
}
// check is done by updateLinks()
valsToFill.put(fromName, valsToPut, false);
}
// can't use creatingVals, use primary row which is never null
if (freeze)
row.get(0).getGraph().freeze();
}
}
if (freezeRows && graphSize == 1) {
for (int i = start; i < stop; i++) {
final List<SQLRowValues> row = rows.get(i);
final boolean justFrozen = row.get(0).getGraph().freeze();
assert justFrozen : "Already frozen";
}
}
}
/**
* Merge a list of fetched rowValues so that remove any duplicated rowValues. Eg, transforms
* this :
*
* <pre>
* BATIMENT[2] LOCAL[2] CPI_BT[3]
* BATIMENT[2] LOCAL[2] CPI_BT[2]
* BATIMENT[2] LOCAL[3]
* BATIMENT[2] LOCAL[5] CPI_BT[5]
* BATIMENT[3] LOCAL[4] CPI_BT[4]
* BATIMENT[4]
* </pre>
*
* into this :
*
* <pre>
* BATIMENT[2] LOCAL[2] CPI_BT[3]
* CPI_BT[2]
* LOCAL[3]
* LOCAL[5] CPI_BT[5]
* BATIMENT[3] LOCAL[4] CPI_BT[4]
* BATIMENT[4]
* </pre>
*
* @param l a list of fetched rowValues.
* @return a smaller list in which all rowValues are unique.
*/
private final List<SQLRowValues> merge(final List<SQLRowValues> l) {
return merge(l, l, null, this.descendantPath);
}
private final List<SQLRowValues> mergeGraft(final List<SQLRowValues> l, final ListMap<Tuple2<Path, Number>, SQLRowValues> graftPlaceRows) {
if (graftPlaceRows == null)
throw new IllegalArgumentException("Missing map");
return merge(null, l, graftPlaceRows, this.descendantPath);
}
/**
* Merge a list of rowValues and optionally graft it onto another one.
*
* @param tree the list receiving the graft.
* @param graft the list being merged and optionally grafted on <code>tree</code>, can be the
* same as <code>tree</code>.
* @param graftPlaceRows if this is a graft the destination rowValues, otherwise
* <code>null</code>, this instance will be modified.
* @param descendantPath the path to merge.
* @return the merged list of main values, or the graft places if it's a graft.
*/
static private final List<SQLRowValues> merge(final List<SQLRowValues> tree, final List<SQLRowValues> graft, final ListMap<Tuple2<Path, Number>, SQLRowValues> graftPlaceRows,
Path descendantPath) {
final boolean isGraft = graftPlaceRows != null;
assert (tree != graft) == isGraft : "Trying to graft onto itself";
final Collection<SQLRowValues> res = isGraft ? new LinkedIdentitySet<SQLRowValues>() : new ArrayList<SQLRowValues>();
// so that every graft is actually grafted onto the tree
final ListMap<Tuple2<Path, Number>, SQLRowValues> map = isGraft ? graftPlaceRows : createCollectionMap();
final int stop = descendantPath.length();
for (final SQLRowValues v : graft) {
boolean doAdd = true;
SQLRowValues previous = null;
for (int i = stop; i >= 0 && doAdd; i--) {
final Path subPath = descendantPath.subPath(0, i);
final SQLRowValues desc = v.followPath(subPath);
if (desc != null) {
final Tuple2<Path, Number> row = Tuple2.create(subPath, desc.getIDNumber());
if (map.containsKey(row)) {
doAdd = false;
assert map.get(row).get(0).getFields().containsAll(desc.getFields()) : "Discarding an SQLRowValues with more fields : " + desc;
// previous being null can happen when 2 grafted paths share some steps at
// the start, e.g. SOURCE -> LOCAL and CPI -> LOCAL with a LOCAL having a
// SOURCE but no CPI
if (previous != null) {
final List<SQLRowValues> destinationRows = map.get(row);
final int destinationSize = destinationRows.size();
assert destinationSize > 0 : "Map contains row but have no corresponding value: " + row;
final String ffName = descendantPath.getSingleField(i).getName();
// avoid the first deepCopy() (needed since rows of 'previous' have
// already been added to 'map') and copy before merging
for (int j = 1; j < destinationSize; j++) {
final SQLRowValues previousCopy = previous.deepCopy().put(ffName, destinationRows.get(j));
// put the copied rowValues into 'map' otherwise they'd be
// unreachable and thus couldn't have referents. Tested by
// SQLRowValuesListFetcherTest.testSameReferentMergedMultipleTimes()
// i+1 since we start from 'previous' not 'desc'
for (int k = stop; k >= i + 1; k--) {
final SQLRowValues descCopy = previousCopy.followPath(descendantPath.subPath(i + 1, k));
if (descCopy != null) {
final Tuple2<Path, Number> rowCopy = Tuple2.create(descendantPath.subPath(0, k), descCopy.getIDNumber());
assert map.containsKey(rowCopy) : "Since we already iterated with i";
map.add(rowCopy, descCopy);
}
}
}
// don't call map.put() it has already been handled below
previous.put(ffName, destinationRows.get(0));
if (isGraft) {
final Path pathToGraftPlace = subPath.reverse();
for (final SQLRowValues r : destinationRows) {
final SQLRowValues graftPlaceRow = r.followPath(pathToGraftPlace);
if (graftPlaceRow == null)
throw new IllegalStateException("Row at graft place not found");
res.add(graftPlaceRow);
}
}
}
} else {
map.add(row, desc);
}
previous = desc;
}
}
if (doAdd) {
assert !isGraft : "Adding graft values as tree values";
res.add(v);
}
}
return res instanceof List ? (List<SQLRowValues>) res : new ArrayList<>(res);
}
@Override
public String toString() {
return this.getClass().getSimpleName() + " for " + this.getGraph() + " with " + this.getSelID() + " and " + this.getSelectTransformers();
}
@Override
public boolean equals(Object obj) {
if (obj instanceof SQLRowValuesListFetcher) {
final SQLRowValuesListFetcher o = (SQLRowValuesListFetcher) obj;
final SQLSelect thisReq, oReq;
final Map<Path, Map<Path, SQLRowValuesListFetcher>> thisGrafts, oGrafts;
synchronized (this) {
thisReq = this.getReq();
thisGrafts = this.getGrafts();
}
synchronized (o) {
oReq = o.getReq();
oGrafts = o.getGrafts();
}
// use getReq() to avoid selTransf equality pb (ie we generally use anonymous classes
// which thus lack equals())
return thisReq.equals(oReq) && CompareUtils.equals(this.descendantPath, o.descendantPath) && thisGrafts.equals(oGrafts);
} else
return false;
}
@Override
public int hashCode() {
return this.getReq().hashCode();
}
}