/* * 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.modelbuilder; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; 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.Set; import java.util.TreeMap; import javax.sql.DataSource; import org.apache.log4j.Logger; import net.sf.jailer.ExecutionContext; import net.sf.jailer.configuration.DBMS; import net.sf.jailer.database.Session; import net.sf.jailer.datamodel.Association; import net.sf.jailer.datamodel.Cardinality; import net.sf.jailer.datamodel.Column; import net.sf.jailer.datamodel.DataModel; import net.sf.jailer.datamodel.PrimaryKeyFactory; import net.sf.jailer.datamodel.Table; import net.sf.jailer.importfilter.ImportFilterManager; import net.sf.jailer.util.CsvFile; import net.sf.jailer.util.CsvFile.Line; import net.sf.jailer.util.Quoting; import net.sf.jailer.util.SqlUtil; /** * Automatically builds a data-model using a list of {@link ModelElementFinder}. * * Writes all model elements into the files * <ul> * <li>model-builder-table.csv<li> * <li>model-builder-association.csv<li> * <ul> * except the already known elements (table.csv/association.csv) * and the excluded elements listed in exclude-[tables|associations].csv * * @author Ralf Wisser */ public class ModelBuilder { /** * The logger. */ private static final Logger _log = Logger.getLogger(ModelBuilder.class); /** * The statement executor for executing SQL statements. */ private static Session session; /** * Name of CSV file for generated table definitions. */ public static String getModelBuilderTablesFilename(ExecutionContext executionContext) { return DataModel.getDatamodelFolder(executionContext) + File.separator + "model-builder-table.csv"; } /** * Name of CSV file for generated column definitions. */ public static String getModelBuilderColumnsFilename(ExecutionContext executionContext) { return DataModel.getDatamodelFolder(executionContext) + File.separator + "model-builder-column.csv"; } /** * Name of CSV file for generated association definitions. */ public static String getModelBuilderAssociationsFilename(ExecutionContext executionContext) { return DataModel.getDatamodelFolder(executionContext) + File.separator + "model-builder-association.csv"; } /** * The exclude-tables file. */ private static CsvFile getExcludeTablesCSV(ExecutionContext executionContext) { try { File exTFile = new File(DataModel.getDatamodelFolder(executionContext) + File.separator + "exclude-tables.csv"); if (!exTFile.exists()) { exTFile.createNewFile(); } return new CsvFile(exTFile); } catch (Exception e) { throw new RuntimeException(e); } } /** * The exclude-associations file. */ private static CsvFile getExcludeAssociationsCSV(ExecutionContext executionContext) { try { File exAFile = new File(DataModel.getDatamodelFolder(executionContext) + File.separator + "exclude-associations.csv"); if (!exAFile.exists()) { exAFile.createNewFile(); } return new CsvFile(exAFile); } catch (Exception e) { throw new RuntimeException(e); } } /** * Builds and merges model. * * @param warnings string-buffer to print warnings into, may be <code>null</code> */ public static void buildAndMerge(DataSource dataSource, DBMS dbms, String schema, StringBuffer warnings, ExecutionContext executionContext) throws Exception { build(dataSource, dbms, schema, warnings, executionContext); merge(getModelBuilderTablesFilename(executionContext), DataModel.getTablesFile(executionContext), 0, TABLE_HEADER); merge(getModelBuilderAssociationsFilename(executionContext), DataModel.getAssociationsFile(executionContext), 5, ASSOC_HEADER); merge(getModelBuilderColumnsFilename(executionContext), DataModel.getColumnsFile(executionContext), 0, COLUMN_HEADER); cleanUp(executionContext); } private static void merge(String sourceFile, String destFile, int keyColumn, String header) throws Exception { CsvFile source = new CsvFile(new File(sourceFile)); CsvFile dest = new CsvFile(new File(destFile)); Map<String, Line> destLines = new TreeMap<String, CsvFile.Line>(); for (Line line: dest.getLines()) { destLines.put(line.cells.get(keyColumn), line); } StringBuilder result = new StringBuilder(); for (Line line: source.getLines()) { String key = line.cells.get(keyColumn); destLines.remove(key); result.append(line.toString() + "\n"); } for (Line line: destLines.values()) { result.append(line.toString() + "\n"); } writeFile(destFile, header + result.toString()); } /** * Builds model. * * @param warnings string-buffer to print warnings into, may be <code>null</code> */ public static void build(DataSource dataSource, DBMS dbms, String schema, StringBuffer warnings, ExecutionContext executionContext) throws Exception { session = new Session(dataSource, dbms); session.setIntrospectionSchema(schema); resetFiles(executionContext); DataModel dataModel = new DataModel(executionContext); Collection<Table> tables = new ArrayList<Table>(); ModelElementFinder finder = new JDBCMetaDataBasedModelElementFinder(); _log.info("find tables with " + finder); tables.addAll(finder.findTables(session, executionContext)); Collection<Table> allTables = new ArrayList<Table>(tables); Set<Table> written = new HashSet<Table>(); for (Iterator<Table> iT = tables.iterator(); iT.hasNext(); ) { Table table = iT.next(); if (/* dataModel.getTable(table.getName()) != null || */ written.contains(table)) { iT.remove(); } else { written.add(table); } } String tableDefinitions = ""; List<Table> sortedTables = new ArrayList<Table>(tables); Collections.sort(sortedTables, new Comparator<Table>() { public int compare(Table t1, Table t2) { return t1.getName().compareTo(t2.getName()); } }); Map<Table, List<Column>> columnPerTable = new HashMap<Table, List<Column>>(); Quoting quoting = new Quoting(session); StringBuilder columnsDefinition = new StringBuilder(); CsvFile excludeTablesCSV = getExcludeTablesCSV(executionContext); for (Table table: allTables) { if (!isJailerTable(table, quoting) && !excludeTablesCSV.contains(new String[] { table.getName()}) && !excludeTablesCSV.contains(new String[] { table.getName().toUpperCase() })) { _log.info("find colums with " + finder); List<Column> columns = finder.findColumns(table, session, executionContext); if (!columns.isEmpty()) { columnPerTable.put(table, columns); columnsDefinition.append(CsvFile.encodeCell(table.getName()) + "; "); for (Column c: columns) { columnsDefinition.append(CsvFile.encodeCell(c.toSQL(null) + (c.isIdentityColumn? " identity" : "") + (c.isVirtual? " virtual" : "") + (c.isNullable? " null" : "")) + "; "); } columnsDefinition.append("\n"); } } } resetColumnsFile(columnsDefinition.toString(), executionContext); for (Table table: sortedTables) { if (!isJailerTable(table, quoting) && !excludeTablesCSV.contains(new String[] { table.getName()}) && !excludeTablesCSV.contains(new String[] { table.getName().toUpperCase() })) { if (table.primaryKey.getColumns().isEmpty()) { // try find user defined pk Table old = dataModel.getTable(table.getName()); if (old != null && !old.primaryKey.getColumns().isEmpty() && columnPerTable.containsKey(table)) { List<Column> newPk = new ArrayList<Column>(); for (Column c: columnPerTable.get(table)) { for (Column opk: old.primaryKey.getColumns()) { if (c.name.equals(opk.name)) { newPk.add(c); break; } } } if (newPk.size() == old.primaryKey.getColumns().size()) { table = new Table(old.getName(), new PrimaryKeyFactory().createPrimaryKey(newPk), false, false); table.setAuthor(old.getAuthor()); } } String warning = "Table '" + table.getName() + "' has no primary key"; if (table.primaryKey.getColumns().size() == 0) { warnings.append(warning + "\n"); } else { warning += ", taking manually defined key."; } _log.warn(warning); } tableDefinitions += CsvFile.encodeCell(table.getName()) + "; N; "; for (Column pk: table.primaryKey.getColumns()) { tableDefinitions += CsvFile.encodeCell(pk.toString()) + ";"; } tableDefinitions += " ;" + CsvFile.encodeCell(table.getAuthor()) + ";\n"; } } resetTableFile(tableDefinitions, executionContext); // re-read data model with new tables dataModel = new DataModel(getModelBuilderTablesFilename(executionContext), getModelBuilderAssociationsFilename(executionContext), new HashMap<String, String>(), assocFilter, executionContext); Collection<Association> associations = new ArrayList<Association>(); Map<Association, String[]> namingSuggestion = new HashMap<Association, String[]>(); _log.info("find associations with " + finder); associations.addAll(finder.findAssociations(dataModel, namingSuggestion, session, executionContext)); Collection<Association> associationsToWrite = new ArrayList<Association>(); CsvFile excludeAssociationsCSV = getExcludeAssociationsCSV(executionContext); for (Association association: associations) { if (!excludeAssociationsCSV.contains(new String[] { association.source.getName(), association.destination.getName(), null, association.getJoinCondition() })) { if (!contains(association, dataModel)) { insert(association, dataModel); associationsToWrite.add(association); } } } String associationDefinition = ""; Set<String> names = new HashSet<String>(); for (Table table: dataModel.getTables()) { for (Association association: table.associations) { if (association.getName() != null) { names.add(association.getName()); } } } for (Association association: associationsToWrite) { String firstInsert = " "; if (association.isInsertSourceBeforeDestination()) { firstInsert = "A"; } if (association.isInsertDestinationBeforeSource()) { firstInsert = "B"; } String card = " "; if (association.getCardinality() != null) { card = association.getCardinality().toString(); } String sep = "_to_"; if (association.source.getName().charAt(0) >= 'A' && association.source.getName().charAt(0) <= 'Z') { sep = "_TO_"; } String name = association.source.getName() + sep + association.destination.getName(); if (namingSuggestion.containsKey(association)) { for (String nameSuggestion: namingSuggestion.get(association)) { name = nameSuggestion; if (!names.contains(name)) { break; } } } if (names.contains(name)) { for (int i = 1; ; ++i) { String nameWithSuffix = name + "_" + i; if (!names.contains(nameWithSuffix)) { name = nameWithSuffix; break; } } } names.add(name); associationDefinition += CsvFile.encodeCell(association.source.getName()) + "; " + CsvFile.encodeCell(association.destination.getName()) + "; " + firstInsert + "; " + card + "; " + CsvFile.encodeCell(association.getJoinCondition()) + "; " + CsvFile.encodeCell(name) + "; " + CsvFile.encodeCell(association.getAuthor()) + ";\n"; } resetAssociationFile(associationDefinition, executionContext); } private static String ASSOC_HEADER = "# generated by Jailer\n\n" + "# Table A; Table B; first-insert; cardinality (opt); join-condition; name; author\n"; private static void resetAssociationFile(String associationDefinition, ExecutionContext executionContext) throws IOException { writeFile(getModelBuilderAssociationsFilename(executionContext), ASSOC_HEADER + associationDefinition); } private static String TABLE_HEADER = "# generated by Jailer\n\n" + "# Name; upsert; primary key; ; author\n"; private static void resetTableFile(String tableDefinitions, ExecutionContext executionContext) throws IOException { writeFile(getModelBuilderTablesFilename(executionContext), TABLE_HEADER + tableDefinitions); } private static String COLUMN_HEADER = "# generated by Jailer\n\n" + "# Table; columns\n"; private static void resetColumnsFile(String columnsDefinitions, ExecutionContext executionContext) throws IOException { writeFile(getModelBuilderColumnsFilename(executionContext), COLUMN_HEADER + columnsDefinitions); } /** * Inserts an association into a model. * * @param association the association * @param dataModel the model */ private static void insert(Association association, DataModel dataModel) { Association associationA = association; Cardinality reversedCard = association.getCardinality(); if (reversedCard != null) { reversedCard = reversedCard.reverse(); } Association associationB = new Association(association.destination, association.destination, association.isInsertSourceBeforeDestination(), association.isInsertSourceBeforeDestination(), association.getJoinCondition(), dataModel, true, reversedCard); associationA.reversalAssociation = associationB; associationB.reversalAssociation = associationA; associationA.source.associations.add(associationA); associationB.source.associations.add(associationB); } /** * Checks if table is one of Jailers working tables. * * @param table the table to check * @param quoting * @return <code>true</code> if table is one of Jailers working tables */ private static boolean isJailerTable(Table table, Quoting quoting) { String tName = quoting.unquote(table.getUnqualifiedName()).toUpperCase(); return SqlUtil.JAILER_TABLES.contains(tName) || tName.startsWith(ImportFilterManager.MAPPINGTABLE_NAME_PREFIX) || (tName.endsWith("_T") && SqlUtil.JAILER_TABLES.contains(tName.substring(0, tName.length() - 2))); } /** * Checks if an association is already in a model. * * @param association the association * @param dataModel the model * @return <code>true</code> iff association is already in model */ private static boolean contains(Association association, DataModel dataModel) { for (Association a: association.source.associations) { if (a.source.equals(association.source)) { if (a.destination.equals(association.destination)) { if (a.isInsertDestinationBeforeSource() || !association.isInsertDestinationBeforeSource()) { if (a.isInsertSourceBeforeDestination() || !association.isInsertSourceBeforeDestination()) { if (a.getJoinCondition().equals(association.getJoinCondition())) { return true; } } } } } } return false; } /** * Writes content into a file. * * @param content the content * @param fileName the name of the file */ private static void writeFile(String fileName, String content) throws IOException { File f = new File(fileName); if (!f.exists()) { f.getParentFile().mkdirs(); f.createNewFile(); } PrintWriter out = new PrintWriter(new FileOutputStream(fileName)); out.print(content); out.close(); _log.info("file '" + fileName + "' written"); } /** * Resets 'model-builder-*.csv' files. */ public static void resetFiles(ExecutionContext executionContext) throws IOException { resetTableFile("", executionContext); resetAssociationFile("", executionContext); } /** * Removes temporary files. */ public static void cleanUp(ExecutionContext executionContext) { File f = new File(getModelBuilderTablesFilename(executionContext)); if (f.exists()) { f.delete(); _log.info("File '" + f.getAbsolutePath() + "' removed"); } f = new File(getModelBuilderAssociationsFilename(executionContext)); if (f.exists()) { f.delete(); _log.info("File '" + f.getAbsolutePath() + "' removed"); } f = new File(getModelBuilderColumnsFilename(executionContext)); if (f.exists()) { f.delete(); _log.info("File '" + f.getAbsolutePath() + "' removed"); } } public static CsvFile.LineFilter assocFilter = null; }