/* * 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.render; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintWriter; import java.sql.SQLException; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Set; import org.apache.log4j.Logger; import net.sf.jailer.ExecutionContext; import net.sf.jailer.datamodel.Association; import net.sf.jailer.datamodel.Column; import net.sf.jailer.datamodel.DataModel; import net.sf.jailer.datamodel.Table; import net.sf.jailer.domainmodel.Composite; import net.sf.jailer.domainmodel.Domain; import net.sf.jailer.domainmodel.DomainModel; import net.sf.jailer.util.CancellationException; import net.sf.jailer.util.CancellationHandler; import net.sf.jailer.util.PrintUtil; import net.sf.jailer.util.SqlUtil; /** * Generates a human readable HTML-representation of the data-model. * * @author Ralf Wisser */ public class HtmlDataModelRenderer implements DataModelRenderer { private static final String COLOR_KEYWORDS = "font-style: italic; color: rgb(120, 0, 0);"; /** * The directory to put the HTML-render in. */ private String outputFolder; /** * Maximum depth of expansion on table render. */ private int maxDepth; /** * The logger. */ private static final Logger _log = Logger.getLogger(HtmlDataModelRenderer.class); /** * The execution context. */ private ExecutionContext executionContext; /** * Constructor. * * @param outputDir the directory to put the HTML-render in * @param maxDepth maximum depth of expansion on table render */ // TODO: remove public HtmlDataModelRenderer(String outputDir, int maxDepth) { this.outputFolder = outputDir; this.maxDepth = maxDepth; } /** * Constructor. */ public HtmlDataModelRenderer() { } /** * @return the outputDir */ public String getOutputFolder() { return outputFolder; } /** * @param outputFolder the outputDir to set */ public void setOutputFolder(String outputFolder) { this.outputFolder = outputFolder; } /** * @return the maxDepth */ public int getMaxDepth() { return maxDepth; } /** * @param maxDepth the maxDepth to set */ public void setMaxDepth(int maxDepth) { this.maxDepth = maxDepth; } /** * Generates a human readable HTML-representation of the data-model. * * @param dataModel the data-model */ public void render(DataModel dataModel, ExecutionContext executionContext, List<String> restrictionFiles) { this.executionContext = executionContext; try { List<Table> tableList = new ArrayList<Table>(dataModel.getTables()); Collections.sort(tableList); List<String> tablesColumn = new ArrayList<String>(); List<String> domainsColumn = new ArrayList<String>(); DomainModel domainModel = new DomainModel(dataModel); for (Table table: tableList) { Composite composite = domainModel.composites.get(table); Domain domain = domainModel.getDomain(table); if (composite != null) { tablesColumn.add(linkTo(table)); domainsColumn.add(domain == null? "" : "   <small>" + linkTo(domain) + "</small>"); } StringBuffer legend = new StringBuffer(); String closure = renderClosure(domainModel, composite == null? domainModel.getComposite(table) : composite, legend); closure = new PrintUtil().applyTemplate("template" + File.separatorChar + "table.html", new Object[] { "Closure", "", closure }); String columns = generateColumnsTable(table); if (columns == null) { columns = ""; } else { columns += "<br>"; } String components = ""; if (composite != null && composite.componentTables.size() > 0) { components = generateComponentsTable(composite) + "<br>"; } String domainSuffix = ""; if (domain != null) { domainSuffix = " <small>(" + linkTo(domain) + ")</small>"; } String title = composite == null? "Component " + table.getName() : composite.toString(); writeFile(new File(outputFolder, toFileName(table)), new PrintUtil().applyTemplate("template" + File.separator + "tableframe.html", new Object[] { title, renderTableBody(table, table, 0, 1, new HashSet<Table>()), closure + legend, components + columns, domainSuffix })); CancellationHandler.checkForCancellation(null); } String restrictions = "none"; List<String> restrictionModels = restrictionFiles; if (!restrictionModels.isEmpty()) { restrictions = restrictionModels.toString(); restrictions = restrictions.substring(1, restrictions.length() - 1); } String domains = ""; if (!domainModel.getDomains().isEmpty()) { domains = renderDomainModel(domainModel) + "<br>"; } writeFile(new File(outputFolder, "index.html"), new PrintUtil().applyTemplate("template" + File.separatorChar + "index.html", new Object[] { new Date(), generateHTMLTable("Tables", null, tablesColumn, domainsColumn), restrictions, domains })); } catch (CancellationException e) { throw e; } catch (Exception e) { throw new RuntimeException(e); } } /** * Renders the closure of a composite. * * @param composite the composite * @return render of composite's closure */ private String renderClosure(DomainModel domainModel, Composite composite, StringBuffer legend) throws FileNotFoundException, IOException { StringBuffer lines = new StringBuffer(); int distance = 0; Set<Composite> closure = new HashSet<Composite>(); Set<Composite> associatedComposites = new HashSet<Composite>(); boolean printLegend = false; Domain domain = domainModel.getDomain(composite.mainTable); do { associatedComposites.clear(); if (distance == 0) { associatedComposites.add(composite); } else { for (Composite c: closure) { for (Association a: c.getAssociations()) { if (a.getJoinCondition() != null) { Composite destinationComposite = domainModel.getComposite(a.destination); if (!closure.contains(destinationComposite)) { if (!excludeFromClosure(a.destination)) { associatedComposites.add(destinationComposite); } } } } } } List<Composite> cl = new ArrayList<Composite>(associatedComposites); Collections.sort(cl, new Comparator<Composite>() { public int compare(Composite o1, Composite o2) { return o1.mainTable.compareTo(o2.mainTable); } }); StringBuffer ts = new StringBuffer(); boolean firstTime = true; for (Composite dt: cl) { if (!firstTime) { ts.append(", "); } Domain dtDomain = domainModel.getDomain(dt.mainTable); boolean differentDomains = false; boolean subDomain = false; if (dtDomain == null && domain == null) { differentDomains = false; } else if (dtDomain == null || domain == null) { differentDomains = true; } else { differentDomains = !domain.equals(dtDomain); subDomain = dtDomain.isSubDomainOf(domain); } if (differentDomains && !subDomain) { ts.append("<span style=\"font-style: italic;\">"); printLegend = true; } ts.append(dt.equals(composite)? dt.mainTable.getName() : linkTo(dt.mainTable)); if (subDomain) { ts.append("*"); printLegend = true; } if (differentDomains && !subDomain) { ts.append("**</span>"); } firstTime = false; } if (!cl.isEmpty()) { lines.append(new PrintUtil().applyTemplate("template" + File.separator + "table_line.html", new Object[] { "", "  distance " + distance, "", " ", ts.toString(), COLOR_KEYWORDS, distance % 2 != 0? "class=\"highlightedrow\"" : "" })); } ++distance; closure.addAll(associatedComposites); } while (!associatedComposites.isEmpty()); if (printLegend) { legend.append(new PrintUtil().applyTemplate("template" + File.separatorChar + "legend.html", new Object[0])); } return lines.toString(); } protected boolean excludeFromClosure(Table table) { return false; } protected boolean excludeFromNeighborhood(Table table) { return false; } /** * Renders a table. * * @param table the table * @return render of table */ private String renderTableBody(Table table, Table current, int depth, int indent, Set<Table> alreadyRendered) throws FileNotFoundException, IOException { if (alreadyRendered.contains(table)) { return ""; } alreadyRendered.add(table); StringBuffer lines = new StringBuffer(); List<Association> all = new ArrayList<Association>(table.associations); Collections.sort(all, new Comparator<Association>() { public int compare(Association o1, Association o2) { return o1.destination.getName().compareTo(o2.destination.getName()); } }); List<Association> dep = new ArrayList<Association>(); List<Association> hasDep = new ArrayList<Association>(); List<Association> assoc = new ArrayList<Association>(); List<Association> ignored = new ArrayList<Association>(); for (Association association: all) { if (association.isIgnored()) { ignored.add(association); } else if (association.isInsertDestinationBeforeSource()) { dep.add(association); } else if (association.isInsertSourceBeforeDestination()) { hasDep.add(association); } else { assoc.add(association); } } int lineNr; String prefix = ""; String gap = "<small><small> <br></small></small>"; if (!dep.isEmpty()) { lines.append(tableRow(indent, "depends on")); lineNr = 0; for (Association association: dep) { if (!"".equals(association.toString())) { prefix = gap; lines.append(tableRow(indent, association, current, ++lineNr % 2 == 0)); } } } if (!hasDep.isEmpty()) { lines.append(tableRow(indent, prefix + "has dependent")); lineNr = 0; for (Association association: hasDep) { if (!"".equals(association.toString())) { prefix = gap; lines.append(tableRow(indent, association, current, ++lineNr % 2 == 0)); } } } if (!assoc.isEmpty()) { lines.append(tableRow(indent, prefix + "is associated with")); lineNr = 0; for (Association association: assoc) { if (!"".equals(assoc.toString())) { prefix = gap; lines.append(tableRow(indent, association, current, ++lineNr % 2 == 0)); } } } if (!ignored.isEmpty()) { lines.append(tableRow(indent, prefix + "ignored")); lineNr = 0; for (Association association: ignored) { if (!"".equals(association.toString())) { prefix = gap; lines.append(tableRow(indent, association, current, ++lineNr % 2 == 0)); } } } StringBuffer result = new StringBuffer(new PrintUtil().applyTemplate("template" + File.separator + "table.html", new Object[] { table.equals(current)? "Associations" : linkTo(table), indentSpaces(indent), lines.toString() })); if (depth < maxDepth) { if (depth == 0) { result.append("<br>" + new PrintUtil().applyTemplate("template" + File.separator + "table.html", new Object[] { "Neighborhood", indentSpaces(1), "" }) + "<br>"); } Set<Table> rendered = new HashSet<Table>(); boolean firstTime = true; for (Association association: all) { if (!rendered.contains(association.destination)) { if (!excludeFromNeighborhood(association.destination)) { String tableBody = renderTableBody(association.destination, current, depth + 1, indent + 1, alreadyRendered); if (tableBody.length() > 0) { if (!firstTime) { result.append("<br>"); } firstTime = false; } result.append(tableBody); rendered.add(association.destination); } } } } return result.toString(); } /** * Generates a row in the table render. * * @param indent the indentation * @return a row in the table render */ private String tableRow(int indent, String content) throws FileNotFoundException, IOException { return new PrintUtil().applyTemplate("template" + File.separator + "table_top_line.html", new Object[] { indentSpaces(indent), content, "", "", "", COLOR_KEYWORDS, "" }); } /** * Generates a row in the table render. * * @param indent the indentation * @param highlighted * @return a row in the table render */ private String tableRow(int indent, Association association, Table current, boolean highlighted) throws FileNotFoundException, IOException { String jc = association.renderJoinCondition("<span style=\"" + COLOR_KEYWORDS + "\">restricted by</span>"); String aliasA = "A", aliasB = "B"; if (!association.source.equals(association.destination)) { aliasA = association.source.getName(); aliasB = association.destination.getName(); } aliasA = linkTo(association.source, aliasA); aliasB = linkTo(association.destination, aliasB); jc = SqlUtil.replaceAliases(jc, aliasA, aliasB); return new PrintUtil().applyTemplate("template" + File.separator + "table_line.html", new Object[] { indentSpaces(indent), "  " + (association.destination.equals(current)? association.destination.getName() : linkTo(association.destination)), "  " + (association.getCardinality() != null? association.getCardinality() : ""), " on ", jc, "", highlighted? "class=\"highlightedrow\"" : "" }); } /** * Generates a HTML render of a table schema. * * @param table the table * @return HTML render of table schema */ private String generateColumnsTable(Table table) throws SQLException, FileNotFoundException, IOException { StringBuffer result = new StringBuffer(); int count = 0; for (Column column: table.getColumns()) { ++count; String COLUMN_NAME = column.name; boolean nullable = true; String type = "      " + column.toSQL(null).substring(column.name.length()).trim().replaceAll(" ", " "); String constraint = (!nullable ? "    <small>NOT NULL</small>" : ""); boolean isPK = false; for (Column c: table.primaryKey.getColumns()) { isPK = isPK || c.name.equalsIgnoreCase(COLUMN_NAME); } result.append(new PrintUtil().applyTemplate("template" + File.separator + "table_line.html", new Object[] { indentSpaces(1), "  " + COLUMN_NAME, type, "", constraint, isPK? COLOR_KEYWORDS : "", count % 2 == 0? "class=\"highlightedrow\"" : "" })); } return count == 0? null : (new PrintUtil().applyTemplate("template" + File.separatorChar + "table.html", new Object[] { "Columns", "", result.toString() })); } /** * Generates a HTML render of a components table. * * @param composite the composite * @return HTML render */ private String generateComponentsTable(Composite composite) throws SQLException, FileNotFoundException, IOException { List<String> componentNames = new ArrayList<String>(); for (Table component: composite.componentTables) { componentNames.add(linkTo(component)); } return generateHTMLTable("Components", null, componentNames, null); } /** * Generates a HTML table. */ private String generateHTMLTable(String title, List<Integer> indents, List<String> column1, List<String> column2) throws FileNotFoundException, IOException { StringBuffer result = new StringBuffer(); for (int i = 0; i < column1.size(); ++i) { result.append(new PrintUtil().applyTemplate("template" + File.separator + "table_line.html", new Object[] { "", indentSpaces(indents == null? 1 : indents.get(i)) + column1.get(i), column2 == null? "" : column2.get(i), "", "", "", i % 2 != 0? "class=\"highlightedrow\"" : "" })); } return column1.isEmpty()? null : (new PrintUtil().applyTemplate("template" + File.separatorChar + "table.html", new Object[] { title, "", result.toString() })); } /** * Generates a human readable HTML-representation of the domain-model. * * @param domainModel the domain model */ public String renderDomainModel(DomainModel domainModel) throws FileNotFoundException, IOException { List<String> column1 = new ArrayList<String>(); List<String> column2 = new ArrayList<String>(); List<Integer> indent = new ArrayList<Integer>(); Set<Domain> renderedDomains = new HashSet<Domain>(); for (Domain domain: domainModel.getDomains().values()) { if (domain.getSuperDomains().isEmpty()) { appendDomainTree(domain, column1, column2, 1, indent, domainModel.getDomains().size() + 2, domain.name, renderedDomains); } List<String> column = new ArrayList<String>(); for (Domain subDomain: domain.getSubDomains()) { column.add(linkTo(subDomain)); } String containsTable = generateHTMLTable("contains", null, column, null); String content = ""; if (containsTable != null) { content += containsTable + "<br>"; } column.clear(); for (Domain superDomain: domain.getSuperDomains()) { column.add(linkTo(superDomain)); } String partOfTable = generateHTMLTable("Part of", null, column, null); if (partOfTable != null) { content += partOfTable + "<br>"; } column.clear(); for (Table table: domain.tables) { if (domainModel.composites.get(table) != null) { column.add(linkTo(table)); } } String tablesTable = generateHTMLTable("Tables", null, column, null); if (tablesTable != null) { content += tablesTable + "<br>"; } writeFile(new File(outputFolder, domain.name + "_DOMAIN.html"), new PrintUtil().applyTemplate("template" + File.separator + "tableframe.html", new Object[] { "Domain " + domain.name, content, "", "", "" })); } return generateHTMLTable("Domains", indent, column1, column2); } /** * Puts lines into HTML render table for the domain model. * * @param domain root domain * @param column1 domain name * @param column2 domain description * @param level current indent level * @param indent indent level for each line * @param renderedDomains set of already rendered domains (to prevent duplicate rendering) */ private void appendDomainTree(Domain domain, List<String> column1, List<String> column2, int level, List<Integer> indent, int maxLevel, String path, Set<Domain> renderedDomains) { if (level > maxLevel) { throw new RuntimeException("cyclic domain containment: " + path); } String suffix = ""; if (renderedDomains.contains(domain) && !domain.getSubDomains().isEmpty()) { suffix = " ↑"; } column1.add(linkTo(domain) + suffix + "   "); column2.add("  <small>" + domain.tables.size() + " Tables</small>  "); indent.add(level); if (!renderedDomains.contains(domain)) { renderedDomains.add(domain); for (Domain subDomain: domain.getSubDomains()) { appendDomainTree(subDomain, column1, column2, level + 1, indent, maxLevel, path + "->" + subDomain.name, renderedDomains); } } } /** * Returns Space-string of given length. * * @param indent the lenght */ private String indentSpaces(int indent) { StringBuffer result = new StringBuffer(); for (int i = 1; i < indent; ++i) { for (int j = 0; j < 8; ++j) { result.append(" "); } } return result.toString(); } /** * Returns a HTML-hyper link to the render of a given domain. * * @param domain the domain * @return HTML-hyper link to the render of domain */ private String linkTo(Domain domain) { return "<a href=\"" + domain.name + "_DOMAIN.html\">" + domain.name + "</a>"; } /** * Returns a HTML-hyper link to the render of a given table. * * @param table the table * @return HTML-hyper link to the render of table */ private String linkTo(Table table) { return linkTo(table, table.getName()); } /** * Returns a HTML-hyper link to the render of a given table. * * @param table the table * @param name the name of the link * @return HTML-hyper link to the render of table */ private String linkTo(Table table, String name) { return "<a href=\"" + toFileName(table) + "\">" + name + "</a>"; } /** * Gets name of the file containing the HTML render of a given table. * * @param table the table * @return name of the file containing the HTML render of table */ public static String toFileName(Table table) { StringBuilder sb = new StringBuilder(); String tableName = table.getName(); String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_."; for (int i = 0; i < tableName.length(); ++i) { char c = tableName.charAt(i); if (chars.indexOf(c) >= 0) { sb.append(c); } } return sb.toString() + ".html"; } /** * Writes content into a file. * * @param content the content * @param file the file */ public static void writeFile(File file, String content) throws IOException { PrintWriter out = new PrintWriter(new FileOutputStream(file)); out.print(content); out.close(); _log.info("file '" + file + "' written"); } }