/* * 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.datamodel; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Set; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import org.w3c.dom.Comment; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; import net.sf.jailer.database.Session; import net.sf.jailer.util.Quoting; import net.sf.jailer.util.SqlUtil; import net.sf.jailer.xml.NodeVisitor; import net.sf.jailer.xml.XmlUtil; /** * Describes a database-table. * * @author Ralf Wisser */ public class Table extends ModelElement implements Comparable<Table> { /** * The table-name. */ private final String name; /** * The primary-key of the table. */ public final PrimaryKey primaryKey; /** * List of table columns. */ private List<Column> columns; /** * Associations to other tables. */ public final List<Association> associations = new ArrayList<Association>(); /** * Use upsert (merge) or insert-statement for entities of this table in export-script. */ public Boolean upsert; /** * Data model default for export mode. */ public final boolean defaultUpsert; /** * Exclude from Deletion? */ public Boolean excludeFromDeletion; /** * Data Model default for Exclude from Deletion. */ public final boolean defaultExcludeFromDeletion; /** * Template for XML exports. */ private String xmlTemplate = null; /** * The original table-name. Differs from name if a source-schema-mapping has been applied to the name. */ private String originalName; /** * Unique number of this table. */ int ordinal; /** * Constructor. * * @param name the table-name * @param primaryKey the names of the primary-key columns * @param defaultUpsert data model default for export mode */ public Table(String name, PrimaryKey primaryKey, boolean defaultUpsert, boolean defaultExcludeFromDeletion) { this.name = name; this.primaryKey = primaryKey; this.defaultUpsert = defaultUpsert; this.defaultExcludeFromDeletion = defaultExcludeFromDeletion; } /** * Gets the table name. * * @return the table name */ public String getName() { return name; } /** * Gets export modus. * * @return Boolean.TRUE for upsert/merge, Boolean.FALSE for insert */ public Boolean getUpsert() { return upsert == null? defaultUpsert : upsert; } /** * Sets columns. * * @param columns list of table columns */ public void setColumns(List<Column> columns) { this.columns = columns; } /** * Gets columns. * * @return list of table columns */ public List<Column> getColumns() { if (columns == null) { return Collections.emptyList(); } return columns; } /** * Compares tables. */ public boolean equals(Object other) { if (other instanceof Table) { return name.equals(((Table) other).name); } return false; } /** * The hash-code. */ public int hashCode() { return name.hashCode(); } /** * Stringifies the table. */ public String toString() { String str = name + " (" + primaryKey + ")\n"; List<Association> all = new ArrayList<Association>(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); } } if (!dep.isEmpty()) { str += "\n depends on:\n"; for (Association association: dep) { if (!"".equals(association.toString())) { str += " " + association + "\n"; } } } if (!hasDep.isEmpty()) { str += "\n has dependent:\n"; for (Association association: hasDep) { if (!"".equals(association.toString())) { str += " " + association + "\n"; } } } if (!assoc.isEmpty()) { str += "\n is associated with:\n"; for (Association association: assoc) { if (!"".equals(assoc.toString())) { str += " " + association + "\n"; } } } if (!ignored.isEmpty()) { str += "\n ignored:\n"; for (Association association: ignored) { if (!"".equals(association.toString())) { str += " " + association + "\n"; } } } return str + "\n"; } public int compareTo(Table o) { return name.compareTo(o.name); } /** * Gets the closure of the table. * * @param directed consider associations as directed? * * @return closure of the table (all tables associated (in-)direct with table) */ public Set<Table> closure(boolean directed) { return closure(new HashSet<Table>(), new HashSet<Table>(), directed); } /** * Gets the closure of the table. * * @param directed consider associations as directed? * @param tablesToIgnore ignore this tables * * @return closure of the table (all tables associated (in-)direct with table) */ public Set<Table> closure(Set<Table> tablesToIgnore, boolean directed) { return closure(new HashSet<Table>(), tablesToIgnore, directed); } /** * Gets the closure of the table. * * @param tables tables known in closure * @param directed consider associations as directed? * @param tablesToIgnore ignore this tables * * @return closure of the table (all tables associated (in-)directly with table) */ private Set<Table> closure(Set<Table> tables, Set<Table> tablesToIgnore, boolean directed) { Set<Table> closure = new HashSet<Table>(); if (!tables.contains(this) && !tablesToIgnore.contains(this)) { closure.add(this); tables.add(this); for (Association association: associations) { if (!tables.contains(association.destination)) { if (association.getJoinCondition() != null || (association.reversalAssociation.getJoinCondition() != null && !directed)) { closure.addAll(association.destination.closure(tables, tablesToIgnore, directed)); } } } } return closure; } /** * Gets the closure of the table, ignoring restrictions. * * @param tables tables known in closure * * @return closure of the table (all tables associated (in-)directly with table) */ public Set<Table> unrestrictedClosure(Set<Table> tables) { Set<Table> closure = new HashSet<Table>(); if (!tables.contains(this)) { closure.add(this); tables.add(this); for (Association association: associations) { if (!tables.contains(association.destination)) { closure.addAll(association.destination.unrestrictedClosure(tables)); } } } return closure; } /** * Sets template for XML exports. */ public void setXmlTemplate(String xmlTemplate) { this.xmlTemplate = xmlTemplate; } /** * Gets template for XML exports. */ public String getXmlTemplate() { return xmlTemplate; } /** * Gets template for XML exports as DOM. */ public Document getXmlTemplateAsDocument(Quoting quoting) throws ParserConfigurationException, SAXException, IOException { return getXmlTemplateAsDocument(xmlTemplate, quoting); } /** * Gets default template for XML exports as DOM. */ public Document getDefaultXmlTemplate(Quoting quoting) throws ParserConfigurationException, SAXException, IOException { return getXmlTemplateAsDocument(quoting); } /** * Gets template for XML exports as DOM. */ private Document getXmlTemplateAsDocument(String xmlTemplate, Quoting quoting) throws ParserConfigurationException, SAXException, IOException { Document template; if (xmlTemplate == null) { template = createInitialXmlTemplate(quoting); } else { template = XmlUtil.parse(xmlTemplate); } removeNonAggregatedAssociationElements((Element) template.getChildNodes().item(0)); // find associations: final Set<String> mappedAssociations = new HashSet<String>(); XmlUtil.visitDocumentNodes(template, new NodeVisitor() { public void visitAssociationElement(String associationName) { mappedAssociations.add(associationName); } public void visitElementEnd(String elementName, boolean isRoot) { } public void visitText(String text) { } public void visitComment(String comment) { } public void visitElementStart(String elementName, boolean isRoot, String[] attributeNames, String[] attributeValues) { } }); // add associations: for (Association a: associations) { if (a.getAggregationSchema() != AggregationSchema.NONE && !mappedAssociations.contains(a.getName())) { Comment comment= template.createComment("associated " + a.destination.getUnqualifiedName() + (Cardinality.MANY_TO_ONE.equals(a.getCardinality()) || Cardinality.ONE_TO_ONE.equals(a.getCardinality())? " row" : " rows")); template.getChildNodes().item(0).appendChild(comment); Element associationElement = template.createElementNS(XmlUtil.NS_URI, XmlUtil.ASSOCIATION_TAG); associationElement.setPrefix(XmlUtil.NS_PREFIX); associationElement.appendChild(template.createTextNode(a.getName())); template.getChildNodes().item(0).appendChild(associationElement); } } return template; } private void removeNonAggregatedAssociationElements(Element element) { NodeList children = element.getChildNodes(); int i = 0; while (i < children.getLength()) { if (children.item(i) instanceof Element) { Element e = (Element) children.item(i); if (XmlUtil.NS_URI.equals(e.getNamespaceURI()) && XmlUtil.ASSOCIATION_TAG.equals(e.getLocalName())) { boolean f = false; for (Association a: associations) { if (a.getAggregationSchema() != AggregationSchema.NONE && e.getTextContent() != null) { if (a.getName().equals(e.getTextContent().trim())) { f = true; break; } } } if (f) { ++i; } else { element.removeChild(e); } } else { removeNonAggregatedAssociationElements(e); ++i; } } else { ++i; } } } /** * Creates initial XML mapping template. */ private Document createInitialXmlTemplate(Quoting quoting) throws ParserConfigurationException { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setNamespaceAware(true); DocumentBuilder builder = factory.newDocumentBuilder(); Document template = builder.newDocument(); Element root = template.createElement(XmlUtil.asElementName(getUnqualifiedName().toLowerCase())); root.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:" + XmlUtil.NS_PREFIX, XmlUtil.NS_URI); template.appendChild(root); boolean commented = false; for (Column column: getColumns()) { if (!commented) { Comment comment= template.createComment("columns of " + getUnqualifiedName() + " as T"); root.appendChild(comment); commented = true; } boolean isPK = false; for (Column pk: primaryKey.getColumns()) { if (pk.name.equals(column.name)) { isPK = true; break; } } Element columnElement = template.createElement(XmlUtil.asElementName(column.name.toLowerCase())); String quotedName = quoting != null? quoting.requote(column.name) : column.name; if (!isPK) { columnElement.setAttribute(XmlUtil.NS_PREFIX + ":if-not-null", XmlUtil.SQL_PREFIX + "T." + quotedName); } columnElement.setTextContent(XmlUtil.SQL_PREFIX + "T." + quotedName); root.appendChild(columnElement); } return template; } /** * Gets un-mapped schema name of table. * * @param defaultSchema the default schema to return if table name is unqualified * @return schema name */ public String getOriginalSchema(String defaultSchema) { int i = indexOfDot(getOriginalName()); if (i >= 0) { return getOriginalName().substring(0, i); } return defaultSchema; } /** * Gets mapped schema name of table. * * @param defaultSchema the default schema to return if table name is unqualified * @return schema name */ public String getSchema(String defaultSchema) { int i = indexOfDot(name); if (i >= 0) { return name.substring(0, i); } return defaultSchema; } /** * Gets unqualified name of table. * * @return unqualified name of table */ public String getUnqualifiedName() { int i = indexOfDot(name); if (i >= 0) { return name.substring(i + 1); } return name; } /** * Gets index of schema-table separator. */ private int indexOfDot(String fullName) { if (fullName.length() > 0) { char c = fullName.charAt(0); if (SqlUtil.LETTERS_AND_DIGITS.indexOf(c) < 0) { // quoting int end = fullName.substring(1).indexOf(c); if (end >= 0) { end += 1; int i = fullName.substring(end).indexOf('.'); if (i >= 0) { return i + end; } return -1; } } } return fullName.indexOf('.'); } /** * Sets the original table-name. Differs from name if a source-schema-mapping has been applied to the name. * * @param originalName the original name */ public void setOriginalName(String originalName) { this.originalName = originalName; } /** * Gets the original table-name. Differs from name if a source-schema-mapping has been applied to the name. * * @return the original name */ public String getOriginalName() { return originalName == null? name : originalName; } /** * Gets unique number of this table to be used as type discriminator in JAILER_ENTITY table. * * @return unique number */ public int getOrdinal() { return ordinal; } /** * Gets all non-virtual columns of the table in the order in which they are selected. * * @param session current session * @return all non-virtual columns of the table in the order in which they are selected */ public List<Column> getSelectionClause(Session session) { ArrayList<Column> result = new ArrayList<Column>(); for (Column column: getColumns()) { if (!column.isVirtualOrBlocked(session)) { result.add(column); } } return result; } public boolean isExcludedFromDeletion() { return excludeFromDeletion == null? defaultExcludeFromDeletion : excludeFromDeletion; } }