org.openconcerto.sql.model.graph.DatabaseGraph.java Source code

Java tutorial

Introduction

Here is the source code for org.openconcerto.sql.model.graph.DatabaseGraph.java

Source

/*
 * 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.
 */

/*
* DatabaseGraph created on 13 mai 2004
*/
package org.openconcerto.sql.model.graph;

import static org.openconcerto.xml.JDOMUtils.OUTPUTTER;
import static java.util.Collections.singletonList;
import org.openconcerto.sql.Log;
import org.openconcerto.sql.model.ConnectionHandlerNoSetup;
import org.openconcerto.sql.model.DBFileCache;
import org.openconcerto.sql.model.DBItemFileCache;
import org.openconcerto.sql.model.DBRoot;
import org.openconcerto.sql.model.DBSystemRoot;
import org.openconcerto.sql.model.FieldRef;
import org.openconcerto.sql.model.LoadingListener.GraphLoadingEvent;
import org.openconcerto.sql.model.SQLBase;
import org.openconcerto.sql.model.SQLDataSource;
import org.openconcerto.sql.model.SQLField;
import org.openconcerto.sql.model.SQLSchema;
import org.openconcerto.sql.model.SQLServer;
import org.openconcerto.sql.model.SQLSystem;
import org.openconcerto.sql.model.SQLTable;
import org.openconcerto.sql.model.TableRef;
import org.openconcerto.sql.model.Where;
import org.openconcerto.sql.model.graph.Link.Direction;
import org.openconcerto.sql.model.graph.Link.Rule;
import org.openconcerto.sql.model.graph.ToRefreshSpec.ToRefreshActual;
import org.openconcerto.utils.ArrayComparator;
import org.openconcerto.utils.CompareUtils;
import org.openconcerto.utils.FileUtils;
import org.openconcerto.utils.SetMap;
import org.openconcerto.utils.cc.IPredicate;

import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.sql.DatabaseMetaData;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
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.logging.Level;

import net.jcip.annotations.GuardedBy;
import net.jcip.annotations.ThreadSafe;

import org.apache.commons.collections.CollectionUtils;
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.JDOMException;
import org.jdom2.input.SAXBuilder;
import org.jgrapht.Graphs;
import org.jgrapht.graph.DirectedMultigraph;

/**
 * Le graphe de la base de donne. Les noeuds tant des tables, les liens les relations de clefs
 * trangres. Cette classe utilise SQLKey pour deduire les relations entre tables.
 * 
 * @author ILM Informatique 13 mai 2004
 * @see org.openconcerto.sql.model.graph.SQLKey
 */
@ThreadSafe
public class DatabaseGraph extends BaseGraph {

    /**
     * Whether to infer foreign constraints from fields' names.
     * 
     * @see SQLKey
     */
    public static final String INFER_FK = "org.openconcerto.sql.graph.inferFK";

    private static final String XML_VERSION = "20121024-1614";
    private static final String FILENAME = "graph.xml";

    // Some systems follow the JDBC to the letter and order by PKTABLE_CAT, PKTABLE_SCHEM,
    // PKTABLE_NAME, KEY_SEQ : thus ignoring FK_NAME
    static final Comparator<Object[]> IMPORTED_KEYS_COMP;

    static {
        // PKTABLE_CAT, PKTABLE_SCHEM, PKTABLE_NAME, FK_NAME, KEY_SEQ
        final List<Comparator<Object[]>> comps = new ArrayList<Comparator<Object[]>>();
        comps.add(ArrayComparator.createNatural(0, String.class));
        comps.add(ArrayComparator.createNatural(1, String.class));
        comps.add(ArrayComparator.createNatural(2, String.class));
        comps.add(ArrayComparator.createNatural(11, String.class));
        comps.add(ArrayComparator.createNatural(8, Short.class));
        IMPORTED_KEYS_COMP = CompareUtils.createComparator(comps);
    }

    // passedBase is the base that was passed for the catalog parameter of getImportedKeys() or
    // getExportedKeys()
    public static SQLTable getTableFromJDBCMetaData(final SQLBase passedBase, final String jdbcCat,
            final String jdbcSchem, final String jdbcName) {
        final SQLServer server = passedBase.getServer();

        final String correctedCat;
        // h2 doesn't support and jdbcCat is in upper case
        // postgresql returns null
        if (server.getSQLSystem().isInterBaseSupported()) {
            correctedCat = jdbcCat;
        } else {
            correctedCat = passedBase.getName();
        }

        final SQLBase base = server.getBase(correctedCat);
        final SQLSchema schema = base == null ? null : base.getSchema(jdbcSchem);
        if (schema == null)
            throw new IllegalStateException("Schema " + correctedCat + "." + jdbcSchem
                    + " does not exist (probably filtered by DBSystemRoot.getRootsToMap())");
        final SQLTable res;
        if (server.getSQLSystem() == SQLSystem.MYSQL)
            // MySQL returns all lowercase foreignTableName, see Bug #18446 :
            // INFORMATION_SCHEMA.KEY_COLUMN_USAGE.REFERENCED_TABLE_NAME always lowercase
            // also http://bugs.mysql.com/bug.php?id=60773
            res = getTableIgnoringCase(schema, jdbcName);
        else
            res = (SQLTable) schema.getCheckedChild(jdbcName);
        return res;
    }

    private final DBSystemRoot base;
    @GuardedBy("this")
    private Map<String, Set<String>> mappedFromFile;
    // cache
    @GuardedBy("this")
    private final Map<SQLTable, Set<Link>> foreignLinks = new HashMap<SQLTable, Set<Link>>();
    @GuardedBy("this")
    private final Map<List<SQLField>, Link> foreignLink = new HashMap<List<SQLField>, Link>();

    private final ThreadLocal<Integer> atomicRefreshDepth = new ThreadLocal<Integer>() {
        protected Integer initialValue() {
            return 0;
        };
    };
    private final ThreadLocal<ToRefreshSpec> atomicRefreshItems = new ThreadLocal<ToRefreshSpec>() {
        @Override
        protected ToRefreshSpec initialValue() {
            return new ToRefreshSpec();
        }
    };

    /**
     * Cre le graphe de la base passe.
     * 
     * @param root la base dont on veut le graphe.
     */
    public DatabaseGraph(final DBSystemRoot root) {
        super(new DirectedMultigraph<SQLTable, Link>(Link.class));
        this.base = root;
        this.mappedFromFile = null;
    }

    public final void refresh(final TablesMap tablesRefreshed, final boolean readCache) throws SQLException {
        if (inAtomicRefresh()) {
            this.atomicRefreshItems.get().add(tablesRefreshed, readCache);
        } else {
            refresh(new ToRefreshSpec().add(tablesRefreshed, readCache));
        }
    }

    private final void refresh(final ToRefreshSpec toRefresh) throws SQLException {
        synchronized (this.base.getTreeMutex()) {
            synchronized (this) {
                final GraphLoadingEvent evt = new GraphLoadingEvent(this.base).fireEvent();
                try {
                    // TODO limit to file permission
                    this.mappedFromFile = Collections.unmodifiableMap(AccessController
                            .doPrivileged(new PrivilegedExceptionAction<Map<String, Set<String>>>() {
                                @Override
                                public Map<String, Set<String>> run() throws SQLException {
                                    return mapTables(toRefresh);
                                }
                            }));
                } catch (PrivilegedActionException e) {
                    throw (SQLException) e.getCause();
                } finally {
                    evt.fireFinishingEvent();
                }
            }
        }
    }

    /**
     * Whether this thread is in an atomic refresh. NOTE: if <code>true</code> then the graph won't
     * be up to date.
     * 
     * @return <code>true</code> if this thread is in an atomic refresh.
     */
    public final boolean inAtomicRefresh() {
        return this.atomicRefreshDepth.get().intValue() > 0;
    }

    /**
     * Execute the passed Callable with only one refresh at the end. This method is reentrant.
     * 
     * @param callable what to do.
     * @return the result of <code>callable</code>.
     * @throws SQLException if an error occurs.
     */
    public final <V> V atomicRefresh(final Callable<V> callable) throws SQLException {
        final V res;
        this.atomicRefreshDepth.set(this.atomicRefreshDepth.get().intValue() + 1);
        // this method is useful for grouping multiple changes to the structure, so be sure to
        // prevent other threads from modifying and thus changing the graph
        synchronized (this.base.getTreeMutex()) {
            final int newVal;
            try {
                res = callable.call();
            } catch (Exception e) {
                throw new SQLException("Call failed", e);
            } finally {
                newVal = this.atomicRefreshDepth.get().intValue() - 1;
                this.atomicRefreshDepth.set(newVal);
                assert newVal >= 0;
            }
            if (newVal == 0) {
                final ToRefreshSpec itemsToRefresh = this.atomicRefreshItems.get();
                this.atomicRefreshItems.remove();
                this.atomicRefreshDepth.remove();
                // we need to call refresh() only once to avoid order and cycle issues
                refresh(itemsToRefresh);
            }
        }
        return res;
    }

    /**
     * The list of roots mapped from file.
     * 
     * @return list of roots and tables mapped from file.
     */
    synchronized final Map<String, Set<String>> getMappedFromFile() {
        return this.mappedFromFile;
    }

    private final SQLServer getServer() {
        return this.base.getAnc(SQLServer.class);
    }

    /**
     * Construit la carte des tables
     * 
     * @param toRefreshSpec the roots and tables to refresh.
     * @return roots and tables loaded from file.
     * @throws SQLException if an error occurs.
     */
    private synchronized Map<String, Set<String>> mapTables(final ToRefreshSpec toRefreshSpec) throws SQLException {
        assert Thread.holdsLock(this.base.getTreeMutex()) : "Cannot graph a changing object";
        Map<String, Set<String>> res = new TablesMap();

        final Set<SQLTable> currentTables = this.getAllTables();
        final ToRefreshActual toRefresh = toRefreshSpec.getActual(this.base, currentTables);
        // clear graph and add tables (vertices)
        {
            final Set<SQLTable> newTablesInScope = toRefresh.getNewTablesInScope();
            final Set<SQLTable> oldTablesInScope = toRefresh.getOldTablesInScope();
            // refresh all ?
            final boolean clearGraph = oldTablesInScope.equals(currentTables);

            // clear cache
            synchronized (this) {
                if (clearGraph) {
                    this.foreignLink.clear();
                    this.foreignLinks.clear();
                } else {
                    for (final Iterator<Entry<List<SQLField>, Link>> iter = this.foreignLink.entrySet()
                            .iterator(); iter.hasNext();) {
                        final Entry<List<SQLField>, Link> e = iter.next();
                        // don't use e.getValue() since it can be null
                        final SQLTable linkTable = e.getKey().get(0).getTable();
                        if (oldTablesInScope.contains(linkTable))
                            iter.remove();
                    }
                    for (final Iterator<Entry<SQLTable, Set<Link>>> iter = this.foreignLinks.entrySet()
                            .iterator(); iter.hasNext();) {
                        final Entry<SQLTable, Set<Link>> e = iter.next();
                        final SQLTable linkTable = e.getKey().getTable();
                        if (oldTablesInScope.contains(linkTable))
                            iter.remove();
                    }
                }
            }

            if (clearGraph) {
                this.getGraphP().removeAllVertices(oldTablesInScope);
                assert this.getGraphP().vertexSet().size() == 0 && this.getGraphP().edgeSet().size() == 0;
            } else {
                // Removing a vertex also removes edges, so check that we also refresh referent
                // tables otherwise they won't have any foreign links anymore which is wrong if
                // removedTable was just renamed
                // Also the cache is only cleared for tables in scope, meaning that the cache for
                // those referent tables will be incoherent with the actual graph
                final Collection<SQLTable> removedTables = org.openconcerto.utils.CollectionUtils
                        .subtract(oldTablesInScope, newTablesInScope);
                for (final SQLTable removedTable : removedTables) {
                    final Set<SQLTable> referentTables = getReferentTables(removedTable);
                    // MAYBE add option to refresh needed tables instead of failing
                    if (!oldTablesInScope.containsAll(referentTables)) {
                        throw new IllegalStateException(
                                removedTable + " has been removed but some of its referents won't be refreshed : "
                                        + org.openconcerto.utils.CollectionUtils.subtract(referentTables,
                                                oldTablesInScope));
                    }
                }
                this.getGraphP().removeAllVertices(removedTables);

                // remove links that will be refreshed.
                final Set<Link> linksToRemove = new HashSet<Link>();
                for (final SQLTable t : org.openconcerto.utils.CollectionUtils.intersection(oldTablesInScope,
                        newTablesInScope)) {
                    linksToRemove.addAll(this.getGraphP().outgoingEdgesOf(t));
                }
                this.getGraphP().removeAllEdges(linksToRemove);
            }

            // add new tables (and existing but it's OK graph vertices is a set)
            Graphs.addAllVertices(this.getGraphP(), newTablesInScope);
        }
        final TablesMap fromXML = toRefresh.getFromXML();
        final TablesMap fromJDBC = toRefresh.getFromJDBC();
        if (fromXML.size() > 0) {
            final DBItemFileCache dir = this.getFileCache();
            try {
                if (dir != null) {
                    Log.get().config("for mapping " + this + " trying xmls in " + dir);
                    final long t1 = System.currentTimeMillis();
                    res = this.mapFromXML(fromXML);
                    // remove what was loaded
                    fromXML.removeAll(res);
                    final long t2 = System.currentTimeMillis();
                    Log.get().config("XML took " + (t2 - t1) + "ms for mapping the graph of " + this.base.getName()
                            + "." + res);
                }
            } catch (Exception e) {
                SQLBase.logCacheError(dir, e);
                this.deleteGraphFiles();
            }
            // add to JDBC what wasn't loaded
            fromJDBC.merge(fromXML);
        }
        if (!fromJDBC.isEmpty()) {
            final long t1 = System.currentTimeMillis();
            for (final Entry<String, Set<String>> e : fromJDBC.entrySet()) {
                final String rootName = e.getKey();
                final Set<String> tableNames = e.getValue();
                final DBRoot r = this.base.getRoot(rootName);
                // first try to map the whole root at once
                if (!this.map(r, tableNames)) {
                    // if this isn't supported use standard JDBC
                    for (final String table : tableNames) {
                        this.map(r, table, null);
                    }
                }
                this.save(r);
            }
            final long t2 = System.currentTimeMillis();
            Log.get()
                    .config("JDBC took " + (t2 - t1) + "ms for mapping the graph of " + this.base + "." + fromJDBC);
        }
        return res;
    }

    private final void addLink(final List<SQLField> from, final List<SQLField> to, String foreignKeyName,
            Rule updateRule, Rule deleteRule) {
        addLink(new Link(from, to, foreignKeyName, updateRule, deleteRule));
    }

    private final void addLink(final Link l) {
        DirectedEdge.addEdge(this.getGraphP(), l);
    }

    private boolean map(final DBRoot r, final Set<String> tableNames) throws SQLException {
        // on PG test goes from 75ms to 18ms
        if (tableNames.size() <= 1)
            return false;

        if (r.getServer().getSQLSystem() == SQLSystem.POSTGRESQL) {
            this.map(r, null, tableNames);
            return true;
        } else {
            return false;
        }
    }

    private Rule getRule(final Number n, final SQLSystem sys) {
        final Rule res = Rule.fromShort(n.shortValue());
        // MS SQL incorrectly report RESTRICT as it doesn't support it
        return sys == SQLSystem.MSSQL && Rule.RESTRICT.equals(res) ? Rule.NO_ACTION : res;
    }

    private void map(final DBRoot r, final String tableName, final Set<String> tableNames) throws SQLException {
        // either we refresh the whole root and we must know which tables to use
        // or we refresh only one table and tableNames is useless
        assert tableName == null ^ tableNames == null;
        final SetMap<String, String> metadataFKs = new SetMap<String, String>();
        @SuppressWarnings("unchecked")
        final List<Object[]> importedKeys = this.base.getDataSource()
                .useConnection(new ConnectionHandlerNoSetup<List, SQLException>() {
                    @Override
                    public List handle(final SQLDataSource ds) throws SQLException {
                        final DatabaseMetaData metaData = ds.getConnection().getMetaData();
                        return (List) SQLDataSource.ARRAY_LIST_HANDLER.handle(metaData
                                .getImportedKeys(r.getBase().getMDName(), r.getSchema().getName(), tableName));
                    }
                });
        // accumulators for multi-field foreign key
        final List<SQLField> from = new ArrayList<SQLField>();
        final List<SQLField> to = new ArrayList<SQLField>();
        final SQLSystem sys = this.base.getServer().getSQLSystem();
        Rule updateRule = null;
        Rule deleteRule = null;
        String name = null;
        // Follow the JDBC to the letter and order by PKTABLE_CAT, PKTABLE_SCHEM, PKTABLE_NAME,
        // KEY_SEQ : thus ignoring FK_NAME
        if (sys == SQLSystem.MSSQL) {
            Collections.sort(importedKeys, IMPORTED_KEYS_COMP);
        }
        final Iterator<Object[]> ikIter = importedKeys.iterator();
        while (ikIter.hasNext()) {
            final Object[] m = ikIter.next();

            // FKTABLE_SCHEM
            assert CompareUtils.equals(m[5], r.getSchema().getName());
            // FKTABLE_NAME
            final String fkTableName = (String) m[6];
            assert tableName == null || tableName.equals(fkTableName);
            if (tableNames != null && !tableNames.contains(fkTableName))
                continue;
            // not by name, postgresql returns lowercase
            // "FKCOLUMN_NAME"
            final String keyName = (String) m[7];
            // "KEY_SEQ"
            final short seq = ((Number) m[8]).shortValue();
            // "PKCOLUMN_NAME"
            final String foreignTableColName = (String) m[3];
            // "FK_NAME"
            final String foreignKeyName = (String) m[11];

            final SQLField key = r.getTable(fkTableName).getField(keyName);

            final SQLTable foreignTable;
            try {
                foreignTable = getTableFromJDBCMetaData(r.getBase(), (String) m[0], (String) m[1], (String) m[2]);
            } catch (Exception e) {
                throw new IllegalStateException("Could not find what " + key.getSQLName() + " references", e);
            }

            metadataFKs.add(fkTableName, keyName);
            if (seq == 1) {
                // if we start a new link add the current one
                if (from.size() > 0)
                    addLink(from, to, name, updateRule, deleteRule);
                from.clear();
                to.clear();
            }
            from.add(key);
            assert seq == 1 || from.get(from.size() - 2).getTable() == from.get(from.size() - 1).getTable();
            to.add(foreignTable.getField(foreignTableColName));
            assert seq == 1 || to.get(to.size() - 2).getTable() == to.get(to.size() - 1).getTable();

            final Rule prevUpdateRule = updateRule;
            final Rule prevDeleteRule = deleteRule;
            // "UPDATE_RULE"
            updateRule = getRule((Number) m[9], sys);
            // "DELETE_RULE"
            deleteRule = getRule((Number) m[10], sys);
            if (seq > 1) {
                if (prevUpdateRule != updateRule)
                    throw new IllegalStateException(
                            "Incoherent update rules " + prevUpdateRule + " != " + updateRule);
                if (prevDeleteRule != deleteRule)
                    throw new IllegalStateException(
                            "Incoherent delete rules " + prevDeleteRule + " != " + deleteRule);
            }

            name = foreignKeyName;
            // MAYBE DEFERRABILITY
        }
        if (from.size() > 0)
            addLink(from, to, name, updateRule, deleteRule);

        if (Boolean.getBoolean(INFER_FK)) {
            final Set<String> tables = tableName != null ? Collections.singleton(tableName) : tableNames;
            for (final String tableToInfer : tables) {
                final SQLTable table = r.getTable(tableToInfer);
                final Set<String> lexicalFKs = SQLKey.foreignKeys(table);
                // already done
                lexicalFKs.removeAll(metadataFKs.getNonNull(table.getName()));
                // MAYBE option to print out foreign keys w/o constraint
                for (final String keyName : lexicalFKs) {
                    final SQLField key = table.getField(keyName);
                    addLink(singletonList(key), singletonList(SQLKey.keyToTable(key).getKey()), null, null, null);
                }
            }
        }
    }

    static private final SQLTable getTableIgnoringCase(final SQLSchema s, String tablename) {
        final List<SQLTable> matchingTables = new ArrayList<SQLTable>(4);
        for (final String tname : s.getTableNames())
            if (tname.equalsIgnoreCase(tablename))
                matchingTables.add(s.getTable(tname));
        if (matchingTables.size() == 0)
            // this will throw an exception
            return (SQLTable) s.getCheckedChild(tablename);
        else if (matchingTables.size() == 1)
            return matchingTables.get(0);
        else
            throw new IllegalStateException("More than one table matches " + tablename + " : " + matchingTables);
    }

    // ** cache

    /**
     * Where xml dumps are saved, always <code>null</code> if {@link DBSystemRoot#useCache()} is
     * <code>false</code>.
     * 
     * @return the directory of xmls dumps, <code>null</code> if it can't be found.
     */
    private DBItemFileCache getFileCache() {
        final boolean useXML = this.base.useCache();
        final DBFileCache d = this.getServer().getFileCache();
        if (!useXML || d == null)
            return null;
        else {
            return d.getChild(this.base);
        }
    }

    private final File getRootFile(String root) {
        final DBItemFileCache saveDir = this.getFileCache();
        if (saveDir == null)
            return null;
        return getGraphFile(saveDir.getChild(root));
    }

    private final List<DBItemFileCache> getSavedCaches(boolean withFile) {
        final DBItemFileCache item = this.getFileCache();
        if (item == null)
            return Collections.emptyList();
        else {
            return item.getSavedDesc(DBRoot.class, withFile ? FILENAME : null);
        }
    }

    final void deleteGraphFiles() {
        for (final DBItemFileCache i : this.getSavedCaches(true)) {
            getGraphFile(i).delete();
        }
    }

    final void deleteGraphFile(String rootName) {
        getRootFile(rootName).delete();
    }

    private File getGraphFile(final DBItemFileCache i) {
        return i.getFile(FILENAME);
    }

    boolean save(final DBRoot r) {
        final String rootName = r.getName();
        final File rootFile = this.getRootFile(rootName);
        if (rootFile == null) {
            return false;
        } else {
            assert Thread
                    .holdsLock(this.base.getTreeMutex()) : "Might save garbage if two threads open the same file";
            BufferedWriter pWriter = null;
            try {
                FileUtils.mkdir_p(rootFile.getParentFile());
                pWriter = FileUtils.createXMLWriter(rootFile);
                pWriter.write("<root codecVersion=\"");
                pWriter.write(XML_VERSION);
                pWriter.write("\"");
                SQLSchema.getVersionAttr(r.getSchema(), pWriter);
                pWriter.write(" >\n");
                for (final SQLTable t : r.getDescs(SQLTable.class)) {
                    final Set<Link> flinks = this.getForeignLinks(t);
                    // now that the atomic level is the table we must explicitly record which tables
                    // have no links (to differentiate from tables which we don't know of)
                    pWriter.write("<table name=\"");
                    pWriter.write(OUTPUTTER.escapeAttributeEntities(t.getName()));
                    pWriter.write("\">\n");
                    for (final Link l : flinks) {
                        l.toXML(pWriter);
                    }
                    pWriter.write("</table>\n");
                }
                pWriter.write("\n</root>\n");

                return true;
            } catch (Exception e) {
                Log.get().log(Level.WARNING, "unable to save files in " + rootFile, e);
                return false;
            } finally {
                if (pWriter != null) {
                    try {
                        pWriter.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    /**
     * Loads all passed saved roots.
     * 
     * @param fromXML the roots and tables to refresh.
     * @return the root and tables names that were loaded.
     * @throws JDOMException if a file is not valid XML.
     * @throws IOException if a file content is not correct.
     */
    private TablesMap mapFromXML(final TablesMap fromXML) throws JDOMException, IOException {
        final TablesMap res = new TablesMap();
        for (final DBItemFileCache cache : getSavedCaches(true)) {
            final String rootName = cache.getName();
            if (!fromXML.containsKey(rootName))
                continue;
            final Document doc = new SAXBuilder().build(getGraphFile(cache));
            final String fileVersion = doc.getRootElement().getAttributeValue("codecVersion");
            if (!XML_VERSION.equals(fileVersion))
                throw new IOException("wrong format version, expected " + XML_VERSION + " got: " + fileVersion);
            // if the systemRoot doesn't contain the saved root, it means it is filtered (otherwise
            // it would have been erased) so we don't need to load it
            if (this.base.contains(rootName)) {
                final DBRoot r = (DBRoot) this.base.getCheckedChild(rootName);
                // checking version in XMLStructureSource is not enough since it can happen that
                // a DBFileCache is available before this but after the structure was loaded. In
                // this case no version check has been made and thus this file can be obsolete.
                final String xmlVersion = SQLSchema.getVersion(doc.getRootElement());
                final String actualVersion = r.getSchema().getVersion();
                if (!CompareUtils.equals(xmlVersion, actualVersion))
                    throw new IOException("wrong DB version, expected " + actualVersion + " got: " + xmlVersion);
                final Set<String> fromXMLTableNames = fromXML.get(rootName);
                for (final Element tableElem : doc.getRootElement().getChildren()) {
                    final SQLTable t = r.getTable(tableElem.getAttributeValue("name"));
                    if (fromXMLTableNames.contains(t.getName())) {
                        for (final Element linkElem : tableElem.getChildren()) {
                            addLink(Link.fromXML(t, linkElem));
                        }
                        // t was loaded (even if it had no links)
                        res.add(rootName, t.getName());
                    }
                }
                // rootName was loaded from XML (even if it had no tables)
                if (!res.containsKey(rootName))
                    res.put(rootName, Collections.<String>emptySet());
            }
        }
        return res;
    }

    // From + To

    /**
     * Renvoie tous les liens qui partent ou arrivent sur la table passe.
     * 
     * @param table la table dont on veut connaitre les liens.
     * @return tous les liens qui partent ou arrivent sur cette table.
     */
    public synchronized Set<Link> getAllLinks(SQLTable table) {
        if (table == null)
            throw new NullPointerException();
        return this.getGraphP().edgesOf(table);
    }

    public Set<Link> getLinks(SQLTable table, Direction dir) {
        if (table == null || dir == null)
            throw new NullPointerException();
        if (dir == Direction.ANY)
            return this.getAllLinks(table);
        else if (dir == Direction.REFERENT)
            return this.getReferentLinks(table);
        else
            return this.getForeignLinks(table);
    }

    public Set<Link> getLinks(SQLTable table, Direction dir, final IPredicate<? super Link> pred) {
        final Set<Link> allLinks = this.getLinks(table, dir);
        // don't create instance for nothing
        return pred == null || pred == IPredicate.truePredicate() ? allLinks
                : org.openconcerto.utils.CollectionUtils.select(allLinks, pred, new HashSet<Link>());
    }

    public Set<Link> getLinksWithOpposite(final SQLTable table, final Direction dir,
            final String oppositeTableName) {
        return this.getLinks(table, dir,
                oppositeTableName == null ? null : new Link.NamePredicate(table, oppositeTableName));
    }

    /**
     * Get a single link from/to <code>table</code>.
     * 
     * @param table an end of the link.
     * @param dir should the link start from or end at <code>table</code>.
     * @return the single link.
     * @throws IllegalStateException if not one and only one link matching.
     */
    public Link getLink(final SQLTable table, final Direction dir) {
        return this.getLink(table, dir, null);
    }

    public Link getLink(final SQLTable table, final Direction dir, final IPredicate<? super Link> pred) {
        return this.getLink(table, dir, pred, false);
    }

    public Link getLinkWithOpposite(final SQLTable table, final Direction dir, final String oppositeTableName,
            final boolean nullIfNone) {
        return this.getLink(table, dir,
                oppositeTableName == null ? null : new Link.NamePredicate(table, oppositeTableName), nullIfNone);
    }

    /**
     * Get a single link from/to <code>table</code>.
     * 
     * @param table an end of the link.
     * @param dir should the link start from or end at <code>table</code>.
     * @param pred to filter the links from/to <code>table</code>.
     * @param nullIfNone if <code>false</code> this method never returns <code>null</code>, if
     *        <code>true</code> it will return <code>null</code> if no link matches.
     * @throws IllegalStateException if not one and only one link matching.
     * @return the single link, or <code>null</code> if none matched and <code>nullIfNone</code> is
     *         true.
     */
    public Link getLink(final SQLTable table, final Direction dir, final IPredicate<? super Link> pred,
            final boolean nullIfNone) {
        final Set<Link> res = this.getLinks(table, dir, pred);
        if (res.size() > 1) {
            throw new IllegalStateException("More than one link from " + table + " with direction " + dir
                    + " and predicate " + pred + " : " + res);
        } else if (res.size() == 0) {
            if (nullIfNone)
                return null;
            else
                throw new IllegalStateException(
                        "No link from " + table + " with direction " + dir + " and predicate " + pred);
        }
        return res.iterator().next();
    }

    Set<Link> getLinks(SQLTable table, Direction dir, final boolean onlyOne) {
        return this.getLinks(table, dir, onlyOne, null);
    }

    Set<Link> getLinks(SQLTable table, Direction dir, final boolean onlyOne, final IPredicate<? super Link> pred) {
        if (onlyOne)
            // don't want nullIfNone
            return Collections.singleton(this.getLink(table, dir, pred, false));
        else
            return this.getLinks(table, dir, pred);
    }

    // Foreign

    /**
     * Renvoie tous les liens qui partent de la table passe.
     * 
     * @param table la table dont on veut connaitre les liens.
     * @return tous les liens qui partent de cette table.
     */
    public synchronized Set<Link> getForeignLinks(SQLTable table) {
        Set<Link> res = this.foreignLinks.get(table);
        // works because res cannot be null
        if (res == null) {
            res = Collections.unmodifiableSet(this.getGraphP().outgoingEdgesOf(table));
            this.foreignLinks.put(table, res);
        }
        return res;
    }

    public Set<SQLField> getForeignKeys(SQLTable table) {
        return getLabels(this.getForeignLinks(table));
    }

    public final Set<List<SQLField>> getForeignKeysFields(SQLTable table) {
        return getCols(this.getForeignLinks(table));
    }

    /**
     * Return the link corresponding to the passed foreign field.
     * 
     * @param fk a foreign key.
     * @return the Link corresponding to the passed field, or <code>null</code> if it is not a
     *         foreign field.
     */
    public Link getForeignLink(SQLField fk) {
        return this.getForeignLink(singletonList(fk));
    }

    public Link getForeignLink(SQLTable t, List<String> fields) {
        final List<SQLField> fks = new ArrayList<SQLField>(fields.size());
        for (final String s : fields)
            fks.add(t.getField(s));
        return this.getForeignLink(fks);
    }

    public synchronized Link getForeignLink(final List<SQLField> fk) {
        if (fk.size() == 0)
            throw new IllegalArgumentException("empty list");
        // result can be null
        if (!this.foreignLink.containsKey(fk)) {
            this.foreignLink.put(fk, (Link) CollectionUtils.find(this.getForeignLinks(fk.get(0).getTable()),
                    new LabelPredicate(fk)));
        }
        return this.foreignLink.get(fk);
    }

    /**
     * Retourne la table trangre sur laquelle pointe le champ pass.
     * 
     * @param fk une clef trangre.
     * @return la table sur laquelle pointe le champ.
     */
    public SQLTable getForeignTable(SQLField fk) {
        final Link l = this.getForeignLink(fk);
        if (l != null)
            return l.getTarget();
        else
            return null;
    }

    /**
     * Retourne les liens de t1  t2.
     * 
     * @param t1 la premiere table.
     * @param t2 la 2eme table.
     * @return les liens de t1  t2.
     * @throws NullPointerException if either t1 or t2 is <code>null</code>.
     */
    public synchronized Set<Link> getForeignLinks(SQLTable t1, SQLTable t2) {
        if (t1 == null || t2 == null)
            throw new NullPointerException("t1: " + t1 + ", t2: " + t2);
        return this.getGraphP().getAllEdges(t1, t2);
    }

    public Set<SQLField> getForeignFields(SQLTable t1, SQLTable t2) {
        return getLabels(this.getForeignLinks(t1, t2));
    }

    // Referent

    /**
     * Renvoie tous les liens qui arrivent sur la table passe.
     * 
     * @param table la table dont on veut connaitre les liens.
     * @return tous les liens qui arrivent sur cette table.
     */
    public synchronized Set<Link> getReferentLinks(SQLTable table) {
        return this.getGraphP().incomingEdgesOf(table);
    }

    public Set<SQLField> getReferentKeys(SQLTable table) {
        return getLabels(this.getReferentLinks(table));
    }

    /**
     * Renvoie toutes les tables qui pointent sur la table passe.
     * 
     * @param table la table dont on veut connaitre les rfrentes.
     * @return toutes les tables qui pointent <code>table</code>.
     */
    public Set<SQLTable> getReferentTables(SQLTable table) {
        final Set<SQLTable> res = new HashSet<SQLTable>();
        for (final Link l : this.getReferentLinks(table))
            res.add(l.getSource());
        return res;
    }

    public Set<SQLTable> findReferentTables(SQLTable table, final String refTable, final List<String> refKeys) {
        final Set<SQLTable> res = new HashSet<SQLTable>();
        for (final Link l : this.getReferentLinks(table)) {
            if (l.getSource().getName().equals(refTable) && (refKeys.isEmpty() || l.getCols().equals(refKeys)))
                res.add(l.getSource());
        }
        return res;
    }

    public SQLTable findReferentTable(SQLTable table, final String refTable, final List<String> refKeys) {
        return org.openconcerto.utils.CollectionUtils.getSole(this.findReferentTables(table, refTable, refKeys));
    }

    /**
     * Find the only referent table named <code>refTable</code>.
     * 
     * @param table a table.
     * @param refTable a table name that should point to <code>table</code>.
     * @param refKeys the names of the fields from <code>refTable</code>, empty meaning don't use
     *        them to filter.
     * @return the only matching table or <code>null</code> (i.e. if there's none or more than one).
     * @see #findReferentTables(SQLTable, String, List)
     */
    public SQLTable findReferentTable(SQLTable table, final String refTable, final String... refKeys) {
        return this.findReferentTable(table, refTable, Arrays.asList(refKeys));
    }

    // Between

    /**
     * Retourne tous les liens entre t1 et t2. C'est  dire les liens qui partent de t1 pour t2 et
     * inversement.
     * 
     * @param t1 la premiere table.
     * @param t2 la 2eme table.
     * @return l'ensemble des liens directs entre les 2 tables.
     */
    public Set<Link> getLinks(SQLTable t1, SQLTable t2) {
        Set<Link> res = new HashSet<Link>(this.getForeignLinks(t1, t2));
        res.addAll(this.getForeignLinks(t2, t1));
        return res;
    }

    /**
     * Renvoie tous les champs entre t1 et t2.
     * 
     * @param t1 la premiere table.
     * @param t2 la 2eme table.
     * @return l'ensemble des champs qui lient les 2 tables.
     * @see #getLinks(SQLTable, SQLTable)
     */
    public Set<SQLField> getFields(SQLTable t1, SQLTable t2) {
        return getLabels(this.getLinks(t1, t2));
    }

    // *** Wheres

    /**
     * Renvoie la clause WHERE pour faire la jointure en t1 et t2. Par exemple entre MISSION et
     * RAPPORT : <br/>
     * RAPPORT.ID_MISSION=MISSION.ID_MISSION OR MISSION.ID_RAPPORT_INITIAL=RAPPORT.ID_RAPPORT. Pour
     * un sous-ensemble des liens, utiliser {@link #getWhereClause(TableRef, TableRef, Step)}, pour
     * un seul champ {@link #getWhereClause(SQLField)}.
     * 
     * @param t1 la premiere table.
     * @param t2 la deuxieme table.
     * @return le OR de tous les liens entres les 2 tables.
     */
    public Where getWhereClause(TableRef t1, TableRef t2) {
        return this.getWhereClause(t1, t2, null);
    }

    public Where getWhereClause(final Step step) {
        return this.getWhereClause(step.getFrom(), step.getTo(), step);
    }

    /**
     * Renvoie la clause WHERE pour faire la jointure en t1 et t2.
     * 
     * @param t1 la premiere table.
     * @param t2 la deuxieme table.
     * @param fields les liens  utiliser, <code>null</code> pour tous.
     * @return le OR des champs passs.
     */
    public Where getWhereClause(TableRef t1, TableRef t2, Step fields) {
        // OR car OBSERVATION.ID_ARTICLE_1,2,3
        return Where.or(this.getStraightWhereClause(t1, t2, fields));
    }

    // MAYBE allow to pass self reference links in both directions with a SetMap<Direction, Link>,
    // e.g. find both next and previous mission

    /**
     * Renvoie les clauses WHERE pour faire la jointure en t1 et t2.
     * 
     * @param t1 la premiere table.
     * @param t2 la deuxieme table.
     * @param step les liens  utiliser, <code>null</code> pour tous.
     * @return les WHERE demands.
     */
    public Set<Where> getStraightWhereClause(TableRef t1, TableRef t2, Step step) {
        if (step == null) {
            step = Step.create(t1.getTable(), t2.getTable());
        } else {
            if (t1 == null)
                t1 = step.getFrom();
            else if (step.getFrom() != t1.getTable())
                throw new IllegalArgumentException("step isn't from t1 " + step);
            if (t2 == null)
                t2 = step.getTo();
            else if (step.getTo() != t2.getTable())
                throw new IllegalArgumentException("step isn'ts to t2 " + step);
        }

        final Set<Where> res = new HashSet<Where>();
        for (final Link l : step.getLinks()) {
            final Direction dir = step.getDirection(l);
            res.add(getWhereClause(t1, t2, l, dir));
        }

        return res;
    }

    public Where getWhereClause(final Link l) {
        return getWhereClause(l.getSource(), l.getTarget(), l, Direction.FOREIGN);
    }

    /**
     * The where clause to join t1 to t2.
     * 
     * @param t1 the first table, can be <code>null</code>, e.g. <code>null</code>.
     * @param t2 the second table, can be <code>null</code>, e.g. "LOCAL loc".
     * @param l a link between <code>t1</code> and <code>t2</code>, e.g. LOCAL.ID_BATIMENT.
     * @param dir how to go through <code>l</code>, either {@link Direction#FOREIGN} or
     *        {@link Direction#REFERENT}, e.g. <code>REFERENT</code>.
     * @return the WHERE, e.g. loc.ID_BATIMENT = BATIMENT.ID.
     */
    public Where getWhereClause(final TableRef t1, final TableRef t2, final Link l, final Direction dir) {
        if (l == null)
            throw new NullPointerException("Null link");
        TableRef src, dest;
        if (dir == Direction.FOREIGN) {
            src = t1;
            dest = t2;
        } else if (dir == Direction.REFERENT) {
            src = t2;
            dest = t1;
        } else {
            throw new IllegalArgumentException("Invalid direction : " + dir);
        }
        if (src == null)
            src = l.getSource();
        else if (src.getTable() != l.getSource())
            throw new IllegalArgumentException("Wrong source table " + src.getTable() + " != " + l.getSource());
        if (dest == null)
            dest = l.getTarget();
        else if (dest.getTable() != l.getTarget())
            throw new IllegalArgumentException("Wrong target table " + dest.getTable() + " != " + l.getTarget());
        final Iterator<SQLField> primaryKeys = dest.getTable().getPrimaryKeys().iterator();
        Where w = null;
        for (final SQLField f : l.getFields()) {
            assert f.getTable() == src.getTable();
            final FieldRef f1 = src.getField(f.getName());
            final FieldRef f2 = dest.getField(primaryKeys.next().getName());
            w = new Where(f1, "=", f2).and(w);
        }
        assert w != null : "Empty fields for " + l;
        assert !primaryKeys.hasNext() : "Mismatch";
        return w;
    }

    /**
     * Renvoie la clause where pour faire la jointure suivant ce champ.
     * 
     * @param f un champ, eg RAPPORT_GENERE.ID_MISSION.
     * @return eg "RAPPORT_GENERE.ID_MISSION=MISSION.ID_MISSION".
     */
    public Where getWhereClause(SQLField f) {
        return new Where(f, "=", this.getForeignTable(f).getKey());
    }

    // *** Jointures

    public Where getJointure(Path p) {
        Where res = null;
        for (int i = 0; i < p.length(); i++) {
            res = this.getWhereClause(p.getStep(i)).and(res);
        }
        return res;
    }

    public Set<Where> getStraightJoin(final Path p) {
        if (p.length() == 0) {
            throw new IllegalArgumentException("Path empty");
        } else if (p.length() == 1) {
            final SQLTable previous = p.getTable(0);
            final SQLTable table = p.getTable(1);
            return this.getStraightWhereClause(previous, table, p.getStep(0));
        } else {
            final Set<Where> res = new HashSet<Where>();

            final Set<Where> wheres = this.getStraightJoin(p.justFirst());
            final Iterator<Where> wheresIter = wheres.iterator();
            while (wheresIter.hasNext()) {
                final Where where = wheresIter.next();
                final Iterator<Where> restIter = this.getStraightJoin(p.minusFirst()).iterator();
                while (restIter.hasNext()) {
                    final Where w = restIter.next();
                    res.add(where.and(w));
                }
            }
            return res;
        }
    }

    public Where getJointure(int ID, Path p) {
        return new Where(p.getFirst().getKey(), "=", ID).and(this.getJointure(p));
    }

    public synchronized GraFFF cloneForFilter(List<SQLTable> linksToRemove) {
        return GraFFF.create(this.getGraphP(), linksToRemove);
    }

    public synchronized GraFFF cloneForFilterKeep(Set<SQLField> linksToKeep) {
        return GraFFF.createKeep(this.getGraphP(), linksToKeep);
    }

    protected DirectedMultigraph<SQLTable, Link> getGraphP() {
        return (DirectedMultigraph<SQLTable, Link>) this.getGraph();
    }

    static public <C extends Collection<String>> C getNames(Collection<Link> links, C fields) {
        for (final Link l : links)
            fields.add(l.getLabel().getName());
        return fields;
    }

    static public Set<String> getNames(Collection<Link> links) {
        return getNames(links, new HashSet<String>());
    }

    /**
     * Add the labels of <code>links</code> to the passed collection.
     * 
     * @param <C> type of collection.
     * @param links a Collection of Link.
     * @param fields a collection where labels are added.
     * @return <code>fields</code>.
     */
    static public <C extends Collection<SQLField>> C getLabels(Collection<Link> links, C fields) {
        for (final Link l : links)
            fields.add(l.getLabel());
        return fields;
    }

    static public Set<SQLField> getLabels(Collection<Link> links) {
        return getLabels(links, new HashSet<SQLField>());
    }

    static public <C extends Collection<List<SQLField>>> C getCols(Collection<Link> links, C fields) {
        for (final Link l : links)
            fields.add(l.getFields());
        return fields;
    }

    static public Set<List<SQLField>> getCols(Collection<Link> links) {
        return getCols(links, new HashSet<List<SQLField>>());
    }

}