OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

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

/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 * 
 * Copyright 2011 OpenConcerto, by ILM Informatique. All rights reserved.
 * 
 * The contents of this file are subject to the terms of the GNU General Public License Version 3
 * only ("GPL"). You may not use this file except in compliance with the License. You can obtain a
 * copy of the License at http://www.gnu.org/licenses/gpl-3.0.html See the License for the specific
 * language governing permissions and limitations under the License.
 * 
 * When distributing the software, include this License Header Notice in each file.
 */
 
 package org.openconcerto.sql.element;

import org.openconcerto.sql.Configuration;
import org.openconcerto.sql.Log;
import org.openconcerto.sql.TM;
import org.openconcerto.sql.element.SQLElement.PrivateMode;
import org.openconcerto.sql.element.SQLElement.ReferenceAction;
import org.openconcerto.sql.element.SQLElementLink.LinkType;
import org.openconcerto.sql.model.FieldRef;
import org.openconcerto.sql.model.SQLField;
import org.openconcerto.sql.model.SQLRow;
import org.openconcerto.sql.model.SQLRowAccessor;
import org.openconcerto.sql.model.SQLRowValues;
import org.openconcerto.sql.model.SQLRowValues.CreateMode;
import org.openconcerto.sql.model.SQLRowValuesCluster;
import org.openconcerto.sql.model.SQLRowValuesListFetcher;
import org.openconcerto.sql.model.SQLSelect;
import org.openconcerto.sql.model.SQLSelect.LockStrength;
import org.openconcerto.sql.model.SQLTable;
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.request.SQLFieldTranslator;
import org.openconcerto.utils.CollectionMap2.Mode;
import org.openconcerto.utils.CollectionMap2Itf.ListMapItf;
import org.openconcerto.utils.CollectionUtils;
import org.openconcerto.utils.CompareUtils;
import org.openconcerto.utils.ListMap;
import org.openconcerto.utils.Tuple3;
import org.openconcerto.utils.cc.CustomEquals;
import org.openconcerto.utils.cc.CustomEquals.ProxyItf;
import org.openconcerto.utils.cc.HashingStrategy;
import org.openconcerto.utils.cc.ITransformer;

import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;

/**
 * Cache several trees of rows (a row and its descendants).
 * 
 * @author Sylvain
 */
public final class TreesOfSQLRows {

    public static final TreesOfSQLRows createFromIDs(final SQLElement elem, final Collection<? extends Number> ids) {
        final List<SQLRow> rows = new ArrayList<SQLRow>(ids.size());
        for (final Number id : ids) {
            // don't access the DB here, expand() will do it once for all rows
            rows.add(new SQLRow(elem.getTable(), id.intValue()));
        }
        return new TreesOfSQLRows(elem, rows);
    }

    private static String createRestrictDesc(SQLElement refElem, SQLRowAccessor refVals, SQLElementLink elemLink) {
        final String rowDesc = refElem != null ? refElem.getDescription(refVals.asRow()) : refVals.asRow().toString();
        final String fieldS = getLabel(elemLink, null);
        // la tâche du 26/05 ne peut perdre son champ UTILISATEUR
        return TM.getTM().trM("sqlElement.linkCantBeCut", CollectionUtils.createMap("row", refVals.asRow(), "rowDesc", rowDesc, "fieldLabel", fieldS));
    }

    private static String getLabel(SQLElementLink elemLink, Path p) {
        final SQLFieldTranslator translator = Configuration.getInstance().getTranslator();
        final SQLTable table;
        final String itemName;
        if (elemLink != null) {
            assert p == null || elemLink.getPath().equals(p);
            table = elemLink.getPath().getFirst();
            itemName = elemLink.getName();
        } else {
            assert p.length() == 1 : "Joins should have an Element : " + p;
            assert p.getDirection() == Direction.FOREIGN;
            final SQLField singleField = p.getStep(0).getSingleField();
            table = singleField.getTable();
            itemName = singleField.getName();
        }
        final String fieldLabel = translator.getDescFor(table, itemName).getLabel();
        return fieldLabel != null ? fieldLabel : itemName;
    }

    private final SQLElement elem;
    private final Set<SQLRow> originalRoots;
    private Map<SQLRow, SQLRowValues> trees;
    private Set<SQLRow> mainRows;
    private Map<SQLRow, SQLRowValues> allRows;
    private LinksToCut externReferences;

    public TreesOfSQLRows(final SQLElement elem, SQLRow row) {
        this(elem, Collections.singleton(row));
    }

    public TreesOfSQLRows(final SQLElement elem, final Collection<? extends SQLRowAccessor> rows) {
        super();
        this.elem = elem;
        this.originalRoots = new HashSet<SQLRow>();
        this.trees = null;
        // check each row and remove duplicates (i.e. this.originalRoots might be smaller than rows)
        for (final SQLRowAccessor r : rows) {
            this.elem.check(r);
            this.originalRoots.add(r.asRow());
        }
        this.externReferences = null;
    }

    public final SQLElement getElem() {
        return this.elem;
    }

    /**
     * The unique rows that were passed to the constructor. NOTE : the rows that do not exist or are
     * archived won't be in {@link #getTrees()}.
     * 
     * @return a set of rows.
     */
    public final Set<SQLRow> getRows() {
        return this.originalRoots;
    }

    public final Set<SQLRow> getMainRows() {
        return this.mainRows;
    }

    public final Set<SQLRow> getAllRows() {
        return this.allRows.keySet();
    }

    /**
     * Whether the passed trees are contained in this. NOTE : only mains rows are considered, not
     * private ones.
     * 
     * @param o other trees.
     * @return <code>true</code> if all {@link #getMainRows() main rows} of <code>o</code> are in
     *         this and all {@link #getExternReferences() links to cut} as well.
     */
    public final boolean containsAll(final TreesOfSQLRows o) {
        if (this == o)
            return true;
        return this.getMainRows().containsAll(o.getMainRows()) && this.getExternReferences().containsAll(o.getExternReferences());
    }

    public final boolean isFetched() {
        return this.externReferences != null;
    }

    private final void checkFetched() {
        if (!this.isFetched())
            throw new IllegalStateException("Not yet fetched");
    }

    /**
     * Fetch the rows.
     * 
     * @param ls how to lock rows.
     * @return the rows to archive indexed by their roots (a subset of {@link #getRows()}).
     * @throws SQLException if an error occurs.
     */
    public final Map<SQLRow, SQLRowValues> fetch(final LockStrength ls) throws SQLException {
        if (this.isFetched())
            throw new IllegalStateException("Already fetched");

        final Tuple3<Map<SQLRow, SQLRowValues>, Rows, LinksToCut> expand = this.expand(ls);
        this.trees = Collections.unmodifiableMap(expand.get0());
        this.mainRows = Collections.unmodifiableSet(expand.get1().mainRows);
        this.allRows = Collections.unmodifiableMap(expand.get1().vals);
        this.externReferences = expand.get2();

        if (hasFetchedLess())
            Log.get().fine("Some rows are missing : " + this.trees.keySet() + "\n" + this.getRows());
        return this.getTrees();
    }

    public final boolean hasFetchedLess() {
        final Set<SQLRow> rowsFetched = this.trees.keySet();
        assert this.getRows().containsAll(rowsFetched);
        // archived or deleted (or never existed)
        return !rowsFetched.equals(this.getRows());
    }

    /**
     * The trees of rows to archive.
     * 
     * @return the rows to archive indexed by their roots (a subset of {@link #getRows()}).
     */
    public final Map<SQLRow, SQLRowValues> getTrees() {
        checkFetched();
        return this.trees;
    }

    public final Set<SQLRowValuesCluster> getClusters() {
        final Set<SQLRowValuesCluster> res = Collections.newSetFromMap(new IdentityHashMap<SQLRowValuesCluster, Boolean>());
        for (final SQLRowValues r : this.getTrees().values()) {
            // trees can be linked together
            res.add(r.getGraph());
        }
        return res;
    }

    // the root rows linked to their privates and descendant, all visited main rows, the rows
    // pointing to visited rows
    private final Tuple3<Map<SQLRow, SQLRowValues>, Rows, LinksToCut> expand(final LockStrength ls) throws SQLException {
        // root rows (linked to their privates if any) indexed by ID
        final Map<Integer, SQLRowValues> valsMap = new HashMap<Integer, SQLRowValues>();
        final Rows hasBeen = new Rows();
        final LinksToCutMutable toCut = new LinksToCutMutable();
        final Map<SQLRow, SQLRowValues> res = new HashMap<SQLRow, SQLRowValues>();

        // fetch privates of root rows
        final SQLRowValues privateGraph = this.getElem().createGraph(ArchivedGraph.ARCHIVE_AND_FOREIGNS, PrivateMode.ALL_PRIVATES, true);
        final NextRows privates = new NextRows(hasBeen, toCut);
        // always fetch to have up to date values, and behave the same way whether the element has
        // privates or not
        final Set<Number> ids = new HashSet<Number>();
        for (final SQLRow r : this.getRows()) {
            ids.add(r.getIDNumber());
        }
        final SQLRowValuesListFetcher fetcher = SQLRowValuesListFetcher.create(privateGraph);
        fetcher.setSelTransf(new ITransformer<SQLSelect, SQLSelect>() {
            @Override
            public SQLSelect transformChecked(SQLSelect input) {
                input.setLockStrength(ls);
                input.addLockedTable(privateGraph.getTable().getName());
                return input.andWhere(new Where(privateGraph.getTable().getKey(), ids));
            }
        });
        for (final SQLRowValues newVals : fetcher.fetch()) {
            final SQLRow r = newVals.asRow();
            valsMap.put(newVals.getID(), newVals);
            privates.collect(newVals);
            res.put(r, newVals);
        }

        privates.expand(ls);
        return Tuple3.create(res, hasBeen, new LinksToCut(toCut.getMap()));
    }

    // NOTE using a collection of vals changed the time it took to archive a site (736 01) from 225s
    // to 5s.
    /**
     * Expand the passed values by going through referent keys.
     * 
     * @param t the table, eg /LOCAL/.
     * @param valsMap the values to expand, eg {3=>LOCAL(3)->BAT(4), 12=>LOCAL(12)->BAT(4)}.
     * @param hasBeen the rows already expanded, eg {BAT[4], LOCAL[3], LOCAL[12]}.
     * @param toCut the links to cut, eg {|BAT.ID_PRECEDENT|=> [BAT[2]]}.
     * @param ignorePrivateParentRF <code>true</code> if {@link LinkType#COMPOSITION private links}
     *        to <code>t</code> should be ignored.
     * @param ls how to lock rows.
     * @throws SQLException if a link is {@link ReferenceAction#RESTRICT}.
     */
    private final void expand(final SQLTable t, final Map<Integer, SQLRowValues> valsMap, final Rows hasBeen, final LinksToCutMutable toCut, final boolean ignorePrivateParentRF, final LockStrength ls)
            throws SQLException {
        if (valsMap.size() == 0)
            return;

        final SQLElement elem = getElem().getElement(t);

        final Set<Link> ownedLinks = new HashSet<Link>();
        for (final SQLElementLink elemLink : elem.getOwnedLinks().getByPath().values()) {
            if (elemLink.isJoin())
                ownedLinks.add(elemLink.getPath().getStep(0).getSingleLink());
        }
        final Map<Link, SQLElementLink> links = new HashMap<Link, SQLElementLink>();
        for (final SQLElementLink elemLink : elem.getLinksOwnedByOthers().getByPath().values()) {
            links.put(elemLink.getPath().getStep(-1).getSingleLink(), elemLink);
        }

        final NextRows privates = new NextRows(hasBeen, toCut);
        for (final Link link : t.getDBSystemRoot().getGraph().getReferentLinks(t)) {
            // all owned links are fetched alongside the main row
            if (ownedLinks.contains(link))
                continue;
            final SQLElementLink elemLink = links.get(link);
            if (elemLink == null)
                throw new IllegalStateException("Referent link " + link + " missing from " + links);
            final Path elemPath = elemLink.getPath();
            assert elemLink.getOwned() == elem;
            assert elemPath.getLast() == t;
            final Link foreignLink = elemPath.getStep(-1).getSingleLink();

            if (ignorePrivateParentRF && elemLink.getLinkType().equals(LinkType.COMPOSITION)) {
                // if we did fetch the referents rows, they would be contained in hasBeen
                continue;
            }
            // eg "ID_LOCAL"
            final String ffName = foreignLink.getSingleField().getName();
            final SQLElement refElem = elemLink.getOwner();
            final Path pathToTableWithFK = elemPath.minusLast();
            final ReferenceAction action = elemLink.getAction();
            if (action == null) {
                throw new IllegalStateException("Null action for " + refElem + " " + elemPath);
            }
            final SQLRowValues graphToFetch;
            if (action == ReferenceAction.CASCADE) {
                // otherwise we would need to find and expand the parent rows of referents
                if (refElem.isPrivate())
                    throw new UnsupportedOperationException("Cannot cascade to private element " + refElem + " from " + elemPath);
                graphToFetch = refElem.createGraph(ArchivedGraph.ARCHIVE_AND_FOREIGNS, PrivateMode.ALL_PRIVATES, true);
            } else {
                graphToFetch = new SQLRowValues(pathToTableWithFK.getFirst());
            }
            // add the foreign fields pointing to the rows to expand
            graphToFetch.assurePath(pathToTableWithFK).putNulls(foreignLink.getCols());
            final SQLRowValuesListFetcher fetcher = SQLRowValuesListFetcher.create(graphToFetch);
            fetcher.setSelTransf(new ITransformer<SQLSelect, SQLSelect>() {
                @Override
                public SQLSelect transformChecked(SQLSelect input) {
                    input.setLockStrength(ls);
                    input.addLockedTable(graphToFetch.getTable().getName());
                    return input;
                }
            });
            final ListMap<Path, SQLRowValuesListFetcher> fetchers = fetcher.getFetchers(pathToTableWithFK);
            if (fetchers.allValues().size() != 1)
                throw new IllegalStateException("Fetcher which references " + t + " not found : " + fetchers);
            fetchers.allValues().iterator().next().appendSelTransf(new ITransformer<SQLSelect, SQLSelect>() {
                @Override
                public SQLSelect transformChecked(SQLSelect input) {
                    final FieldRef refField = input.getAlias(pathToTableWithFK.getLast()).getField(ffName);
                    // eg where RECEPTEUR.ID_LOCAL in (3,12)
                    return input.andWhere(new Where(refField, valsMap.keySet()));
                }
            });
            for (final SQLRowValues newVals : fetcher.fetch()) {
                final SQLRow r = newVals.asRow();
                final boolean already = hasBeen.contains(r);
                // a row might reference the same linked row multiple times
                final Collection<SQLRowValues> rowsWithFK = newVals.followPath(pathToTableWithFK, CreateMode.CREATE_NONE, false);
                switch (action) {
                case RESTRICT:
                    throw new SQLException(createRestrictDesc(refElem, newVals, elemLink));
                case CASCADE:
                    if (!already) {
                        // walk before linking to existing graph
                        privates.collect(newVals);
                        // link with existing graph, eg RECEPTEUR(235)->LOCAL(3)
                        for (final SQLRowValues rowWithFK : rowsWithFK) {
                            rowWithFK.putForeign(foreignLink, hasBeen.getValues(rowWithFK.getForeign(foreignLink).asRow()));
                        }
                    }
                    break;
                case SET_EMPTY:
                    // if the row should be archived no need to cut any of its links
                    if (!already)
                        toCut.add(elemLink, rowsWithFK);
                    break;
                }
                // if already expanded just link and do not add to next
                if (already) {
                    // now link the new join row or the existing row
                    for (final SQLRowValues joinRow : rowsWithFK) {
                        final boolean linked = hasBeen.tryToLink(joinRow, foreignLink);
                        if (!linked)
                            throw new IllegalStateException("Join row not found : " + joinRow);
                    }
                }
            }
        }
        privates.expand(ls);
    }

    private final class NextRows {
        private final Rows hasBeen;
        private final LinksToCutMutable toCut;
        private final Map<SQLTable, Map<Integer, SQLRowValues>> mainRows;
        // only contains private rows (no main or join rows)
        private final Map<SQLTable, Map<Integer, SQLRowValues>> privateRows;

        public NextRows(final Rows hasBeen, final LinksToCutMutable toCut) {
            this.hasBeen = hasBeen;
            this.toCut = toCut;
            this.mainRows = new HashMap<SQLTable, Map<Integer, SQLRowValues>>();
            this.privateRows = new HashMap<SQLTable, Map<Integer, SQLRowValues>>();
        }

        /**
         * Record all private rows of the passed graph.
         * 
         * @param mainRow main row linked to its private graph. NOTE : the main row can itself be a
         *        private.
         */
        private void collect(final SQLRowValues mainRow) {
            for (final SQLRowValues privateVals : mainRow.getGraph().getItems()) {
                // since newVals isn't in, its privates can't
                assert !this.hasBeen.contains(privateVals.asRow());
                final SQLElement rowElem = getElem().getElement(privateVals.getTable());
                final Map<SQLTable, Map<Integer, SQLRowValues>> m;
                final boolean isMainRow = privateVals == mainRow;
                if (isMainRow) {
                    assert !(rowElem instanceof JoinSQLElement);
                    m = this.mainRows;
                } else {
                    if (rowElem.isPrivate()) {
                        m = this.privateRows;
                    } else {
                        assert rowElem instanceof JoinSQLElement;
                        m = null;
                    }
                }
                this.hasBeen.put(privateVals.asRow(), privateVals, isMainRow);
                if (m != null) {
                    Map<Integer, SQLRowValues> map = m.get(privateVals.getTable());
                    if (map == null) {
                        map = new HashMap<Integer, SQLRowValues>();
                        m.put(privateVals.getTable(), map);
                    }
                    map.put(privateVals.getID(), privateVals);
                }
            }
        }

        private void expand(final LockStrength ls) throws SQLException {
            this.expand(this.mainRows, false, ls);
            this.expand(this.privateRows, true, ls);
            // if the row has been added to the graph (by another link) no need to cut any of its
            // links
            this.toCut.restoreLinks(this.hasBeen);
        }

        private void expand(final Map<SQLTable, Map<Integer, SQLRowValues>> m, final boolean privateRows, final LockStrength ls) throws SQLException {
            for (final Entry<SQLTable, Map<Integer, SQLRowValues>> e : m.entrySet()) {
                TreesOfSQLRows.this.expand(e.getKey(), e.getValue(), this.hasBeen, this.toCut, privateRows, ls);
            }
        }
    }

    // unique SQLRowValues indexed by SQLRow
    static private final class Rows {

        private final Map<SQLRow, SQLRowValues> vals;
        private final Set<SQLRow> mainRows;

        private Rows() {
            this.vals = new HashMap<SQLRow, SQLRowValues>();
            this.mainRows = new HashSet<SQLRow>();
        }

        private boolean contains(final SQLRow r) {
            return this.vals.containsKey(r);
        }

        private SQLRowValues getValues(final SQLRow r) {
            return this.vals.get(r);
        }

        private void put(final SQLRow r, final SQLRowValues newVals, final boolean isMainRow) {
            assert newVals.asRow().equals(r);
            if (this.vals.put(r, newVals) != null)
                throw new IllegalStateException("Row already in : " + newVals);
            if (isMainRow)
                this.mainRows.add(r);
        }

        // link rowWithFK if it is already contained in this
        private boolean tryToLink(final SQLRowValues rowWithFK, final Link l) {
            final SQLRowValues inGraphRow = this.getValues(rowWithFK.asRow());
            final boolean linked = inGraphRow != null;
            if (linked) {
                // add link
                final SQLRowValues dest = this.getValues(rowWithFK.getForeign(l).asRow());
                if (dest == null)
                    throw new IllegalStateException("destination of " + l + " not found for " + rowWithFK);
                inGraphRow.putForeign(l, dest);
            }
            return linked;
        }

    }

    static private final class LinksToCutMutable {

        // use List to avoid comparing SQLRowValues instances
        private final ListMap<SQLElementLink, SQLRowValues> toCut;

        private LinksToCutMutable() {
            this.toCut = new ListMap<SQLElementLink, SQLRowValues>(32, Mode.NULL_FORBIDDEN, false);
        }

        private void add(SQLElementLink link, Collection<SQLRowValues> rowsWithFK) {
            assert !this.toCut.containsKey(link) || !CollectionUtils.containsAny(this.toCut.get(link), rowsWithFK) : "some rows (and their optional joins) already added : " + link + " " + rowsWithFK;
            // a row might reference the same row multiple times
            this.toCut.addAll(link, rowsWithFK);
        }

        private final ListMap<SQLElementLink, SQLRowValues> getMap() {
            return this.toCut;
        }

        private final void restoreLinks(final Rows hasBeen) {
            final Iterator<Entry<SQLElementLink, List<SQLRowValues>>> iter = this.getMap().entrySet().iterator();
            while (iter.hasNext()) {
                final Entry<SQLElementLink, List<SQLRowValues>> e = iter.next();
                final SQLElementLink elemLink = e.getKey();
                final Link linkToCut = elemLink.getPath().getStep(-1).getSingleLink();

                final Iterator<SQLRowValues> iter2 = e.getValue().iterator();
                while (iter2.hasNext()) {
                    final SQLRowValues rowWithFK = iter2.next();
                    if (hasBeen.tryToLink(rowWithFK, linkToCut)) {
                        // remove from toCut
                        iter2.remove();
                    }
                }
                if (e.getValue().isEmpty())
                    iter.remove();
            }
        }
    }

    // ***

    /**
     * Put all the main (i.e. non private) rows of the trees (except the roots) in a map by table.
     * 
     * @return the descendants by table.
     */
    public final Map<SQLTable, List<SQLRowAccessor>> getDescendantsByTable() {
        final ListMap<SQLTable, SQLRowAccessor> res = new ListMap<SQLTable, SQLRowAccessor>();
        final Set<SQLRow> roots = this.getRows();
        for (final SQLRowValuesCluster c : this.getClusters()) {
            for (final SQLRowValues v : c.getItems()) {
                final SQLRow r = v.asRow();
                if (!roots.contains(r) && this.getMainRows().contains(r))
                    res.add(v.getTable(), v);
            }
        }
        return res;
    }

    // * extern

    static final class LinkToCut implements Comparable<LinkToCut> {
        private final SQLElementLink link;
        private final String label;

        protected LinkToCut(final SQLElementLink link) {
            super();
            if (link == null)
                throw new NullPointerException("Null link");
            this.link = link;
            this.label = TreesOfSQLRows.getLabel(this.link, null);
        }

        public final Path getPath() {
            return this.link.getPath();
        }

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

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

        public final String getLabel() {
            return this.label;
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            return prime + this.link.hashCode();
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            final LinkToCut other = (LinkToCut) obj;
            return this.link.equals(other.link);
        }

        @Override
        public int compareTo(LinkToCut o) {
            final int compareTable = CompareUtils.compareList(this.getTable().getSQLName().asList(), o.getTable().getSQLName().asList());
            if (compareTable != 0)
                return compareTable;
            else
                return this.getItem().compareTo(o.getItem());
        }
    }

    static public final class LinksToCut {

        private final ListMapItf<SQLElementLink, SQLRowValues> toCut;

        private LinksToCut(final ListMapItf<SQLElementLink, SQLRowValues> map) {
            map.removeAllEmptyCollections();
            this.toCut = ListMap.unmodifiableMap(map);
        }

        public final ListMapItf<SQLElementLink, SQLRowValues> getMap() {
            return this.toCut;
        }

        public final SortedMap<LinkToCut, Integer> countByLink() {
            final SortedMap<LinkToCut, Integer> res = new TreeMap<LinkToCut, Integer>();
            for (final Entry<SQLElementLink, List<SQLRowValues>> e : this.getMap().entrySet()) {
                final SQLElementLink elemLink = e.getKey();
                res.put(new LinkToCut(elemLink), e.getValue().size());
            }
            return res;
        }

        boolean containsAll(final LinksToCut o) {
            if (this == o)
                return true;
            if (!this.getMap().keySet().containsAll(o.getMap().keySet()))
                return false;
            final HashingStrategy<SQLRowAccessor> strategy = SQLRowAccessor.getRowStrategy();
            for (final Entry<SQLElementLink, ? extends Collection<SQLRowValues>> e : this.getMap().entrySet()) {
                final List<SQLRowValues> otherRows = o.getMap().get(e.getKey());
                // cannot be empty, see constructor
                if (otherRows != null) {
                    /*
                     * each row can be a graph : it's always the row with the foreign key, so in
                     * case of a join there's also the main row attached. Don't bother comparing the
                     * all values, just use the IDs.
                     */
                    final Set<ProxyItf<SQLRowValues>> thisVals = CustomEquals.createSet(strategy, e.getValue());
                    final Set<ProxyItf<SQLRowValues>> otherVals = CustomEquals.createSet(strategy, otherRows);
                    assert thisVals.size() == e.getValue().size() && otherVals.size() == o.getMap().get(e.getKey()).size() : "There were duplicates";
                    if (!thisVals.containsAll(otherVals))
                        return false;
                }
            }
            return true;
        }
    }

    /**
     * Return the rows that point to these trees.
     * 
     * @return the rows by referent field.
     */
    public final LinksToCut getExternReferences() {
        checkFetched();
        return this.externReferences;
    }
    
    @Override
    public String toString() {
        return this.getClass().getSimpleName() + " for " + this.getElem() + " " + this.getRows();
    }
}