View Javadoc
1   /*
2    * This file is a part of the SchemaSpy project (http://schemaspy.sourceforge.net).
3    * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010 John Currier
4    *
5    * SchemaSpy is free software; you can redistribute it and/or
6    * modify it under the terms of the GNU Lesser General Public
7    * License as published by the Free Software Foundation; either
8    * version 2.1 of the License, or (at your option) any later version.
9    *
10   * SchemaSpy is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13   * Lesser General Public License for more details.
14   *
15   * You should have received a copy of the GNU Lesser General Public
16   * License along with this library; if not, write to the Free Software
17   * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
18   */
19  package net.sourceforge.schemaspy.view;
20  
21  import java.util.Collection;
22  import java.util.Comparator;
23  import java.util.Map;
24  import java.util.Set;
25  import java.util.TreeSet;
26  import java.util.regex.Pattern;
27  import net.sourceforge.schemaspy.model.ForeignKeyConstraint;
28  import net.sourceforge.schemaspy.model.Table;
29  import net.sourceforge.schemaspy.model.TableColumn;
30  import net.sourceforge.schemaspy.model.TableIndex;
31  import net.sourceforge.schemaspy.util.DOMUtil;
32  import org.w3c.dom.Document;
33  import org.w3c.dom.Element;
34  import org.w3c.dom.Node;
35  
36  /**
37   * Formats {@link Table}s into an XML DOM tree.
38   *
39   * @author John Currier
40   */
41  public class XmlTableFormatter {
42      private static final XmlTableFormatter instance = new XmlTableFormatter();
43  
44      // valid chars came from http://www.w3.org/TR/REC-xml/#charsets
45      // and attempting to match 0x10000-0x10FFFF with the \p Unicode escapes
46      // (from http://www.regular-expressions.info/unicode.html)
47      private static final Pattern validXmlChars =
48          Pattern.compile("^[ -\uD7FF\uE000-\uFFFD\\p{L}\\p{M}\\p{Z}\\p{S}\\p{N}\\p{P}]*$");
49  
50      /**
51       * Singleton...don't allow instantiation
52       */
53      private XmlTableFormatter() {}
54  
55      /**
56       * Singleton accessor
57       *
58       * @return
59       */
60      public static XmlTableFormatter getInstance() {
61          return instance;
62      }
63  
64      /**
65       * Append the specified tables to the XML node
66       *
67       * @param schemaNode
68       * @param tables
69       */
70      public void appendTables(Element schemaNode, Collection<Table> tables) {
71          Set<Table> byName = new TreeSet<Table>(new Comparator<Table>() {
72              public int compare(Table table1, Table table2) {
73                  return table1.getName().compareToIgnoreCase(table2.getName());
74              }
75          });
76          byName.addAll(tables);
77  
78          Document document = schemaNode.getOwnerDocument();
79          Element tablesNode = document.createElement("tables");
80          schemaNode.appendChild(tablesNode);
81          for (Table table : byName)
82              appendTable(tablesNode, table);
83      }
84  
85      /**
86       * Append table details to the XML node
87       *
88       * @param tablesNode
89       * @param table
90       */
91      private void appendTable(Element tablesNode, Table table) {
92          Document document = tablesNode.getOwnerDocument();
93          Element tableNode = document.createElement("table");
94          tablesNode.appendChild(tableNode);
95          if (table.getId() != null)
96              DOMUtil.appendAttribute(tableNode, "id", String.valueOf(table.getId()));
97          if (table.getSchema() != null)
98              DOMUtil.appendAttribute(tableNode, "schema", table.getSchema());
99          DOMUtil.appendAttribute(tableNode, "name", table.getName());
100         if (table.getNumRows() != -1)
101             DOMUtil.appendAttribute(tableNode, "numRows", String.valueOf(table.getNumRows()));
102         DOMUtil.appendAttribute(tableNode, "type", table.isView() ? "VIEW" : "TABLE");
103         DOMUtil.appendAttribute(tableNode, "remarks", table.getComments() == null ? "" : table.getComments());
104         appendColumns(tableNode, table);
105         appendPrimaryKeys(tableNode, table);
106         appendIndexes(tableNode, table);
107         appendCheckConstraints(tableNode, table);
108         appendView(tableNode, table);
109     }
110 
111     /**
112      * Append all columns in the table to the XML node
113      *
114      * @param tableNode
115      * @param table
116      */
117     private void appendColumns(Element tableNode, Table table) {
118         for (TableColumn column : table.getColumns()) {
119             appendColumn(tableNode, column);
120         }
121     }
122 
123     /**
124      * Append column details to the XML node
125      *
126      * @param tableNode
127      * @param column
128      * @return
129      */
130     private Node appendColumn(Node tableNode, TableColumn column) {
131         Document document = tableNode.getOwnerDocument();
132         Node columnNode = document.createElement("column");
133         tableNode.appendChild(columnNode);
134 
135         DOMUtil.appendAttribute(columnNode, "id", String.valueOf(column.getId()));
136         DOMUtil.appendAttribute(columnNode, "name", column.getName());
137         DOMUtil.appendAttribute(columnNode, "type", column.getType());
138         DOMUtil.appendAttribute(columnNode, "size", String.valueOf(column.getLength()));
139         DOMUtil.appendAttribute(columnNode, "digits", String.valueOf(column.getDecimalDigits()));
140         DOMUtil.appendAttribute(columnNode, "nullable", String.valueOf(column.isNullable()));
141         DOMUtil.appendAttribute(columnNode, "autoUpdated", String.valueOf(column.isAutoUpdated()));
142         if (column.getDefaultValue() != null) {
143             String defaultValue = column.getDefaultValue().toString();
144             if (isBinary(defaultValue)) {
145                 // we're run into a binary default value, convert it to its hex equivalent
146                 defaultValue = asBinary(defaultValue);
147                 // and indicate that it's been converted
148                 DOMUtil.appendAttribute(columnNode, "defaultValueIsBinary", "true");
149             }
150             DOMUtil.appendAttribute(columnNode, "defaultValue", defaultValue);
151         }
152         DOMUtil.appendAttribute(columnNode, "remarks", column.getComments() == null ? "" : column.getComments());
153 
154         for (TableColumn childColumn : column.getChildren()) {
155             Node childNode = document.createElement("child");
156             columnNode.appendChild(childNode);
157             ForeignKeyConstraint constraint = column.getChildConstraint(childColumn);
158             DOMUtil.appendAttribute(childNode, "foreignKey", constraint.getName());
159             DOMUtil.appendAttribute(childNode, "table", childColumn.getTable().getName());
160             DOMUtil.appendAttribute(childNode, "column", childColumn.getName());
161             DOMUtil.appendAttribute(childNode, "implied", String.valueOf(constraint.isImplied()));
162             DOMUtil.appendAttribute(childNode, "onDeleteCascade", String.valueOf(constraint.isCascadeOnDelete()));
163         }
164 
165         for (TableColumn parentColumn : column.getParents()) {
166             Node parentNode = document.createElement("parent");
167             columnNode.appendChild(parentNode);
168             ForeignKeyConstraint constraint = column.getParentConstraint(parentColumn);
169             DOMUtil.appendAttribute(parentNode, "foreignKey", constraint.getName());
170             DOMUtil.appendAttribute(parentNode, "table", parentColumn.getTable().getName());
171             DOMUtil.appendAttribute(parentNode, "column", parentColumn.getName());
172             DOMUtil.appendAttribute(parentNode, "implied", String.valueOf(constraint.isImplied()));
173             DOMUtil.appendAttribute(parentNode, "onDeleteCascade", String.valueOf(constraint.isCascadeOnDelete()));
174         }
175 
176         return columnNode;
177     }
178 
179     /**
180      * Append primary key details to the XML node
181      *
182      * @param tableNode
183      * @param table
184      */
185     private void appendPrimaryKeys(Element tableNode, Table table) {
186         Document document = tableNode.getOwnerDocument();
187         int index = 1;
188 
189         for (TableColumn primaryKeyColumn : table.getPrimaryColumns()) {
190             Node primaryKeyNode = document.createElement("primaryKey");
191             tableNode.appendChild(primaryKeyNode);
192 
193             DOMUtil.appendAttribute(primaryKeyNode, "column", primaryKeyColumn.getName());
194             DOMUtil.appendAttribute(primaryKeyNode, "sequenceNumberInPK", String.valueOf(index++));
195         }
196     }
197 
198     /**
199      * Append check constraint details to the XML node
200      *
201      * @param tableNode
202      * @param table
203      */
204     private void appendCheckConstraints(Element tableNode, Table table) {
205         Document document = tableNode.getOwnerDocument();
206         Map<String, String> constraints = table.getCheckConstraints();
207         if (constraints != null && !constraints.isEmpty()) {
208             for (String name : constraints.keySet()) {
209                 Node constraintNode = document.createElement("checkConstraint");
210                 tableNode.appendChild(constraintNode);
211 
212                 DOMUtil.appendAttribute(constraintNode, "name", name);
213                 DOMUtil.appendAttribute(constraintNode, "constraint", constraints.get(name).toString());
214             }
215         }
216     }
217 
218     /**
219      * Append index details to the XML node
220      *
221      * @param tableNode
222      * @param table
223      */
224     private void appendIndexes(Node tableNode, Table table) {
225         boolean showId = table.getId() != null;
226         Set<TableIndex> indexes = table.getIndexes();
227         if (indexes != null && !indexes.isEmpty()) {
228             indexes = new TreeSet<TableIndex>(indexes); // sort primary keys first
229             Document document = tableNode.getOwnerDocument();
230 
231             for (TableIndex index : indexes) {
232                 Node indexNode = document.createElement("index");
233 
234                 if (showId)
235                     DOMUtil.appendAttribute(indexNode, "id", String.valueOf(index.getId()));
236                 DOMUtil.appendAttribute(indexNode, "name", index.getName());
237                 DOMUtil.appendAttribute(indexNode, "unique", String.valueOf(index.isUnique()));
238 
239                 for (TableColumn column : index.getColumns()) {
240                     Node columnNode = document.createElement("column");
241 
242                     DOMUtil.appendAttribute(columnNode, "name", column.getName());
243                     DOMUtil.appendAttribute(columnNode, "ascending", String.valueOf(index.isAscending(column)));
244                     indexNode.appendChild(columnNode);
245                 }
246                 tableNode.appendChild(indexNode);
247             }
248         }
249     }
250 
251     /**
252      * Append view SQL to the XML node
253      *
254      * @param tableNode
255      * @param table
256      */
257     private void appendView(Element tableNode, Table table) {
258         String sql;
259         if (table.isView() && (sql = table.getViewSql()) != null) {
260             DOMUtil.appendAttribute(tableNode, "viewSql", sql);
261         }
262     }
263 
264     /**
265      * Returns <code>true</code> if the string contains binary data
266      * (chars that are invalid for XML) per http://www.w3.org/TR/REC-xml/#charsets
267      *
268      * @param str
269      * @return
270      */
271     private static boolean isBinary(String str) {
272         return !validXmlChars.matcher(str).matches();
273     }
274 
275     /**
276      * Turns a string into its hex equivalent.
277      * Intended to be used when {@link #isBinary(String)} returns <code>true</code>.
278      *
279      * @param str
280      * @return
281      */
282     private String asBinary(String str) {
283         byte[] bytes = str.getBytes();
284         StringBuilder buf = new StringBuilder(bytes.length * 2);
285         for (int i = 0; i < bytes.length; ++i) {
286             buf.append(String.format("%02X", bytes[i]));
287         }
288         return buf.toString();
289     }
290 }