/*
* 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.domainmodel;
import java.io.File;
import java.io.FilenameFilter;
import java.util.ArrayList;
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 org.apache.log4j.Logger;
import net.sf.jailer.datamodel.Association;
import net.sf.jailer.datamodel.DataModel;
import net.sf.jailer.datamodel.Table;
import net.sf.jailer.util.CsvFile;
import net.sf.jailer.util.CsvFile.Line;
import net.sf.jailer.util.PrintUtil;
/**
* Partitioning of the entire data model
* into smaller sets of functionally related tables.
*
* The definition of a domain model helps you to
* define the restriction and extraction models.
*
* @author Ralf Wisser
*/
public class DomainModel {
/**
* Named domains.
*/
Map<String, Domain> domains = new TreeMap<String, Domain>();
/**
* The data model.
*/
private final DataModel dataModel;
/**
* Maps each table to the composite where the table is the main table.
*/
public final Map<Table, Composite> composites;
/**
* The logger.
*/
private static final Logger _log = Logger.getLogger(DomainModel.class);
/**
* Constructor.
*
* Loads the domain definitions in directory 'domainmodel'.
*/
public DomainModel(DataModel dataModel) throws Exception {
this.dataModel = dataModel;
composites = new TreeMap<Table, Composite>();
loadComposites();
// load files
Map<String, CsvFile> csvFiles = new HashMap<String, CsvFile>();
File domainModelDirectory = new File("domainmodel");
if (!domainModelDirectory.exists() || !domainModelDirectory.isDirectory()) {
return;
}
for (File domainFile: domainModelDirectory.listFiles(new FilenameFilter() {
public boolean accept(File dir, String name) {
return !"composites.csv".equalsIgnoreCase(name) && name.toLowerCase().endsWith(".csv");
}
})) {
_log.info("loading domain " + domainFile);
CsvFile csvFile = new CsvFile(domainFile);
if (csvFile.getLines().isEmpty()) {
throw new RuntimeException("empty domain file: " + domainFile);
}
String domainName = domainFile.getName().substring(0, domainFile.getName().length() - 4);
if (domainName.length() == 0) {
throw new RuntimeException("no domain name in: " + domainFile);
}
csvFiles.put(domainName, csvFile);
}
// create domains
for (String domainName: csvFiles.keySet()) {
Set<Table> tables = new TreeSet<Table>();
CsvFile csvFile = csvFiles.get(domainName);
for (int line = 1; line < csvFile.getLines().size(); ++line) {
Line csvLine = csvFile.getLines().get(line);
String tableName = csvLine.cells.get(0);
Table table = dataModel.getTable(tableName);
if (table == null) {
throw new RuntimeException(csvLine.location + ": unknown table '" + tableName + "'");
}
if (composites.get(table) == null) {
throw new RuntimeException(csvLine.location + ": table '" + tableName + "' is component of " + getComposite(table) + ".");
} else {
tables.addAll(composites.get(table).componentTables);
}
tables.add(table);
}
Domain domain = new Domain(domainName, tables);
domains.put(domainName, domain);
}
// link domains
for (String domainName: csvFiles.keySet()) {
Domain domain = domains.get(domainName);
Line containsLine = csvFiles.get(domainName).getLines().get(0);
for (String subDomainName: containsLine.cells) {
if (subDomainName != null && subDomainName.length() > 0) {
Domain subDomain = domains.get(subDomainName);
if (subDomain == null) {
throw new RuntimeException(containsLine.location + ": unknown domain '" + subDomainName + "'");
}
domain.subDomains.add(subDomain);
subDomain.superDomains.add(domain);
}
}
}
}
/**
* Loads the composites definition file.
*/
private void loadComposites() throws Exception {
File compositesDefinition = new File("domainmodel" + File.separator + "composites.csv");
Map<Table, CsvFile.Line> compositeDefinitionOfTable = new HashMap<Table, Line>();
if (compositesDefinition.exists()) {
CsvFile compostitesCsvFile = new CsvFile(compositesDefinition);
for (CsvFile.Line line: compostitesCsvFile.getLines()) {
Table table = dataModel.getTable(line.cells.get(0));
if (table == null) {
throw new RuntimeException("unknown table '" + line.cells.get(0) + "'");
}
if (compositeDefinitionOfTable.containsKey(table)) {
throw new RuntimeException("duplicate composite definition for '" + line.cells.get(0) + "'");
}
compositeDefinitionOfTable.put(table, line);
}
} else {
_log.info("no composites definition (" + compositesDefinition + ")");
}
Set<Table> allComponents = new HashSet<Table>();
for (Table table: dataModel.getTables()) {
CsvFile.Line line = compositeDefinitionOfTable.get(table);
if (line != null) {
allComponents.add(table);
List<Table> components = new ArrayList<Table>();
for (int i = 1; line.cells.get(i).length() > 0; ++i) {
Table component = dataModel.getTable(line.cells.get(i));
if (component == null) {
throw new RuntimeException("unknown table '" + line.cells.get(i) + "'");
}
allComponents.add(component);
components.add(component);
}
composites.put(table, new Composite(table, components));
}
}
for (Table table: dataModel.getTables()) {
if (!allComponents.contains(table)) {
composites.put(table, new Composite(table, new ArrayList<Table>()));
}
}
}
/**
* Gets all domains.
*
* @return a map from domain names to domains
*/
public Map<String, Domain> getDomains() {
return domains;
}
/**
* Gets domain of table.
*
* @param table the table
* @return domain of table or <code>null</code>, if table belongs to no domain
*/
public Domain getDomain(Table table) {
for (Domain domain: domains.values()) {
if (domain.tables.contains(table)) {
return domain;
}
}
return null;
}
/**
* Cache for {@link #getComposite(Table)}.
*/
private Map<Table, Composite> componentCache = new HashMap<Table, Composite>();
/**
* Gets composite in which a given table is contained.
*
* @param table the table
* @return composite in which a table is contained
*/
public Composite getComposite(Table table) {
if (componentCache.containsKey(table)) {
return componentCache.get(table);
}
for (Composite composite: composites.values()) {
if (composite.mainTable.equals(table) || composite.componentTables.contains(table)) {
componentCache.put(table, composite);
return composite;
}
}
return null;
}
/**
* Checks model invariants:
* <ul>
* <li>two different domains are disjoint.
* <li>the union of all domains is the set of all tables.
* </ul>
*
* @return <code>true</code> if no errors are found
*/
public boolean check() {
Set<Table> withoutDomain = new TreeSet<Table>();
for (Table table: dataModel.getTables()) {
Set<Domain> domainsOfTable = new TreeSet<Domain>();
for (Domain domain: domains.values()) {
if (domain.tables.contains(table)) {
domainsOfTable.add(domain);
}
}
if (domainsOfTable.size() == 0) {
withoutDomain.add(table);
}
if (domainsOfTable.size() > 1) {
printError("Table '" + table.getName() + "' in more than 1 domain: " + domainsOfTable);
}
}
if (withoutDomain.size() > 0) {
warn("Tables without domain: " + new PrintUtil().tableSetAsString(withoutDomain, " "));
}
// escape analysis
for (Domain domain: domains.values()) {
for (Table table: domain.tables) {
for (Association association: table.associations) {
if (!association.isIgnored()) {
Domain destinationDomain = getDomain(association.destination);
if (destinationDomain == null || (!destinationDomain.equals(domain) && !destinationDomain.isSubDomainOf(domain))) {
String associationName = association.source.getName() + "->" + association.destination.getName();
warn((association.isInsertDestinationBeforeSource()? "dependency '" : "association '") + associationName + "' deserts " + domain + (destinationDomain != null? " to " + destinationDomain : ""));
}
}
}
}
}
System.out.println(numberOfErrors + " errors");
System.out.println(numberOfWarnings + " warnings");
return numberOfErrors == 0;
}
private int numberOfWarnings = 0;
private int numberOfErrors = 0;
/**
* Prints warning message.
*
* @param message the message
*/
private void warn(String message) {
System.out.println("warning: " + message);
++numberOfWarnings;
}
/**
* Prints error message.
*
* @param message the message
*/
private void printError(String message) {
System.out.println("error: " + message);
++numberOfErrors;
}
/**
* Stringifies the model.
*/
public String toString() {
StringBuffer modelAsString = new StringBuffer("domain model:\n");
for (Domain domain: domains.values()) {
if (domain.superDomains.isEmpty()) {
modelAsString.append(domainTree(domain, 0, domains.size() + 2));
}
}
modelAsString.append("\n");
for (Domain domain: domains.values()) {
modelAsString.append(domain.name + " = " + new PrintUtil().tableSetAsString(domain.tables, " ") + "\n");
}
return modelAsString.toString();
}
/**
* Stringifies a domain with sub-domains.
*
* @param domain the domain
* @param maxLevel max indent level
*/
private StringBuffer domainTree(Domain domain, int indent, int maxLevel) {
if (indent > maxLevel) {
throw new RuntimeException("cyclic domain containment: '" + domain.name + "'");
}
StringBuffer domainAsString = new StringBuffer();
for (int i = 0; i < indent; ++i) {
domainAsString.append(" ");
}
domainAsString.append(domain.name + "\n");
for (Domain subDomain: domain.subDomains) {
domainAsString.append(domainTree(subDomain, indent + 1, maxLevel));
}
return domainAsString;
}
}