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.model;
20  
21  import java.sql.DatabaseMetaData;
22  import java.sql.ResultSet;
23  import java.sql.SQLException;
24  import java.util.Comparator;
25  import java.util.HashMap;
26  import java.util.List;
27  import java.util.Map;
28  import java.util.Set;
29  import java.util.TreeMap;
30  import java.util.logging.Level;
31  import java.util.logging.Logger;
32  import java.util.regex.Pattern;
33  import net.sourceforge.schemaspy.model.xml.TableColumnMeta;
34  
35  public class TableColumn {
36      private final Table table;
37      private final String name;
38      private final Object id;
39      private final String type;
40      private final int length;
41      private final int decimalDigits;
42      private final String detailedSize;
43      private final boolean isNullable;
44      private       boolean isAutoUpdated;
45      private       Boolean isUnique;
46      private final Object defaultValue;
47      private       String comments;
48      private final Map<TableColumn, ForeignKeyConstraint> parents = new HashMap<TableColumn, ForeignKeyConstraint>();
49      private final Map<TableColumn, ForeignKeyConstraint> children = new TreeMap<TableColumn, ForeignKeyConstraint>(new ColumnComparator());
50      private boolean allowImpliedParents = true;
51      private boolean allowImpliedChildren = true;
52      private boolean isExcluded = false;
53      private boolean isAllExcluded = false;
54      private static final Logger logger = Logger.getLogger(TableColumn.class.getName());
55      private static final boolean finerEnabled = logger.isLoggable(Level.FINER);
56  
57      /**
58       * Create a column associated with a table.
59       *
60       * @param table Table the table that this column belongs to
61       * @param rs ResultSet returned from {@link DatabaseMetaData#getColumns(String, String, String, String)}
62       * @throws SQLException
63       */
64      TableColumn(Table table, ResultSet rs, Pattern excludeIndirectColumns, Pattern excludeColumns) throws SQLException {
65          this.table = table;
66  
67          // names and types are typically reused *many* times in a database,
68          // so keep a single instance of each distinct one
69          // (thanks to Mike Barnes for the suggestion)
70          String tmp = rs.getString("COLUMN_NAME");
71          name = tmp == null ? null : tmp.intern();
72          tmp = rs.getString("TYPE_NAME");
73          type = tmp == null ? "unknown" : tmp.intern();
74  
75          decimalDigits = rs.getInt("DECIMAL_DIGITS");
76          Number bufLength = (Number)rs.getObject("BUFFER_LENGTH");
77          if (bufLength != null && bufLength.shortValue() > 0)
78              length = bufLength.shortValue();
79          else
80              length = rs.getInt("COLUMN_SIZE");
81  
82          StringBuilder buf = new StringBuilder();
83          buf.append(length);
84          if (decimalDigits > 0) {
85              buf.append(',');
86              buf.append(decimalDigits);
87          }
88          detailedSize = buf.toString();
89  
90          isNullable = rs.getInt("NULLABLE") == DatabaseMetaData.columnNullable;
91          defaultValue = rs.getString("COLUMN_DEF");
92          setComments(rs.getString("REMARKS"));
93          id = new Integer(rs.getInt("ORDINAL_POSITION") - 1);
94  
95          isAllExcluded = matches(excludeColumns);
96          isExcluded = isAllExcluded || matches(excludeIndirectColumns);
97          if (isExcluded && finerEnabled) {
98              logger.finer("Excluding column " + getTable() + '.' + getName() +
99                          ": matches " + excludeColumns + ":" + isAllExcluded + " " +
100                         excludeIndirectColumns + ":" + matches(excludeIndirectColumns));
101         }
102     }
103 
104     /**
105      * A TableColumn that's derived from something other than traditional database metadata
106      * (e.g. defined in XML).
107      *
108      * @param table
109      * @param colMeta
110      */
111     public TableColumn(Table table, TableColumnMeta colMeta) {
112         this.table = table;
113         name = colMeta.getName();
114         id = null;
115         type = "Unknown";
116         length = 0;
117         decimalDigits = 0;
118         detailedSize = "";
119         isNullable = false;
120         isAutoUpdated = false;
121         defaultValue = null;
122         comments = colMeta.getComments();
123     }
124 
125     /**
126      * Returns the {@link Table} that this column belongs to.
127      *
128      * @return
129      */
130     public Table getTable() {
131         return table;
132     }
133 
134     /**
135      * Returns the column's name.
136      *
137      * @return
138      */
139     public String getName() {
140         return name;
141     }
142 
143     /**
144      * Returns the ID of the column or <code>null</code> if the database doesn't support the concept.
145      *
146      * @return
147      */
148     public Object getId() {
149         return id;
150     }
151 
152     /**
153      * Type of the column.
154      * See {@link DatabaseMetaData#getColumns(String, String, String, String)}'s <code>TYPE_NAME</code>.
155      * @return
156      */
157     public String getType() {
158         return type;
159     }
160 
161     /**
162      * Length of the column.
163      * See {@link DatabaseMetaData#getColumns(String, String, String, String)}'s <code>BUFFER_LENGTH</code>,
164      * or if that's <code>null</code>, <code>COLUMN_SIZE</code>.
165      * @return
166      */
167     public int getLength() {
168         return length;
169     }
170 
171     /**
172      * Decimal digits of the column.
173      * See {@link DatabaseMetaData#getColumns(String, String, String, String)}'s <code>DECIMAL_DIGITS</code>.
174      *
175      * @return
176      */
177     public int getDecimalDigits() {
178         return decimalDigits;
179     }
180 
181     /**
182      * String representation of length with optional decimal digits (if decimal digits &gt; 0).
183      *
184      * @return
185      */
186     public String getDetailedSize() {
187         return detailedSize;
188     }
189 
190     /**
191      * Returns <code>true</code> if null values are allowed
192      *
193      * @return
194      */
195     public boolean isNullable() {
196         return isNullable;
197     }
198 
199     /**
200      * See {@link java.sql.ResultSetMetaData#isAutoIncrement(int)}
201      *
202      * @return
203      */
204     public boolean isAutoUpdated() {
205         return isAutoUpdated;
206     }
207 
208     /**
209      * setIsAutoUpdated
210      *
211      * @param isAutoUpdated boolean
212      */
213     public void setIsAutoUpdated(boolean isAutoUpdated) {
214         this.isAutoUpdated = isAutoUpdated;
215     }
216 
217     /**
218      * Returns <code>true</code> if this column can only contain unique values
219      *
220      * @return
221      */
222     public boolean isUnique() {
223         if (isUnique == null) {
224             // see if there's a unique index on this column by itself
225             for (TableIndex index : table.getIndexes()) {
226                 if (index.isUnique()) {
227                     List<TableColumn> indexColumns = index.getColumns();
228                     if (indexColumns.size() == 1 && indexColumns.contains(this)) {
229                         isUnique = true;
230                         break;
231                     }
232                 }
233             }
234 
235             if (isUnique == null) {
236                 // if it's a single PK column then it's unique
237                 isUnique = table.getPrimaryColumns().size() == 1 && isPrimary();
238             }
239         }
240 
241         return isUnique;
242     }
243 
244     /**
245      * Returns <code>true</code> if this column is a primary key
246      *
247      * @return
248      */
249     public boolean isPrimary() {
250         return table.getPrimaryColumns().contains(this);
251     }
252 
253     /**
254      * Returns <code>true</code> if this column points to another table's primary key.
255      *
256      * @return
257      */
258     public boolean isForeignKey() {
259         return !parents.isEmpty();
260     }
261 
262     /**
263      * Returns the value that the database uses for this column if one isn't provided.
264      *
265      * @return
266      */
267     public Object getDefaultValue() {
268         return defaultValue;
269     }
270 
271     /**
272      * @return Comments associated with this column, or <code>null</code> if none.
273      */
274     public String getComments() {
275         return comments;
276     }
277 
278     /**
279      * See {@link #getComments()}
280      * @param comments
281      */
282     public void setComments(String comments) {
283         this.comments = (comments == null || comments.trim().length() == 0) ? null : comments.trim();
284     }
285 
286     /**
287      * Returns <code>true</code> if this column is to be excluded from relationship diagrams.
288      * Unless {@link #isAllExcluded()} is true this column will be included in the detailed
289      * diagrams of the containing table.
290      *
291      * <p>This is typically an attempt to reduce clutter that can be introduced when many tables
292      * reference a given column.
293      *
294      * @return
295      */
296     public boolean isExcluded() {
297         return isExcluded;
298     }
299 
300     /**
301      * Returns <code>true</code> if this column is to be excluded from all relationships in
302      * relationship diagrams.  This includes the detailed diagrams of the containing table.
303      *
304      * <p>This is typically an attempt to reduce clutter that can be introduced when many tables
305      * reference a given column.
306      *
307      * @return
308      */
309     public boolean isAllExcluded() {
310         return isAllExcluded;
311     }
312 
313     /**
314      * Add a parent column (PK) to this column (FK) via the associated constraint
315      *
316      * @param parent
317      * @param constraint
318      */
319     public void addParent(TableColumn parent, ForeignKeyConstraint constraint) {
320         parents.put(parent, constraint);
321         table.addedParent();
322     }
323 
324     /**
325      * Remove the specified parent column from this column
326      *
327      * @param parent
328      */
329     public void removeParent(TableColumn parent) {
330         parents.remove(parent);
331     }
332 
333     /**
334      * Disassociate all parents from this column
335      */
336     public void unlinkParents() {
337         for (TableColumn parent : parents.keySet()) {
338             parent.removeChild(this);
339         }
340         parents.clear();
341     }
342 
343     /**
344      * Returns the {@link Set} of all {@link TableColumn parents} associated with this column
345      *
346      * @return
347      */
348     public Set<TableColumn> getParents() {
349         return parents.keySet();
350     }
351 
352     /**
353      * Returns the constraint that connects this column to the specified column (this 'child' column to specified 'parent' column)
354      */
355     public ForeignKeyConstraint getParentConstraint(TableColumn parent) {
356         return parents.get(parent);
357     }
358 
359     /**
360      * Removes a parent constraint and returns it, or null if there are no parent constraints
361      *
362      * @return the removed {@link ForeignKeyConstraint}
363      */
364     public ForeignKeyConstraint removeAParentFKConstraint() {
365         for (TableColumn relatedColumn : parents.keySet()) {
366             ForeignKeyConstraint constraint = parents.remove(relatedColumn);
367             relatedColumn.removeChild(this);
368             return constraint;
369         }
370 
371         return null;
372     }
373 
374     /**
375      * Remove one child {@link ForeignKeyConstraint} that points to this column.
376      *
377      * @return the removed constraint, or <code>null</code> if none were available to be removed
378      */
379     public ForeignKeyConstraint removeAChildFKConstraint() {
380         for (TableColumn relatedColumn : children.keySet()) {
381             ForeignKeyConstraint constraint = children.remove(relatedColumn);
382             relatedColumn.removeParent(this);
383             return constraint;
384         }
385 
386         return null;
387     }
388 
389     /**
390      * Add a child column (FK) to this column (PK) via the associated constraint
391      *
392      * @param child
393      * @param constraint
394      */
395     public void addChild(TableColumn child, ForeignKeyConstraint constraint) {
396         children.put(child, constraint);
397         table.addedChild();
398     }
399 
400     /**
401      * Remove the specified child column from this column
402      *
403      * @param child
404      */
405     public void removeChild(TableColumn child) {
406         children.remove(child);
407     }
408 
409     /**
410      * Disassociate all children from this column
411      */
412     public void unlinkChildren() {
413         for (TableColumn child : children.keySet())
414             child.removeParent(this);
415         children.clear();
416     }
417 
418     /**
419      * Returns <code>Set</code> of <code>TableColumn</code>s that have a real (or implied) foreign key that
420      * references this <code>TableColumn</code>.
421      * @return Set
422      */
423     public Set<TableColumn> getChildren() {
424         return children.keySet();
425     }
426 
427     /**
428      * returns the constraint that connects the specified column to this column
429      * (specified 'child' to this 'parent' column)
430      */
431     public ForeignKeyConstraint getChildConstraint(TableColumn child) {
432         return children.get(child);
433     }
434 
435     /**
436      * Returns <code>true</code> if tableName.columnName matches the supplied
437      * regular expression.
438      *
439      * @param regex
440      * @return
441      */
442     public boolean matches(Pattern regex) {
443         return regex.matcher(getTable().getName() + '.' + getName()).matches();
444     }
445 
446     /**
447      * Update the state of this column with the supplied {@link TableColumnMeta}.
448      * Intended to be used with instances created by {@link #TableColumn(Table, TableColumnMeta)}.
449      *
450      * @param colMeta
451      */
452     public void update(TableColumnMeta colMeta) {
453         String newComments = colMeta.getComments();
454         if (newComments != null)
455             setComments(newComments);
456 
457         if (!isPrimary() && colMeta.isPrimary()) {
458             table.setPrimaryColumn(this);
459         }
460 
461         allowImpliedParents  = !colMeta.isImpliedParentsDisabled();
462         allowImpliedChildren = !colMeta.isImpliedChildrenDisabled();
463         isExcluded |= colMeta.isExcluded();
464         isAllExcluded |= colMeta.isAllExcluded();
465     }
466 
467     /**
468      * Returns the name of this column.
469      */
470     @Override
471     public String toString() {
472         return getName();
473     }
474 
475     /**
476      * Two {@link TableColumn}s are considered equal if their tables and names match.
477      */
478     private class ColumnComparator implements Comparator<TableColumn> {
479         public int compare(TableColumn column1, TableColumn column2) {
480             int rc = column1.getTable().compareTo(column2.getTable());
481             if (rc == 0)
482                 rc = column1.getName().compareToIgnoreCase(column2.getName());
483             return rc;
484         }
485     }
486 
487     /**
488      * Returns <code>true</code> if this column is permitted to be an implied FK
489      * (based on name/type/size matches to PKs).
490      *
491      * @return
492      */
493     public boolean allowsImpliedParents() {
494         return allowImpliedParents;
495     }
496 
497     /**
498      * Returns <code>true</code> if this column is permitted to be a PK to an implied FK
499      * (based on name/type/size matches to PKs).
500      *
501      * @return
502      */
503     public boolean allowsImpliedChildren() {
504         return allowImpliedChildren;
505     }
506 }