001    package org.apache.torque.engine.database.model;
002    
003    /*
004     * Licensed to the Apache Software Foundation (ASF) under one
005     * or more contributor license agreements.  See the NOTICE file
006     * distributed with this work for additional information
007     * regarding copyright ownership.  The ASF licenses this file
008     * to you under the Apache License, Version 2.0 (the
009     * "License"); you may not use this file except in compliance
010     * with the License.  You may obtain a copy of the License at
011     *
012     *   http://www.apache.org/licenses/LICENSE-2.0
013     *
014     * Unless required by applicable law or agreed to in writing,
015     * software distributed under the License is distributed on an
016     * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017     * KIND, either express or implied.  See the License for the
018     * specific language governing permissions and limitations
019     * under the License.
020     */
021    
022    import java.util.ArrayList;
023    import java.util.Collections;
024    import java.util.Hashtable;
025    import java.util.Iterator;
026    import java.util.List;
027    import java.util.Map;
028    import org.apache.commons.collections.map.ListOrderedMap;
029    import org.apache.commons.lang.StringUtils;
030    
031    import org.apache.commons.logging.Log;
032    import org.apache.commons.logging.LogFactory;
033    
034    import org.apache.torque.engine.EngineException;
035    
036    import org.xml.sax.Attributes;
037    
038    /**
039     * Data about a table used in an application.
040     * 
041     * @author <a href="mailto:leon@opticode.co.za">Leon Messerschmidt</a>
042     * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
043     * @author <a href="mailto:mpoeschl@marmot.at>Martin Poeschl</a>
044     * @author <a href="mailto:jmcnally@collab.net>John McNally</a>
045     * @author <a href="mailto:dlr@collab.net>Daniel Rall</a>
046     * @author <a href="mailto:byron_foster@byron_foster@yahoo.com>Byron Foster</a>
047     * @author <a href="mailto:monroe@dukece.com>Greg Monroe</a>
048     * @version $Id: Table.java,v 1.1 2007-10-21 07:57:27 abyrne Exp $
049     */
050    public class Table implements IDMethod {
051            /** Logging class from commons.logging */
052            private static Log log = LogFactory.getLog(Table.class);
053    
054            // private AttributeListImpl attributes;
055            private List columnList;
056            private List foreignKeys;
057            private List indices;
058            private List unices;
059            private List idMethodParameters;
060            private String name;
061            private String description;
062            private String javaName;
063            private String idMethod;
064            private String javaNamingMethod;
065            private Database tableParent;
066            private List referrers;
067            private List foreignTableNames;
068            private boolean containsForeignPK;
069            private Column inheritanceColumn;
070            private boolean skipSql;
071            private boolean abstractValue;
072            private String alias;
073            private String enterface;
074            private String pkg;
075            private String baseClass;
076            private String basePeer;
077            private Hashtable columnsByName;
078            private Hashtable columnsByJavaName;
079            private boolean needsTransactionInPostgres;
080            private boolean heavyIndexing;
081            private boolean forReferenceOnly;
082            private Map options;
083    
084            /**
085             * Default Constructor
086             */
087            public Table() {
088                    this(null);
089            }
090    
091            /**
092             * Constructs a table object with a name
093             * 
094             * @param name
095             *            table name
096             */
097            public Table(String name) {
098                    this.name = name;
099                    columnList = new ArrayList();
100                    foreignKeys = new ArrayList(5);
101                    indices = new ArrayList(5);
102                    unices = new ArrayList(5);
103                    columnsByName = new Hashtable();
104                    columnsByJavaName = new Hashtable();
105                    options = Collections.synchronizedMap(new ListOrderedMap());
106            }
107    
108            /**
109             * Load the table object from an xml tag.
110             * 
111             * @param attrib
112             *            xml attributes
113             * @param defaultIdMethod
114             *            defined at db level
115             */
116            public void loadFromXML(Attributes attrib, String defaultIdMethod) {
117                    name = attrib.getValue("name");
118                    javaName = attrib.getValue("javaName");
119                    idMethod = attrib.getValue("idMethod");
120    
121                    // retrieves the method for converting from specified name to
122                    // a java name.
123                    javaNamingMethod = attrib.getValue("javaNamingMethod");
124                    if (javaNamingMethod == null) {
125                            javaNamingMethod = getDatabase().getDefaultJavaNamingMethod();
126                    }
127    
128                    if ("null".equals(idMethod)) {
129                            idMethod = defaultIdMethod;
130                    }
131                    skipSql = "true".equals(attrib.getValue("skipSql"));
132                    // pkg = attrib.getValue("package");
133                    abstractValue = "true".equals(attrib.getValue("abstract"));
134                    baseClass = attrib.getValue("baseClass");
135                    basePeer = attrib.getValue("basePeer");
136                    alias = attrib.getValue("alias");
137                    heavyIndexing = "true".equals(attrib.getValue("heavyIndexing")) || (!"false".equals(attrib.getValue("heavyIndexing")) && getDatabase().isHeavyIndexing());
138                    description = attrib.getValue("description");
139                    enterface = attrib.getValue("interface");
140            }
141    
142            /**
143             * <p>
144             * A hook for the SAX XML parser to call when this table has been fully loaded from the XML, and all nested elements
145             * have been processed.
146             * </p>
147             * 
148             * <p>
149             * Performs heavy indexing and naming of elements which weren't provided with a name.
150             * </p>
151             */
152            public void doFinalInitialization() {
153                    // Heavy indexing must wait until after all columns composing
154                    // a table's primary key have been parsed.
155                    if (heavyIndexing) {
156                            doHeavyIndexing();
157                    }
158    
159                    // Name any indices which are missing a name using the
160                    // appropriate algorithm.
161                    doNaming();
162            }
163    
164            /**
165             * <p>
166             * Adds extra indices for multi-part primary key columns.
167             * </p>
168             * 
169             * <p>
170             * For databases like MySQL, values in a where clause must match key part order from the left to right. So, in the
171             * key definition <code>PRIMARY KEY (FOO_ID, BAR_ID)</code>, <code>FOO_ID</code> <i>must</i> be the first element
172             * used in the <code>where</code> clause of the SQL query used against this table for the primary key index to be
173             * used. This feature could cause problems under MySQL with heavily indexed tables, as MySQL currently only supports
174             * 16 indices per table (i.e. it might cause too many indices to be created).
175             * </p>
176             * 
177             * <p>
178             * See <a href="http://www.mysql.com/doc/E/X/EXPLAIN.html">the manual</a> for a better description of why heavy
179             * indexing is useful for quickly searchable database tables.
180             * </p>
181             */
182            private void doHeavyIndexing() {
183                    if (log.isDebugEnabled()) {
184                            log.debug("doHeavyIndex() called on table " + name);
185                    }
186    
187                    List pk = getPrimaryKey();
188                    int size = pk.size();
189    
190                    try {
191                            // We start at an offset of 1 because the entire column
192                            // list is generally implicitly indexed by the fact that
193                            // it's a primary key.
194                            for (int i = 1; i < size; i++) {
195                                    addIndex(new Index(this, pk.subList(i, size)));
196                            }
197                    } catch (EngineException e) {
198                            log.error(e, e);
199                    }
200            }
201    
202            /**
203             * Names composing objects which haven't yet been named. This currently consists of foreign-key and index entities.
204             */
205            private void doNaming() {
206                    int i;
207                    int size;
208                    String name;
209    
210                    // Assure names are unique across all databases.
211                    try {
212                            for (i = 0, size = foreignKeys.size(); i < size; i++) {
213                                    ForeignKey fk = (ForeignKey) foreignKeys.get(i);
214                                    name = fk.getName();
215                                    if (StringUtils.isEmpty(name)) {
216                                            name = acquireConstraintName("FK", i + 1);
217                                            fk.setName(name);
218                                    }
219                            }
220    
221                            for (i = 0, size = indices.size(); i < size; i++) {
222                                    Index index = (Index) indices.get(i);
223                                    name = index.getName();
224                                    if (StringUtils.isEmpty(name)) {
225                                            name = acquireConstraintName("I", i + 1);
226                                            index.setName(name);
227                                    }
228                            }
229    
230                            for (i = 0, size = unices.size(); i < size; i++) {
231                                    Unique unique = (Unique) unices.get(i);
232                                    name = unique.getName();
233                                    if (StringUtils.isEmpty(name)) {
234                                            name = acquireConstraintName("U", i + 1);
235                                            unique.setName(name);
236                                    }
237                            }
238                    } catch (EngineException nameAlreadyInUse) {
239                            log.error(nameAlreadyInUse, nameAlreadyInUse);
240                    }
241            }
242    
243            /**
244             * Macro to a constraint name.
245             * 
246             * @param nameType
247             *            constraint type
248             * @param nbr
249             *            unique number for this constraint type
250             * @return unique name for constraint
251             * @throws EngineException
252             */
253            private final String acquireConstraintName(String nameType, int nbr) throws EngineException {
254                    List inputs = new ArrayList(4);
255                    inputs.add(getDatabase());
256                    inputs.add(getName());
257                    inputs.add(nameType);
258                    inputs.add(new Integer(nbr));
259                    return NameFactory.generateName(NameFactory.CONSTRAINT_GENERATOR, inputs);
260            }
261    
262            /**
263             * Gets the value of base class for classes produced from this table.
264             * 
265             * @return The base class for classes produced from this table.
266             */
267            public String getBaseClass() {
268                    if (isAlias() && baseClass == null) {
269                            return alias;
270                    } else if (baseClass == null) {
271                            return getDatabase().getBaseClass();
272                    } else {
273                            return baseClass;
274                    }
275            }
276    
277            /**
278             * Set the value of baseClass.
279             * 
280             * @param v
281             *            Value to assign to baseClass.
282             */
283            public void setBaseClass(String v) {
284                    this.baseClass = v;
285            }
286    
287            /**
288             * Get the value of basePeer.
289             * 
290             * @return value of basePeer.
291             */
292            public String getBasePeer() {
293                    if (isAlias() && basePeer == null) {
294                            return alias + "Peer";
295                    } else if (basePeer == null) {
296                            return getDatabase().getBasePeer();
297                    } else {
298                            return basePeer;
299                    }
300            }
301    
302            /**
303             * Set the value of basePeer.
304             * 
305             * @param v
306             *            Value to assign to basePeer.
307             */
308            public void setBasePeer(String v) {
309                    this.basePeer = v;
310            }
311    
312            /**
313             * A utility function to create a new column from attrib and add it to this table.
314             * 
315             * @param attrib
316             *            xml attributes for the column to add
317             * @return the added column
318             */
319            public Column addColumn(Attributes attrib) {
320                    Column col = new Column();
321                    col.setTable(this);
322                    col.setCorrectGetters(false);
323                    col.loadFromXML(attrib);
324                    addColumn(col);
325                    return col;
326            }
327    
328            /**
329             * Adds a new column to the column list and set the parent table of the column to the current table
330             * 
331             * @param col
332             *            the column to add
333             */
334            public void addColumn(Column col) {
335                    col.setTable(this);
336                    if (col.isInheritance()) {
337                            inheritanceColumn = col;
338                    }
339                    columnList.add(col);
340                    columnsByName.put(col.getName(), col);
341                    columnsByJavaName.put(col.getJavaName(), col);
342                    col.setPosition(columnList.size());
343                    needsTransactionInPostgres |= col.requiresTransactionInPostgres();
344            }
345    
346            /**
347             * A utility function to create a new foreign key from attrib and add it to this table.
348             * 
349             * @param attrib
350             *            the xml attributes
351             * @return the created ForeignKey
352             */
353            public ForeignKey addForeignKey(Attributes attrib) {
354                    ForeignKey fk = new ForeignKey();
355                    fk.loadFromXML(attrib);
356                    addForeignKey(fk);
357                    return fk;
358            }
359    
360            /**
361             * Gets the column that subclasses of the class representing this table can be produced from.
362             */
363            public Column getChildrenColumn() {
364                    return inheritanceColumn;
365            }
366    
367            /**
368             * Get the objects that can be created from this table.
369             */
370            public List getChildrenNames() {
371                    if (inheritanceColumn == null || !inheritanceColumn.isEnumeratedClasses()) {
372                            return null;
373                    }
374                    List children = inheritanceColumn.getChildren();
375                    List names = new ArrayList(children.size());
376                    for (int i = 0; i < children.size(); i++) {
377                            names.add(((Inheritance) children.get(i)).getClassName());
378                    }
379                    return names;
380            }
381    
382            /**
383             * Adds the foreign key from another table that refers to this table.
384             * 
385             * @param fk
386             *            A foreign key refering to this table
387             */
388            public void addReferrer(ForeignKey fk) {
389                    if (referrers == null) {
390                            referrers = new ArrayList(5);
391                    }
392                    referrers.add(fk);
393            }
394    
395            /**
396             * Get list of references to this table.
397             * 
398             * @return A list of references to this table
399             */
400            public List getReferrers() {
401                    return referrers;
402            }
403    
404            /**
405             * Set whether this table contains a foreign PK
406             * 
407             * @param b
408             */
409            public void setContainsForeignPK(boolean b) {
410                    containsForeignPK = b;
411            }
412    
413            /**
414             * Determine if this table contains a foreign PK
415             */
416            public boolean getContainsForeignPK() {
417                    return containsForeignPK;
418            }
419    
420            /**
421             * A list of tables referenced by foreign keys in this table
422             * 
423             * @return A list of tables
424             */
425            public List getForeignTableNames() {
426                    if (foreignTableNames == null) {
427                            foreignTableNames = new ArrayList(1);
428                    }
429                    return foreignTableNames;
430            }
431    
432            /**
433             * Adds a new FK to the FK list and set the parent table of the column to the current table
434             * 
435             * @param fk
436             *            A foreign key
437             */
438            public void addForeignKey(ForeignKey fk) {
439                    fk.setTable(this);
440                    foreignKeys.add(fk);
441    
442                    if (foreignTableNames == null) {
443                            foreignTableNames = new ArrayList(5);
444                    }
445                    if (!foreignTableNames.contains(fk.getForeignTableName())) {
446                            foreignTableNames.add(fk.getForeignTableName());
447                    }
448            }
449    
450            /**
451             * Return true if the column requires a transaction in Postgres
452             */
453            public boolean requiresTransactionInPostgres() {
454                    return needsTransactionInPostgres;
455            }
456    
457            /**
458             * A utility function to create a new id method parameter from attrib and add it to this table.
459             */
460            public IdMethodParameter addIdMethodParameter(Attributes attrib) {
461                    IdMethodParameter imp = new IdMethodParameter();
462                    imp.loadFromXML(attrib);
463                    addIdMethodParameter(imp);
464                    return imp;
465            }
466    
467            /**
468             * Adds a new ID method parameter to the list and sets the parent table of the column associated with the supplied
469             * parameter to this table.
470             * 
471             * @param imp
472             *            The column to add as an ID method parameter.
473             */
474            public void addIdMethodParameter(IdMethodParameter imp) {
475                    imp.setTable(this);
476                    if (idMethodParameters == null) {
477                            idMethodParameters = new ArrayList(2);
478                    }
479                    idMethodParameters.add(imp);
480            }
481    
482            /**
483             * Adds a new index to the index list and set the parent table of the column to the current table
484             */
485            public void addIndex(Index index) {
486                    index.setTable(this);
487                    indices.add(index);
488            }
489    
490            /**
491             * A utility function to create a new index from attrib and add it to this table.
492             */
493            public Index addIndex(Attributes attrib) {
494                    Index index = new Index();
495                    index.loadFromXML(attrib);
496                    addIndex(index);
497                    return index;
498            }
499    
500            /**
501             * Adds a new Unique to the Unique list and set the parent table of the column to the current table
502             */
503            public void addUnique(Unique unique) {
504                    unique.setTable(this);
505                    unices.add(unique);
506            }
507    
508            /**
509             * A utility function to create a new Unique from attrib and add it to this table.
510             * 
511             * @param attrib
512             *            the xml attributes
513             */
514            public Unique addUnique(Attributes attrib) {
515                    Unique unique = new Unique();
516                    unique.loadFromXML(attrib);
517                    addUnique(unique);
518                    return unique;
519            }
520    
521            /**
522             * Get the name of the Table
523             */
524            public String getName() {
525                    return name;
526            }
527    
528            /**
529             * Set the name of the Table
530             */
531            public void setName(String newName) {
532                    name = newName;
533            }
534    
535            /**
536             * Get the description for the Table
537             */
538            public String getDescription() {
539                    return description;
540            }
541    
542            /**
543             * Set the description for the Table
544             * 
545             * @param newDescription
546             *            description for the Table
547             */
548            public void setDescription(String newDescription) {
549                    description = newDescription;
550            }
551    
552            /**
553             * Get name to use in Java sources
554             */
555            public String getJavaName() {
556                    if (javaName == null) {
557                            List inputs = new ArrayList(2);
558                            inputs.add(name);
559                            inputs.add(javaNamingMethod);
560                            try {
561                                    javaName = NameFactory.generateName(NameFactory.JAVA_GENERATOR, inputs);
562                            } catch (EngineException e) {
563                                    log.error(e, e);
564                            }
565                    }
566                    return javaName;
567            }
568    
569            /**
570             * Set name to use in Java sources
571             */
572            public void setJavaName(String javaName) {
573                    this.javaName = javaName;
574            }
575    
576            /**
577             * Get the method for generating pk's
578             */
579            public String getIdMethod() {
580                    if (idMethod == null) {
581                            return IDMethod.NO_ID_METHOD;
582                    } else {
583                            return idMethod;
584                    }
585            }
586    
587            /**
588             * Set the method for generating pk's
589             */
590            public void setIdMethod(String idMethod) {
591                    this.idMethod = idMethod;
592            }
593    
594            /**
595             * Skip generating sql for this table (in the event it should not be created from scratch).
596             * 
597             * @return value of skipSql.
598             */
599            public boolean isSkipSql() {
600                    return (skipSql || isAlias() || isForReferenceOnly());
601            }
602    
603            /**
604             * Set whether this table should have its creation sql generated.
605             * 
606             * @param v
607             *            Value to assign to skipSql.
608             */
609            public void setSkipSql(boolean v) {
610                    this.skipSql = v;
611            }
612    
613            /**
614             * JavaName of om object this entry references.
615             * 
616             * @return value of external.
617             */
618            public String getAlias() {
619                    return alias;
620            }
621    
622            /**
623             * Is this table specified in the schema or is there just a foreign key reference to it.
624             * 
625             * @return value of external.
626             */
627            public boolean isAlias() {
628                    return (alias != null);
629            }
630    
631            /**
632             * Set whether this table specified in the schema or is there just a foreign key reference to it.
633             * 
634             * @param v
635             *            Value to assign to alias.
636             */
637            public void setAlias(String v) {
638                    this.alias = v;
639            }
640    
641            /**
642             * Interface which objects for this table will implement
643             * 
644             * @return value of interface.
645             */
646            public String getInterface() {
647                    return enterface;
648            }
649    
650            /**
651             * Interface which objects for this table will implement
652             * 
653             * @param v
654             *            Value to assign to interface.
655             */
656            public void setInterface(String v) {
657                    this.enterface = v;
658            }
659    
660            /**
661             * When a table is abstract, it marks the business object class that is generated as being abstract. If you have a
662             * table called "FOO", then the Foo BO will be <code>public abstract class Foo</code> This helps support class
663             * hierarchies
664             * 
665             * @return value of abstractValue.
666             */
667            public boolean isAbstract() {
668                    return abstractValue;
669            }
670    
671            /**
672             * When a table is abstract, it marks the business object class that is generated as being abstract. If you have a
673             * table called "FOO", then the Foo BO will be <code>public abstract class Foo</code> This helps support class
674             * hierarchies
675             * 
676             * @param v
677             *            Value to assign to abstractValue.
678             */
679            public void setAbstract(boolean v) {
680                    this.abstractValue = v;
681            }
682    
683            /**
684             * Get the value of package.
685             * 
686             * @return value of package.
687             */
688            public String getPackage() {
689                    if (pkg != null) {
690                            return pkg;
691                    } else {
692                            return this.getDatabase().getPackage();
693                    }
694            }
695    
696            /**
697             * Set the value of package.
698             * 
699             * @param v
700             *            Value to assign to package.
701             */
702            public void setPackage(String v) {
703                    this.pkg = v;
704            }
705    
706            /**
707             * Returns a List containing all the columns in the table
708             * 
709             * @return a List containing all the columns
710             */
711            public List getColumns() {
712                    return columnList;
713            }
714    
715            /**
716             * Utility method to get the number of columns in this table
717             */
718            public int getNumColumns() {
719                    return columnList.size();
720            }
721    
722            /**
723             * Returns a List containing all the FKs in the table
724             * 
725             * @return a List containing all the FKs
726             */
727            public List getForeignKeys() {
728                    return foreignKeys;
729            }
730    
731            /**
732             * Returns a Collection of parameters relevant for the chosen id generation method.
733             */
734            public List getIdMethodParameters() {
735                    return idMethodParameters;
736            }
737    
738            /**
739             * A name to use for creating a sequence if one is not specified.
740             * 
741             * @return name of the sequence
742             */
743            public String getSequenceName() {
744                    String result = null;
745                    if (getIdMethod().equals(NATIVE)) {
746                            List idMethodParams = getIdMethodParameters();
747                            if (idMethodParams == null) {
748                                    result = getName() + "_SEQ";
749                            } else {
750                                    result = ((IdMethodParameter) idMethodParams.get(0)).getValue();
751                            }
752                    }
753                    return result;
754            }
755    
756            /**
757             * Returns a List containing all the indices in the table
758             * 
759             * @return A List containing all the indices
760             */
761            public List getIndices() {
762                    return indices;
763            }
764    
765            /**
766             * Returns a List containing all the UKs in the table
767             * 
768             * @return A List containing all the UKs
769             */
770            public List getUnices() {
771                    return unices;
772            }
773    
774            /**
775             * Returns a specified column.
776             * 
777             * @param name
778             *            name of the column
779             * @return Return a Column object or null if it does not exist.
780             */
781            public Column getColumn(String name) {
782                    return (Column) columnsByName.get(name);
783            }
784    
785            /**
786             * Returns a specified column.
787             * 
788             * @param javaName
789             *            java name of the column
790             * @return Return a Column object or null if it does not exist.
791             */
792            public Column getColumnByJavaName(String javaName) {
793                    return (Column) columnsByJavaName.get(javaName);
794            }
795    
796            /**
797             * Return the first foreign key that includes col in it's list of local columns. Eg. Foreign key (a,b,c) refrences
798             * tbl(x,y,z) will be returned of col is either a,b or c.
799             * 
800             * @param col
801             *            column name included in the key
802             * @return Return a Column object or null if it does not exist.
803             */
804            public ForeignKey getForeignKey(String col) {
805                    ForeignKey firstFK = null;
806                    for (Iterator iter = foreignKeys.iterator(); iter.hasNext();) {
807                            ForeignKey key = (ForeignKey) iter.next();
808                            if (key.getLocalColumns().contains(col)) {
809                                    if (firstFK == null) {
810                                            firstFK = key;
811                                    } else {
812                                            // System.out.println(col+" is in multiple FKs.  This is not"
813                                            // + " being handled properly.");
814                                            // throw new IllegalStateException("Cannot call method if " +
815                                            // "column is referenced multiple times");
816                                    }
817                            }
818                    }
819                    return firstFK;
820            }
821    
822            /**
823             * Returns true if the table contains a specified column
824             * 
825             * @param col
826             *            the column
827             * @return true if the table contains the column
828             */
829            public boolean containsColumn(Column col) {
830                    return columnList.contains(col);
831            }
832    
833            /**
834             * Returns true if the table contains a specified column
835             * 
836             * @param name
837             *            name of the column
838             * @return true if the table contains the column
839             */
840            public boolean containsColumn(String name) {
841                    return (getColumn(name) != null);
842            }
843    
844            /**
845             * Set the parent of the table
846             * 
847             * @param parent
848             *            the parant database
849             */
850            public void setDatabase(Database parent) {
851                    tableParent = parent;
852            }
853    
854            /**
855             * Get the parent of the table
856             * 
857             * @return the parant database
858             */
859            public Database getDatabase() {
860                    return tableParent;
861            }
862    
863            /**
864             * Flag to determine if code/sql gets created for this table. Table will be skipped, if return true.
865             * 
866             * @return value of forReferenceOnly.
867             */
868            public boolean isForReferenceOnly() {
869                    return forReferenceOnly;
870            }
871    
872            /**
873             * Flag to determine if code/sql gets created for this table. Table will be skipped, if set to true.
874             * 
875             * @param v
876             *            Value to assign to forReferenceOnly.
877             */
878            public void setForReferenceOnly(boolean v) {
879                    this.forReferenceOnly = v;
880            }
881    
882            /**
883             * Returns a XML representation of this table.
884             * 
885             * @return XML representation of this table
886             */
887            public String toString() {
888                    StringBuffer result = new StringBuffer();
889    
890                    result.append("<table name=\"").append(name).append('\"');
891    
892                    if (javaName != null) {
893                            result.append(" javaName=\"").append(javaName).append('\"');
894                    }
895    
896                    if (idMethod != null) {
897                            result.append(" idMethod=\"").append(idMethod).append('\"');
898                    }
899    
900                    if (skipSql) {
901                            result.append(" skipSql=\"").append(new Boolean(skipSql)).append('\"');
902                    }
903    
904                    if (abstractValue) {
905                            result.append(" abstract=\"").append(new Boolean(abstractValue)).append('\"');
906                    }
907    
908                    if (baseClass != null) {
909                            result.append(" baseClass=\"").append(baseClass).append('\"');
910                    }
911    
912                    if (basePeer != null) {
913                            result.append(" basePeer=\"").append(basePeer).append('\"');
914                    }
915    
916                    result.append(">\n");
917    
918                    if (columnList != null) {
919                            for (Iterator iter = columnList.iterator(); iter.hasNext();) {
920                                    result.append(iter.next());
921                            }
922                    }
923    
924                    if (foreignKeys != null) {
925                            for (Iterator iter = foreignKeys.iterator(); iter.hasNext();) {
926                                    result.append(iter.next());
927                            }
928                    }
929    
930                    if (idMethodParameters != null) {
931                            Iterator iter = idMethodParameters.iterator();
932                            while (iter.hasNext()) {
933                                    result.append(iter.next());
934                            }
935                    }
936    
937                    result.append("</table>\n");
938    
939                    return result.toString();
940            }
941    
942            /**
943             * Returns the collection of Columns which make up the single primary key for this table.
944             * 
945             * @return A list of the primary key parts.
946             */
947            public List getPrimaryKey() {
948                    List pk = new ArrayList(columnList.size());
949    
950                    Iterator iter = columnList.iterator();
951                    while (iter.hasNext()) {
952                            Column col = (Column) iter.next();
953                            if (col.isPrimaryKey()) {
954                                    pk.add(col);
955                            }
956                    }
957                    return pk;
958            }
959    
960            /**
961             * Determine whether this table has a primary key.
962             * 
963             * @return Whether this table has any primary key parts.
964             */
965            public boolean hasPrimaryKey() {
966                    return (getPrimaryKey().size() > 0);
967            }
968    
969            /**
970             * Returns all parts of the primary key, separated by commas.
971             * 
972             * @return A CSV list of primary key parts.
973             */
974            public String printPrimaryKey() {
975                    return printList(columnList);
976            }
977    
978            /**
979             * Returns the elements of the list, separated by commas.
980             * 
981             * @param list
982             *            a list of Columns
983             * @return A CSV list.
984             */
985            private String printList(List list) {
986                    StringBuffer result = new StringBuffer();
987                    boolean comma = false;
988                    for (Iterator iter = list.iterator(); iter.hasNext();) {
989                            Column col = (Column) iter.next();
990                            if (col.isPrimaryKey()) {
991                                    if (comma) {
992                                            result.append(',');
993                                    } else {
994                                            comma = true;
995                                    }
996                                    result.append(col.getName());
997                            }
998                    }
999                    return result.toString();
1000            }
1001    
1002            /**
1003             * Force all columns to set the correctGetters property.
1004             * 
1005             * @param value
1006             *            The new value of the correctGetters property.
1007             * @since 3.2
1008             */
1009            public void setCorrectGetters(Boolean value) {
1010                    boolean correctGetters = value != null && value.booleanValue();
1011                    for (Iterator it = columnList.iterator(); it.hasNext();) {
1012                            Column col = (Column) it.next();
1013                            col.setCorrectGetters(correctGetters);
1014                    }
1015            }
1016    
1017            /**
1018             * Add an XML Specified option key/value pair to this element's option set.
1019             * 
1020             * @param key
1021             *            the key of the option.
1022             * @param value
1023             *            the value of the option.
1024             */
1025            public void addOption(String key, String value) {
1026                    options.put(key, value);
1027            }
1028    
1029            /**
1030             * Get the value that was associated with this key in an XML option element.
1031             * 
1032             * @param key
1033             *            the key of the option.
1034             * @return The value for the key or a null.
1035             */
1036            public String getOption(String key) {
1037                    return (String) options.get(key);
1038            }
1039    
1040            /**
1041             * Gets the full ordered hashtable array of items specified by XML option statements under this element.
1042             * <p>
1043             * 
1044             * Note, this is not thread save but since it's only used for generation which is single threaded, there should be
1045             * minimum danger using this in Velocity.
1046             * 
1047             * @return An Map of all options. Will not be null but may be empty.
1048             */
1049            public Map getOptions() {
1050                    return options;
1051            }
1052    }