/*
* Copyright 2007 - 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.sf.jailer.util;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import net.sf.jailer.datamodel.Association;
import net.sf.jailer.datamodel.DataModel;
import net.sf.jailer.datamodel.Table;
/**
* Finds dependency cycles.
*
* @author Ralf Wisser
*/
public class CycleFinder {
/**
* Path from n table <code>from</code> to another table <code>to</code>.
* Concatenation of two paths left/right or an edge, if left and right is null.
*/
public static class Path {
final Table from;
final Table to;
final Path left;
final Path right;
final int birthday;
final int length;
Path(Table from, Table to, Path left, Path right, int birthday) {
this.from = from;
this.to = to;
this.left = left;
this.right = right;
this.birthday = birthday;
this.length = left == null? 1 : (left.length + right.length);
}
public boolean equals(Object other) {
if (!(other instanceof Path)) {
return false;
}
Path op = (Path) other;
if (from != op.from || to != op.to || length != op.length) {
return false;
}
Set<Table> p1 = new TreeSet<Table>();
Set<Table> p2 = new TreeSet<Table>();
fillSet(p1);
op.fillSet(p2);
return p1.equals(p2);
}
public int hashCode() {
return 1;
}
public void fillPath(List<Table> path) {
if (left == null) {
path.add(from);
path.add(to);
} else {
left.fillPath(path);
path.remove(path.size() - 1);
right.fillPath(path);
}
}
private void fillSet(Set<Table> set) {
if (left == null) {
set.add(from);
if (to != from) {
set.add(to);
}
} else {
left.fillSet(set);
right.fillSet(set);
}
}
public String toString() {
return "{" + (left == null? from.getName() + "->" + to.getName() : (left + "-->" + right)) + " " + birthday + "/" + length + "}";
}
}
/**
* Finds all dependency cycles in a data model.
*
* @param dataModel the data model
* @return all cycles in the data model
*/
public static Collection<Path> findCycle(DataModel dataModel, Collection<Table> tables) {
Collection<Path> allCycles = new ArrayList<Path>();
Set<Pair<Table, Table>> tabu = new HashSet<Pair<Table,Table>>();
for (;;) {
tables = getCycle(tables, tabu);
Map<Table, List<Path>> fromToPaths = new TreeMap<Table, List<Path>>();
for (Table table: tables) {
List<Path> pathList = new ArrayList<Path>(10);
fromToPaths.put(table, pathList);
}
for (Table table: tables) {
for (Association association: table.associations) {
// if (!association.isIgnored()) {
if (association.isInsertDestinationBeforeSource()) {
if (tables.contains(association.destination)) {
Path path = new Path(association.source, association.destination, null, null, 0);
fromToPaths.get(path.from).add(path);
}
}
// }
}
}
List<Path> newPaths = new ArrayList<Path>();
Set<Table> tSet = new TreeSet<Table>();
for (int today = 1; ; ++today) {
newPaths.clear();
int yesterday = today - 1;
for (Map.Entry<Table, List<Path>> e: fromToPaths.entrySet()) {
for (Path path: e.getValue()) {
CancellationHandler.checkForCancellation(null);
if (path.from != path.to) {
List<Path> list = fromToPaths.get(path.to);
if (list != null) {
for (Path toAppend: list) {
if (toAppend.from != toAppend.to) {
if (toAppend.birthday == yesterday) {
Path newPath = new Path(path.from, toAppend.to, path, toAppend, today);
if (!fromToPaths.get(newPath.from).contains(newPath)) {
int aSize;
if (newPath.from == newPath.to) {
aSize = newPath.length;
} else {
aSize = newPath.length + 1;
}
tSet.clear();
newPath.fillSet(tSet);
if (tSet.size() == aSize) {
newPaths.add(newPath);
}
}
}
}
}
}
}
}
}
if (newPaths.isEmpty()) {
break;
} else {
for (Path path: newPaths) {
fromToPaths.get(path.from).add(path);
}
}
boolean cycFound = false;
for (List<Path> pList: fromToPaths.values()) {
for (Path path: pList) {
if (path.from == path.to) {
cycFound = true;
break;
}
}
if (cycFound) {
break;
}
}
if (cycFound) {
break;
}
}
Map<Set<Table>, Path> cycles = new HashMap<Set<Table>, Path>();
for (List<Path> pList: fromToPaths.values()) {
CancellationHandler.checkForCancellation(null);
for (Path path: pList) {
if (path.from == path.to) {
List<Table> pl = new ArrayList<Table>();
path.fillPath(pl);
for (int i = 0; i < pl.size(); ++i) {
tabu.add(new Pair<Table, Table>(pl.get(i), pl.get((i + 1) % pl.size())));
}
Set<Table> taSet = new TreeSet<Table>();
path.fillSet(taSet);
cycles.put(taSet, path);
}
}
}
allCycles.addAll(cycles.values());
if (cycles.isEmpty()) {
break;
}
}
return allCycles;
}
/**
* Gets set of all tables involved in a cycle.
*
* @param tables all tables
* @return subset of <code>tables</code> involved in a cycle
*/
public static Set<Table> getCycle(Collection<Table> tables) {
return getCycle(tables, new HashSet<Pair<Table,Table>>());
}
/**
* Gets set of all tables involved in a cycle.
*
* @param tables all tables
* @return subset of <code>tables</code> involved in a cycle
*/
private static Set<Table> getCycle(Collection<Table> tables, Set<Pair<Table, Table>> tabu) {
Set<Table> cycle = new TreeSet<Table>(tables);
for (;;) {
Set<Table> notInCycle = new TreeSet<Table>();
for (Table table: cycle) {
boolean hasIn = false;
boolean hasOut = false;
for (Association association: table.associations) {
if (cycle.contains(association.destination)) {
if (association.isInsertSourceBeforeDestination()) {
if (!tabu.contains(new Pair<Table, Table>(association.destination, association.source))) {
hasOut = true;
}
}
if (association.isInsertDestinationBeforeSource()) {
if (!tabu.contains(new Pair<Table, Table>(association.source, association.destination))) {
hasIn = true;
}
}
}
}
if (!(hasIn && hasOut)) {
notInCycle.add(table);
}
}
if (notInCycle.isEmpty()) {
break;
} else {
cycle.removeAll(notInCycle);
}
}
return cycle;
}
}