package org.jcommons.db.load.sort;
import static org.jcommons.lang.string.NamedString.message;
import java.sql.SQLException;
import java.util.*;
import javax.sql.DataSource;
import org.jcommons.db.load.meta.MetaTable;
import org.jcommons.io.sheet.Sheet;
import org.jcommons.lang.string.NamedString;
import org.jcommons.message.*;
/**
* A sorting strategy that lists the sheets in the order they depend upon.
*
* Can cope with cycle references but you need to lock at the error messages to know whether you can really load the
* sheets.
*
* @author Thorsten Goeckeler
*/
public class DependencySheetSorter
implements SheetSortingStrategy
{
private final Message errors = new Messages();
private DataSource dataSource;
private static final String CANNOT_ACCESS_TABLE =
"Cannot access table \"${table}\" to load the respective sheet due to: ${exception}.";
private static final String DEPENDS_ON = "Table \"${table}\" depends on table \"${master}\" which is not provided.";
private static final String DEPENDED_ON = "Table \"${table}\" depends on removed table \"${master}\".";
/** {@inheritDoc} */
@Override
public List<Sheet> sort(final List<Sheet> sheets) {
errors.clear();
if (sheets == null) return Collections.emptyList();
Map<String, Set<String>> dependends = new HashMap<String, Set<String>>();
Map<String, Set<String>> required = new HashMap<String, Set<String>>();
getDependencies(sheets, dependends, required);
Set<String> removed = getTablesWithoutMaster(sheets, required);
removeTablesWithoutMaster(dependends, required, removed);
// now we have only those tables left for which all dependencies can be resolved
return sort(sheets, required, dependends);
}
/**
* Sort the list of sheets in a sequence that allow them to be loaded.
*
* May remove sheets that cannot be loaded because their dependencies are missing.
*
* @param sheets the original list of sheets, never <code>null</code>
* @param required the map of tables and their required master tables
* @param dependends the map of tables and the tables they depend on
* @return the sorted list of sheets that can be loaded in that sequence
*/
private List<Sheet> sort(final List<Sheet> sheets, final Map<String, Set<String>> required,
final Map<String, Set<String>> dependends)
{
List<String> sequence = new LinkedList<String>();
// the algorithm is simple, as we assume that the sheets are loaded first with all non-nullable fields (which
// include primary keys and mandatory foreign keys) and then again with the remaining data including optional
// foreign keys. If those keys are missing, there was no corresponding entry in the respective sheet.
// while (there is a table entry in the required map)
// add all table entries to the list which require no other table
// remove these tables from the table map
// remove these tables in all requirement lists
List<String> removed = new LinkedList<String>();
while (!required.keySet().isEmpty()) {
removed.clear();
// add all tables that require no other table
for (String table : required.keySet()) {
if (required.get(table).isEmpty()) {
// no remaining master tables, it is safe to load this table
sequence.add(table);
removed.add(table);
}
}
// remove these tables from the table mappings
for (String table : removed) {
required.remove(table);
}
// now remove them from the remaining requirements lists
for (String table : required.keySet()) {
// works only if all tables are written in upper case
required.get(table).removeAll(removed);
}
}
removed = null;
// finally match the table list with the sheets
List<Sheet> sortedSheets = new LinkedList<Sheet>();
Map<String, Sheet> map = new HashMap<String, Sheet>();
for (Sheet sheet : sheets) {
map.put(sheet.getName().toUpperCase(), sheet);
}
for (String table : sequence) {
Sheet sheet = map.get(table);
// actually all sheets should be mapped, just a bit of paranoia at work
if (sheet != null) sortedSheets.add(sheet);
}
return sortedSheets;
}
private void removeTablesWithoutMaster(final Map<String, Set<String>> dependends,
final Map<String, Set<String>> required, final Set<String> removed)
{
// now remove those tables and check if we must remove more dependent ones until the list is empty
while (!removed.isEmpty()) {
for (String table : removed) {
dependends.remove(table.toUpperCase());
required.remove(table.toUpperCase());
}
removed.clear();
for (String table : required.keySet()) {
for (String requires : required.get(table)) {
if (!required.containsKey(requires)) {
NamedString text = message(DEPENDED_ON).with("table", table);
text.with("master", requires);
errors.add(new Fault(text.toString()));
removed.add(requires);
}
}
}
}
}
private Set<String> getTablesWithoutMaster(final List<Sheet> sheets, final Map<String, Set<String>> required) {
// check for tables that must be present
Set<String> removed = new HashSet<String>();
for (Sheet sheet : sheets) {
for (String requires : required.get(sheet.getName().toUpperCase())) {
if (!required.containsKey(requires)) {
NamedString text = message(DEPENDS_ON).with("table", sheet.getName().toUpperCase());
text.with("master", requires);
errors.add(new Fault(text.toString()));
removed.add(requires);
}
}
}
return removed;
}
private void getDependencies(final List<Sheet> sheets, final Map<String, Set<String>> dependends,
final Map<String, Set<String>> mandatory)
{
for (Sheet sheet : sheets) {
try {
dependends.put(sheet.getName().toUpperCase(), MetaTable.dependsOn(getDataSource(), sheet.getName()));
mandatory.put(sheet.getName().toUpperCase(), MetaTable.dependsMandatoryOn(getDataSource(), sheet.getName()));
} catch (SQLException ex) {
NamedString text = message(CANNOT_ACCESS_TABLE);
text.with("table", sheet.getName()).with("exception", ex.getMessage());
errors.add(new Fault(text.toString()));
dependends.remove(sheet.getName().toUpperCase());
mandatory.remove(sheet.getName().toUpperCase());
}
}
}
/** {@inheritDoc} */
@Override
public Message validate() {
return errors;
}
/** @return the currently used data source */
public DataSource getDataSource() {
return dataSource;
}
/**
* Inject the data source to be used to access the database.
*
* @param dataSource the database connection to use to load the data
*/
public void setDataSource(final DataSource dataSource) {
this.dataSource = dataSource;
}
}