001package org.andromda.schema2xmi;
002
003import java.sql.Connection;
004import java.sql.DatabaseMetaData;
005import java.sql.DriverManager;
006import java.sql.ResultSet;
007import java.sql.ResultSetMetaData;
008import java.sql.SQLException;
009import java.util.ArrayList;
010import java.util.Collection;
011import java.util.HashMap;
012import java.util.HashSet;
013import java.util.Map;
014import java.util.Set;
015import java.util.StringTokenizer;
016import org.andromda.core.common.ExceptionUtils;
017import org.andromda.core.engine.ModelProcessorException;
018import org.andromda.core.mapping.Mappings;
019import org.andromda.core.namespace.NamespaceComponents;
020import org.andromda.core.repository.Repositories;
021import org.andromda.core.repository.RepositoryFacade;
022import org.apache.commons.dbutils.DbUtils;
023import org.apache.commons.lang.StringUtils;
024import org.apache.log4j.Logger;
025import org.omg.uml.UmlPackage;
026import org.omg.uml.foundation.core.AssociationEnd;
027import org.omg.uml.foundation.core.Attribute;
028import org.omg.uml.foundation.core.Classifier;
029import org.omg.uml.foundation.core.CorePackage;
030import org.omg.uml.foundation.core.DataType;
031import org.omg.uml.foundation.core.Stereotype;
032import org.omg.uml.foundation.core.TagDefinition;
033import org.omg.uml.foundation.core.TaggedValue;
034import org.omg.uml.foundation.core.UmlAssociation;
035import org.omg.uml.foundation.core.UmlClass;
036import org.omg.uml.foundation.datatypes.AggregationKindEnum;
037import org.omg.uml.foundation.datatypes.ChangeableKindEnum;
038import org.omg.uml.foundation.datatypes.DataTypesPackage;
039import org.omg.uml.foundation.datatypes.Multiplicity;
040import org.omg.uml.foundation.datatypes.MultiplicityRange;
041import org.omg.uml.foundation.datatypes.OrderingKindEnum;
042import org.omg.uml.foundation.datatypes.ScopeKindEnum;
043import org.omg.uml.foundation.datatypes.VisibilityKindEnum;
044import org.omg.uml.modelmanagement.Model;
045import org.omg.uml.modelmanagement.ModelManagementPackage;
046
047/**
048 * Performs the transformation of database schema to XMI.
049 *
050 * TODO This class really should have the functionality it uses (writing model
051 *       elements) moved to the metafacades.
052 * TODO This class should be refactored into smaller classes.
053 * @author Chad Brandon
054 * @author Joel Kozikowski
055 */
056public class SchemaTransformer
057{
058    private static final Logger logger = Logger.getLogger(SchemaTransformer.class);
059    private RepositoryFacade repository = null;
060
061    /**
062     * The JDBC driver class
063     */
064    private String jdbcDriver = null;
065
066    /**
067     * The JDBC schema user.
068     */
069    private String jdbcUser = null;
070
071    /**
072     * The JDBC schema password.
073     */
074    private String jdbcPassword = null;
075
076    /**
077     * The JDBC connection URL.
078     */
079    private String jdbcConnectionUrl = null;
080
081    /**
082     * The name of the package in which the name of the elements will be
083     * created.
084     */
085    private String packageName = null;
086
087    /**
088     * Stores the name of the schema where the tables can be found.
089     */
090    private String schema = null;
091
092    /**
093     * The regular expression pattern to match on when deciding what table names
094     * to add to the transformed XMI.
095     */
096    private String tableNamePattern = null;
097
098    /**
099     * Stores the schema types to model type mappings.
100     */
101    private Mappings typeMappings = null;
102
103    /**
104     * Stores the classes keyed by table name.
105     */
106    private Map<String, UmlClass> classes = new HashMap<String,  UmlClass>();
107
108    /**
109     * Stores the foreign keys for each table.
110     */
111    private Map foreignKeys = new HashMap();
112
113    /**
114     * Specifies the Class stereotype.
115     */
116    private String classStereotypes = null;
117
118    /**
119     * Stores the name of the column tagged value to use for storing the name of
120     * the column.
121     */
122    private String columnTaggedValue = null;
123
124    /**
125     * Stores the name of the table tagged value to use for storing the name of
126     * the table.
127     */
128    private String tableTaggedValue = null;
129
130    /**
131     * The metadata column name needed to retrieve the database column type field.
132     */
133    private String metaColumnTypeName = "TYPE_NAME";
134
135    /**
136     * The metadata column name needed to retrieve the database column size field.
137     */
138    private String metaColumnColumnSize = "COLUMN_SIZE";
139//    private String metaColumnColumnSize = "PRECISION";
140
141    /**
142     * The metadata column name needed to retrieve the database column number of decimal places
143     */
144    private String metaColumnDecPlaces = null;
145//    private String metaColumnDecPlaces = "SCALE";
146
147    /**
148     * The set of additional tagged values that are (optionally) added to each attribute
149     * in the generated model.
150     */
151    private Map<String, String> attributeTaggedValues = new HashMap<String, String>();
152
153    /**
154     * Stores the version of XMI that will be produced.
155     */
156    private String xmiVersion = null;
157
158    /**
159     * Constructs a new instance of this SchemaTransformer.
160     * @param jdbcDriver
161     * @param jdbcConnectionUrl
162     * @param jdbcUser
163     * @param jdbcPassword
164     */
165    public SchemaTransformer(
166        String jdbcDriver,
167        String jdbcConnectionUrl,
168        String jdbcUser,
169        String jdbcPassword)
170    {
171        ExceptionUtils.checkEmpty("jdbcDriver", jdbcDriver);
172        ExceptionUtils.checkEmpty("jdbcConnectionUrl", jdbcConnectionUrl);
173        ExceptionUtils.checkEmpty("jdbcUser", jdbcUser);
174        ExceptionUtils.checkEmpty("jdbcPassword", jdbcPassword);
175
176        NamespaceComponents.instance().discover();
177        Repositories.instance().initialize();
178        this.repository = Repositories.instance().getImplementation(
179            Schema2XMIGlobals.REPOSITORY_NAMESPACE_NETBEANSMDR);
180        if (repository == null)
181        {
182            throw new ModelProcessorException(
183                "No Repository could be found, please make sure you have a repository with namespace "
184                + Schema2XMIGlobals.REPOSITORY_NAMESPACE_NETBEANSMDR +
185                " on your classpath");
186        }
187        this.repository.open();
188
189        this.jdbcDriver = jdbcDriver;
190        this.jdbcConnectionUrl = jdbcConnectionUrl;
191        this.jdbcUser = jdbcUser;
192        this.jdbcPassword = jdbcPassword;
193        this.jdbcConnectionUrl = jdbcConnectionUrl;
194    }
195
196    /**
197     * Transforms the Schema file and writes it to the location given by
198     * <code>outputLocation</code>. The <code>inputModel</code> must be a
199     * valid URL, otherwise an exception will be thrown.
200     *
201     * @param inputModel the location of the input model to start with (if there
202     *        is one)
203     * @param outputLocation The location to where the transformed output will
204     *        be written.
205     */
206    public void transform(
207        String inputModel,
208        String outputLocation)
209    {
210        long startTime = System.currentTimeMillis();
211        outputLocation = StringUtils.trimToEmpty(outputLocation);
212        if (outputLocation == null)
213        {
214            throw new IllegalArgumentException("'outputLocation' can not be null");
215        }
216        Connection connection = null;
217        try
218        {
219            if (inputModel != null)
220            {
221                logger.info("Input model --> '" + inputModel + '\'');
222            }
223            this.repository.readModel(
224                new String[] {inputModel},
225                null);
226            Class.forName(this.jdbcDriver);
227            connection = DriverManager.getConnection(this.jdbcConnectionUrl,
228                this.jdbcUser, this.jdbcPassword);
229            repository.writeModel(
230                transform(connection),
231                outputLocation,
232                this.xmiVersion);
233        }
234        catch (Throwable th)
235        {
236            throw new SchemaTransformerException(th);
237        }
238        finally
239        {
240            DbUtils.closeQuietly(connection);
241            repository.close();
242        }
243        logger.info(
244            "Completed adding " + this.classes.size() + " classes, writing model to --> '" + outputLocation +
245            "', TIME --> " + ((System.currentTimeMillis() - startTime) / 1000.0) + "[s]");
246    }
247
248    /**
249     * Sets the <code>mappingsUri</code> which is the URI to the sql types to
250     * model type mappings.
251     *
252     * @param typeMappingsUri The typeMappings to set.
253     */
254    public void setTypeMappings(String typeMappingsUri)
255    {
256        try
257        {
258            this.typeMappings = Mappings.getInstance(typeMappingsUri);
259        }
260        catch (final Throwable throwable)
261        {
262            throw new SchemaTransformerException(throwable);
263        }
264    }
265
266    /**
267     * Sets the name of the package to which the model elements will be created.
268     *
269     * @param packageName The packageName to set.
270     */
271    public void setPackageName(String packageName)
272    {
273        this.packageName = packageName;
274    }
275
276    /**
277     * Sets the name of the schema (where the tables can be found).
278     *
279     * @param schema The schema to set.
280     */
281    public void setSchema(String schema)
282    {
283        this.schema = schema;
284    }
285
286    /**
287     * Sets the regular expression pattern to match on when deciding what table
288     * names to add to the transformed XMI.
289     *
290     * @param tableNamePattern The tableNamePattern to set.
291     */
292    public void setTableNamePattern(String tableNamePattern)
293    {
294        this.tableNamePattern = StringUtils.trimToEmpty(tableNamePattern);
295    }
296
297    /**
298     * The column name pattern.
299     */
300    private String columnNamePattern;
301
302    /**
303     * Sets the regular expression pattern to match on when deciding what attributes
304     * to create in the XMI.
305     *
306     * @param columnNamePattern The pattern for filtering the column name.
307     */
308    public void setColumnNamePattern(String columnNamePattern)
309    {
310        this.columnNamePattern = columnNamePattern;
311    }
312
313    /**
314     * Sets the stereotype name for the new classes.
315     *
316     * @param classStereotypes The classStereotypes to set.
317     */
318    public void setClassStereotypes(String classStereotypes)
319    {
320        this.classStereotypes = StringUtils.deleteWhitespace(classStereotypes);
321    }
322
323    /**
324     * Specifies the identifier stereotype.
325     */
326    private String identifierStereotypes = null;
327
328    /**
329     * Sets the stereotype name for the identifiers on the new classes.
330     *
331     * @param identifierStereotypes The identifierStereotypes to set.
332     */
333    public void setIdentifierStereotypes(String identifierStereotypes)
334    {
335        this.identifierStereotypes = StringUtils.deleteWhitespace(identifierStereotypes);
336    }
337
338    /**
339     * Sets the name of the column tagged value to use for storing the name of
340     * the column.
341     *
342     * @param columnTaggedValue The columnTaggedValue to set.
343     */
344    public void setColumnTaggedValue(String columnTaggedValue)
345    {
346        this.columnTaggedValue = StringUtils.trimToEmpty(columnTaggedValue);
347    }
348
349    /**
350     * Sets the name of the table tagged value to use for storing the name of
351     * the table.
352     *
353     * @param tableTaggedValue The tableTaggedValue to set.
354     */
355    public void setTableTaggedValue(String tableTaggedValue)
356    {
357        this.tableTaggedValue = StringUtils.trimToEmpty(tableTaggedValue);
358    }
359
360    /**
361     * @param taggedValues
362     */
363    public void setAttributeTaggedValues(String taggedValues) {
364        if (StringUtils.isNotBlank(taggedValues)) {
365            StringTokenizer tokList = new StringTokenizer(taggedValues, ",");
366            while (tokList.hasMoreTokens()) {
367               String tok = tokList.nextToken();
368               String[] parts = StringUtils.split(tok, "=");
369               if (parts.length == 2) {
370                   String tag = parts[0];
371                   String value = parts[1];
372                   attributeTaggedValues.put(tag, value);
373               }
374            } // while
375        }
376    }
377
378    /**
379     * Sets the version of XMI that will be produced.
380     *
381     * @param xmiVersion The xmiVersion to set.
382     */
383    public void setXmiVersion(String xmiVersion)
384    {
385        this.xmiVersion = xmiVersion;
386    }
387
388    /**
389     * The package that is currently being processed.
390     */
391    private UmlPackage umlPackage;
392
393    /**
394     * The model thats currently being processed
395     */
396    private Model model;
397
398    /**
399     * Performs the actual translation of the Schema to the XMI and returns the
400     * object model.
401     */
402    private final Object transform(final Connection connection)
403        throws Exception
404    {
405        this.umlPackage = (UmlPackage)this.repository.getModel().getModel();
406
407        final ModelManagementPackage modelManagementPackage = umlPackage.getModelManagement();
408
409        final Collection models = modelManagementPackage.getModel().refAllOfType();
410        if (models != null && !models.isEmpty())
411        {
412            // A given XMI file can contain multiple models.
413            // Use the first model in the XMI file
414            this.model = (Model)models.iterator().next();
415        }
416        else
417        {
418            this.model = modelManagementPackage.getModel().createModel();
419        }
420
421        // create the package on the model
422        org.omg.uml.modelmanagement.UmlPackage leafPackage =
423            this.getOrCreatePackage(
424                umlPackage.getModelManagement(),
425                model,
426                this.packageName);
427        this.createClasses(
428            connection,
429            umlPackage.getModelManagement().getCore(),
430            leafPackage);
431
432        return umlPackage;
433    }
434
435    /**
436     * Gets or creates a package having the specified <code>packageName</code>
437     * using the given <code>modelManagementPackage</code>, places it on the
438     * <code>model</code> and returns the last leaf package.
439     *
440     * @param modelManagementPackage from which we retrieve the UmlPackageClass
441     *        to create a UmlPackage.
442     * @param modelPackage the root UmlPackage
443     * @param packageName
444     * @return modelPackage
445     */
446    protected org.omg.uml.modelmanagement.UmlPackage getOrCreatePackage(
447        ModelManagementPackage modelManagementPackage,
448        org.omg.uml.modelmanagement.UmlPackage modelPackage,
449        String packageName)
450    {
451        packageName = StringUtils.trimToEmpty(packageName);
452        if (StringUtils.isNotBlank(packageName))
453        {
454            String[] packages = packageName.split(Schema2XMIGlobals.PACKAGE_SEPARATOR);
455            if (packages != null && packages.length > 0)
456            {
457                for (int ctr = 0; ctr < packages.length; ctr++)
458                {
459                    Object umlPackage = ModelElementFinder.find(modelPackage, packages[ctr]);
460
461                    if (umlPackage == null)
462                    {
463                        umlPackage =
464                            modelManagementPackage.getUmlPackage().createUmlPackage(
465                            packages[ctr], VisibilityKindEnum.VK_PUBLIC, false, false, false, false);
466                        modelPackage.getOwnedElement().add(umlPackage);
467                    }
468                    modelPackage = (org.omg.uml.modelmanagement.UmlPackage)umlPackage;
469                }
470            }
471        }
472        return modelPackage;
473    }
474
475    /**
476     * Creates all classes from the tables found in the schema.
477     *
478     * @param connection the Connection used to retrieve the schema metadata.
479     * @param corePackage the CorePackage instance we use to create the classes.
480     * @param modelPackage the package which the classes are added.
481     * @throws SQLException
482     */
483    protected void createClasses(
484        Connection connection,
485        CorePackage corePackage,
486        org.omg.uml.modelmanagement.UmlPackage modelPackage)
487        throws SQLException
488    {
489        DatabaseMetaData metadata = connection.getMetaData();
490        ResultSet tableRs = metadata.getTables(
491                null,
492                this.schema,
493                null,
494                new String[] {"TABLE"});
495
496        // loop through and create all classes and store then
497        // in the classes Map keyed by table
498        while (tableRs.next())
499        {
500            String tableName = tableRs.getString("TABLE_NAME");
501            if (StringUtils.isNotBlank(this.tableNamePattern))
502            {
503                if (tableName.matches(this.tableNamePattern))
504                {
505                    UmlClass umlClass = this.createClass(modelPackage, metadata, corePackage, tableName);
506                    this.classes.put(tableName, umlClass);
507                }
508            }
509            else
510            {
511                UmlClass umlClass = this.createClass(modelPackage, metadata, corePackage, tableName);
512                this.classes.put(tableName, umlClass);
513            }
514        }
515        DbUtils.closeQuietly(tableRs);
516        if (this.classes.isEmpty())
517        {
518            String schemaName = "";
519            if (StringUtils.isNotBlank(this.schema))
520            {
521                schemaName = " '" + this.schema + "' ";
522            }
523            StringBuilder warning = new StringBuilder("WARNING! No tables found in schema");
524            warning.append(schemaName);
525            if (StringUtils.isNotBlank(this.tableNamePattern))
526            {
527                warning.append(" matching pattern --> '").append(this.tableNamePattern).append('\'');
528            }
529            logger.warn(warning);
530        }
531
532        // add all attributes and associations to the modelPackage
533        for (String tableName : this.classes.keySet())
534        {
535            final UmlClass umlClass = classes.get(tableName);
536            if (logger.isInfoEnabled())
537            {
538                logger.info("created class --> '" + umlClass.getName() + '\'');
539            }
540
541            // create and add all associations to the package
542            modelPackage.getOwnedElement().addAll(this.createAssociations(metadata, corePackage, tableName));
543
544            // create and add all the attributes
545            umlClass.getFeature().addAll(this.createAttributes(metadata, corePackage, tableName));
546
547            modelPackage.getOwnedElement().add(umlClass);
548        }
549    }
550
551    /**
552     * Creates and returns a UmlClass with the given <code>name</code> using
553     * the <code>corePackage</code> to create it.
554     * @param modelPackage
555     * @param metadata
556     * @param corePackage used to create the class.
557     * @param tableName to tableName for which we'll create the appropriate
558     *        class.
559     * @return the UmlClass
560     */
561    protected UmlClass createClass(
562        org.omg.uml.modelmanagement.UmlPackage modelPackage,
563        DatabaseMetaData metadata,
564        CorePackage corePackage,
565        String tableName)
566    {
567        String className = SqlToModelNameFormatter.toClassName(tableName);
568        UmlClass umlClass =
569            corePackage.getUmlClass().createUmlClass(
570                className, VisibilityKindEnum.VK_PUBLIC, false, false, false, false, false);
571
572        umlClass.getStereotype().addAll(this.getOrCreateStereotypes(corePackage,
573            this.classStereotypes, "Classifier"));
574
575        if (StringUtils.isNotBlank(this.tableTaggedValue))
576        {
577            // add the tagged value for the table name
578            TaggedValue taggedValue = this.createTaggedValue(corePackage,
579                this.tableTaggedValue, tableName);
580            if (taggedValue != null)
581            {
582                umlClass.getTaggedValue().add(taggedValue);
583            }
584        }
585
586        return umlClass;
587    }
588
589    /**
590     * Creates and returns a collection of attributes from creating an attribute
591     * from every column on the table having the give <code>tableName</code>.
592     *
593     * @param metadata the DatabaseMetaData from which to retrieve the columns.
594     * @param corePackage used to create the class.
595     * @param tableName the tableName for which to find columns.
596     * @return the collection of new attributes.
597     * @throws SQLException
598     */
599    protected Collection createAttributes(
600        DatabaseMetaData metadata,
601        CorePackage corePackage,
602        String tableName)
603        throws SQLException
604    {
605        final Collection attributes = new ArrayList();
606        final ResultSet columnRs = metadata.getColumns(null, this.schema, tableName, null);
607        final Collection primaryKeyColumns = this.getPrimaryKeyColumns(metadata, tableName);
608
609        ResultSetMetaData colMeta = columnRs.getMetaData();
610        int colCount = colMeta.getColumnCount();
611
612        while (columnRs.next())
613        {
614            final String columnName = columnRs.getString("COLUMN_NAME");
615
616            if (logger.isDebugEnabled()) {
617                for (int c = 1; c <= colCount; c++) {
618                    logger.debug("Meta column " + colMeta.getColumnName(c) + " = " + columnRs.getString(c));
619                }
620            }
621
622            if (this.columnNamePattern == null || columnName.matches(this.columnNamePattern))
623            {
624                final String attributeName = SqlToModelNameFormatter.toAttributeName(columnName);
625                if (logger.isInfoEnabled())
626                {
627                    logger.info("adding attribute --> '" + attributeName + '\'');
628                }
629
630                // do NOT add foreign key columns as attributes (since
631                // they are placed on association ends)
632                if (!this.hasForeignKey(tableName, columnName))
633                {
634                    Classifier typeClass = null;
635
636                    // first we try to find a mapping that maps to the
637                    // database proprietary type
638                    String typeName = columnRs.getString(this.getMetaColumnTypeName());
639                    String colSize = columnRs.getString(this.getMetaColumnColumnSize());
640                    String decPlaces = null;
641                    if (StringUtils.isNotBlank(this.getMetaColumnDecPlaces()))
642                    {
643                        decPlaces = columnRs.getString(this.getMetaColumnDecPlaces());
644                    }
645
646                    String type =
647                        Schema2XMIUtils.constructTypeName(
648                            typeName,
649                            colSize, decPlaces);
650                    logger.info("  -  searching for type mapping '" + type + '\'');
651                    if (typeMappings.containsFrom(type))
652                    {
653                        typeClass = this.getOrCreateDataType(corePackage, type);
654                    }
655
656                    // - See if we can find a type matching a mapping for a JDBC type
657                    //   (if we haven't found a database specific one)
658                    if (typeClass == null)
659                    {
660                        type = JdbcTypeFinder.find(columnRs.getInt("DATA_TYPE"));
661                        logger.info("  -  searching for type mapping '" + type + '\'');
662                        if (typeMappings.containsFrom(type))
663                        {
664                            typeClass = this.getOrCreateDataType(corePackage, type);
665                        }
666                        else
667                        {
668                            logger.warn("  !  no mapping found, type not added to '" + attributeName + '\'');
669                        }
670                    }
671
672                    boolean required = !this.isColumnNullable(metadata, tableName, columnName);
673
674                    Attribute attribute =
675                        corePackage.getAttribute().createAttribute(
676                            attributeName,
677                            VisibilityKindEnum.VK_PUBLIC,
678                            false,
679                            ScopeKindEnum.SK_INSTANCE,
680                            this.createAttributeMultiplicity(
681                                corePackage.getDataTypes(),
682                                required),
683                            ChangeableKindEnum.CK_CHANGEABLE,
684                            ScopeKindEnum.SK_CLASSIFIER,
685                            OrderingKindEnum.OK_UNORDERED,
686                            null);
687                    attribute.setType(typeClass);
688
689                    if (StringUtils.isNotBlank(this.columnTaggedValue))
690                    {
691                        // add the tagged value for the column name
692                        TaggedValue taggedValue =
693                            this.createTaggedValue(corePackage, this.columnTaggedValue, columnName);
694                        if (taggedValue != null)
695                        {
696                            attribute.getTaggedValue().add(taggedValue);
697                        }
698                    }
699
700                    // Add the attribute specific tagged values (if any)...
701                    if (!attributeTaggedValues.isEmpty())
702                    {
703                        Set<String> keys = attributeTaggedValues.keySet();
704                        for (final String tag : keys)
705                        {
706                            final String value = attributeTaggedValues.get(tag);
707                            TaggedValue taggedValue =
708                                    this.createTaggedValue(corePackage, tag, value);
709                            if (taggedValue != null)
710                            {
711                                attribute.getTaggedValue().add(taggedValue);
712                            }
713                        }
714                    }
715
716                    if (primaryKeyColumns.contains(columnName))
717                    {
718                        attribute.getStereotype().addAll(
719                            this.getOrCreateStereotypes(corePackage, this.identifierStereotypes, "Attribute"));
720                    }
721                    attributes.add(attribute);
722                }
723            }
724        }
725        DbUtils.closeQuietly(columnRs);
726        return attributes;
727    }
728
729    /**
730     * @param metaColumnDecPlaces
731     */
732    public void setMetaColumnDecPlaces(String metaColumnDecPlaces) {
733        this.metaColumnDecPlaces = metaColumnDecPlaces;
734    }
735
736    /**
737     * @return metaColumnDecPlaces
738     */
739    public String getMetaColumnDecPlaces() {
740        return metaColumnDecPlaces;
741    }
742
743    /**
744     * @param metaColumnColumnSize
745     */
746    public void setMetaColumnColumnSize(String metaColumnColumnSize) {
747        this.metaColumnColumnSize = metaColumnColumnSize;
748    }
749
750    /**
751     * @return metaColumnColumnSize
752     */
753    public String getMetaColumnColumnSize() {
754        return metaColumnColumnSize;
755    }
756
757    /**
758     * @param metaColumnTypeName
759     */
760    public void setMetaColumnTypeName(String metaColumnTypeName) {
761        this.metaColumnTypeName = metaColumnTypeName;
762    }
763
764    /**
765     * @return metaColumnTypeName
766     */
767    public String getMetaColumnTypeName() {
768        return metaColumnTypeName;
769    }
770
771    /**
772     * Gets or creates a new data type instance having the given fully qualified
773     * <code>type</code> name.
774     *
775     * @param corePackage the core package
776     * @param type the fully qualified type name.
777     * @return the DataType
778     */
779    protected DataType getOrCreateDataType(
780        CorePackage corePackage,
781        String type)
782    {
783        type = this.typeMappings.getTo(type);
784        Object datatype = ModelElementFinder.find(this.model, type);
785        if (datatype == null || !DataType.class.isAssignableFrom(datatype.getClass()))
786        {
787            String[] names = type.split(Schema2XMIGlobals.PACKAGE_SEPARATOR);
788            if (names != null && names.length > 0)
789            {
790                // the last name is the type name
791                String typeName = names[names.length - 1];
792                names[names.length - 1] = null;
793                String packageName = StringUtils.join(names, Schema2XMIGlobals.PACKAGE_SEPARATOR);
794                org.omg.uml.modelmanagement.UmlPackage umlPackage =
795                    this.getOrCreatePackage(
796                        this.umlPackage.getModelManagement(),
797                        this.model,
798                        packageName);
799                if (umlPackage != null)
800                {
801                    datatype =
802                        corePackage.getDataType().createDataType(
803                            typeName, VisibilityKindEnum.VK_PUBLIC, false, false, false, false);
804                    umlPackage.getOwnedElement().add(datatype);
805                }
806            }
807        }
808        return (DataType)datatype;
809    }
810
811    /**
812     * This method just checks to see if a column is null able or not, if so,
813     * returns true, if not returns false.
814     *
815     * @param metadata the DatabaseMetaData instance used to retrieve the column
816     *        information.
817     * @param tableName the name of the table on which the column exists.
818     * @param columnName the name of the column.
819     * @return true/false on whether or not column is nullable.
820     * @throws SQLException
821     */
822    protected boolean isColumnNullable(
823        DatabaseMetaData metadata,
824        String tableName,
825        String columnName)
826        throws SQLException
827    {
828        boolean nullable = true;
829        ResultSet columnRs = metadata.getColumns(null, this.schema, tableName, columnName);
830        while (columnRs.next())
831        {
832            nullable = columnRs.getInt("NULLABLE") != DatabaseMetaData.attributeNoNulls;
833        }
834        DbUtils.closeQuietly(columnRs);
835        return nullable;
836    }
837
838    /**
839     * Returns a collection of all primary key column names for the given
840     * <code>tableName</code>.
841     *
842     * @param metadata
843     * @param tableName
844     * @return collection of primary key names.
845     * @throws SQLException
846     */
847    protected Collection getPrimaryKeyColumns(
848        DatabaseMetaData metadata,
849        String tableName)
850        throws SQLException
851    {
852        Collection primaryKeys = new HashSet();
853        ResultSet primaryKeyRs = metadata.getPrimaryKeys(null, this.schema, tableName);
854        while (primaryKeyRs.next())
855        {
856            primaryKeys.add(primaryKeyRs.getString("COLUMN_NAME"));
857        }
858        DbUtils.closeQuietly(primaryKeyRs);
859        return primaryKeys;
860    }
861
862    /**
863     * Creates and returns a collection of associations by determining foreign
864     * tables to the table having the given <code>tableName</code>.
865     *
866     * @param metadata the DatabaseMetaData from which to retrieve the columns.
867     * @param corePackage used to create the class.
868     * @param tableName the tableName for which to find columns.
869     * @return the collection of new attributes.
870     * @throws SQLException
871     */
872    protected Collection createAssociations(
873        DatabaseMetaData metadata,
874        CorePackage corePackage,
875        String tableName)
876        throws SQLException
877    {
878        Collection primaryKeys = this.getPrimaryKeyColumns(metadata, tableName);
879        Collection associations = new ArrayList();
880        ResultSet columnRs = metadata.getImportedKeys(null, this.schema, tableName);
881        while (columnRs.next())
882        {
883            // store the foreign key in the foreignKeys Map
884            String fkColumnName = columnRs.getString("FKCOLUMN_NAME");
885            this.addForeignKey(tableName, fkColumnName);
886
887            // now create the association
888            String foreignTableName = columnRs.getString("PKTABLE_NAME");
889            UmlAssociation association =
890                corePackage.getUmlAssociation().createUmlAssociation(
891                    null, VisibilityKindEnum.VK_PUBLIC, false, false, false, false);
892
893            // we set the upper range to 1 if the
894            // they primary key of this table is the
895            // foreign key of another table (by default
896            // its set to a many multiplicity)
897            int primaryUpper = -1;
898            if (primaryKeys.contains(fkColumnName))
899            {
900                primaryUpper = 1;
901            }
902
903            String endName = null;
904
905            // primary association
906            AssociationEnd primaryEnd =
907                corePackage.getAssociationEnd().createAssociationEnd(
908                    endName,
909                    VisibilityKindEnum.VK_PUBLIC,
910                    false,
911                    true,
912                    OrderingKindEnum.OK_UNORDERED,
913                    AggregationKindEnum.AK_NONE,
914                    ScopeKindEnum.SK_INSTANCE,
915                    this.createMultiplicity(
916                        corePackage.getDataTypes(),
917                        0,
918                        primaryUpper),
919                    ChangeableKindEnum.CK_CHANGEABLE);
920            primaryEnd.setParticipant(this.classes.get(tableName));
921            association.getConnection().add(primaryEnd);
922
923            boolean required = !this.isColumnNullable(metadata, tableName, fkColumnName);
924
925            int foreignLower = 0;
926            if (required)
927            {
928                foreignLower = 1;
929            }
930
931            int deleteRule = columnRs.getInt("DELETE_RULE");
932
933            // determine if we should have composition for
934            // the foreign association end depending on cascade delete
935            AggregationKindEnum foreignAggregation = AggregationKindEnum.AK_NONE;
936            if (deleteRule == DatabaseMetaData.importedKeyCascade)
937            {
938                foreignAggregation = AggregationKindEnum.AK_COMPOSITE;
939            }
940
941            // foreign association
942            AssociationEnd foreignEnd =
943                corePackage.getAssociationEnd().createAssociationEnd(
944                    endName,
945                    VisibilityKindEnum.VK_PUBLIC,
946                    false,
947                    true,
948                    OrderingKindEnum.OK_UNORDERED,
949                    foreignAggregation,
950                    ScopeKindEnum.SK_INSTANCE,
951                    this.createMultiplicity(
952                        corePackage.getDataTypes(),
953                        foreignLower,
954                        1),
955                    ChangeableKindEnum.CK_CHANGEABLE);
956            final Classifier foreignParticipant = this.classes.get(foreignTableName);
957            if (foreignParticipant == null)
958            {
959                throw new SchemaTransformerException(
960                    "The associated table '" + foreignTableName +
961                    "' must be available in order to create the association");
962            }
963            foreignEnd.setParticipant(foreignParticipant);
964
965            if (StringUtils.isNotBlank(this.columnTaggedValue))
966            {
967                // add the tagged value for the foreign association end
968                TaggedValue taggedValue = this.createTaggedValue(corePackage,
969                    this.columnTaggedValue, fkColumnName);
970                if (taggedValue != null)
971                {
972                    foreignEnd.getTaggedValue().add(taggedValue);
973                }
974            }
975
976            association.getConnection().add(foreignEnd);
977            associations.add(association);
978
979            if (logger.isInfoEnabled())
980            {
981                logger.info(
982                    "adding association: '" + primaryEnd.getParticipant().getName() + " <--> " +
983                    foreignEnd.getParticipant().getName() + '\'');
984            }
985        }
986        DbUtils.closeQuietly(columnRs);
987        return associations;
988    }
989
990    /**
991     * Creates a tagged value given the specified <code>name</code>.
992     * @param corePackage
993     * @param name the name of the tagged value to create.
994     * @param value the value to populate on the tagged value.
995     * @return returns the new TaggedValue
996     */
997    protected TaggedValue createTaggedValue(
998        CorePackage corePackage,
999        String name,
1000        String value)
1001    {
1002        Collection values = new HashSet();
1003        values.add(value);
1004        TaggedValue taggedValue =
1005            corePackage.getTaggedValue().createTaggedValue(name, VisibilityKindEnum.VK_PUBLIC, false, values);
1006
1007        // see if we can find the tag definition and if so add that
1008        // as the type.
1009        Object tagDefinition = ModelElementFinder.find(this.umlPackage, name);
1010        if (tagDefinition != null && TagDefinition.class.isAssignableFrom(tagDefinition.getClass()))
1011        {
1012            taggedValue.setType((TagDefinition)tagDefinition);
1013        }
1014        return taggedValue;
1015    }
1016
1017    /**
1018     * Gets or creates a stereotypes given the specified comma separated list of
1019     * <code>names</code>. If any of the stereotypes can't be found, they
1020     * will be created.
1021     * @param corePackage
1022     * @param names comma separated list of stereotype names
1023     * @param baseClass the base class for which the stereotype applies.
1024     * @return Collection of Stereotypes
1025     */
1026    protected Collection getOrCreateStereotypes(
1027        CorePackage corePackage,
1028        String names,
1029        String baseClass)
1030    {
1031        Collection stereotypes = new HashSet();
1032        String[] stereotypeNames = null;
1033        if (names != null)
1034        {
1035            stereotypeNames = names.split(",");
1036        }
1037        if (stereotypeNames != null && stereotypeNames.length > 0)
1038        {
1039            for (int ctr = 0; ctr < stereotypeNames.length; ctr++)
1040            {
1041                String name = StringUtils.trimToEmpty(stereotypeNames[ctr]);
1042
1043                // see if we can find the stereotype first
1044                Object stereotype = ModelElementFinder.find(this.umlPackage, name);
1045                if (stereotype == null || !Stereotype.class.isAssignableFrom(stereotype.getClass()))
1046                {
1047                    Collection baseClasses = new ArrayList();
1048                    baseClasses.add(baseClass);
1049                    stereotype =
1050                        corePackage.getStereotype().createStereotype(
1051                         name, VisibilityKindEnum.VK_PUBLIC, false, false, false, false, null, baseClasses);
1052                    this.model.getOwnedElement().add(stereotype);
1053                }
1054                stereotypes.add(stereotype);
1055            }
1056        }
1057        return stereotypes;
1058    }
1059
1060    /**
1061     * Adds a foreign key column name to the <code>foreignKeys</code> Map. The
1062     * map stores a collection of foreign key names keyed by the given
1063     * <code>tableName</code>
1064     *
1065     * @param tableName the name of the table for which to store the keys.
1066     * @param columnName the name of the foreign key column name.
1067     */
1068    protected void addForeignKey(
1069        String tableName,
1070        String columnName)
1071    {
1072        if (StringUtils.isNotBlank(tableName) && StringUtils.isNotBlank(columnName))
1073        {
1074            Collection foreignKeys = (Collection)this.foreignKeys.get(tableName);
1075            if (foreignKeys == null)
1076            {
1077                foreignKeys = new HashSet();
1078            }
1079            foreignKeys.add(columnName);
1080            this.foreignKeys.put(tableName, foreignKeys);
1081        }
1082    }
1083
1084    /**
1085     * Returns true if the table with the given <code>tableName</code> has a
1086     * foreign key with the specified <code>columnName</code>.
1087     *
1088     * @param tableName the name of the table to check for the foreign key
1089     * @param columnName the name of the foreign key column.
1090     * @return true/false depending on whether or not the table has the foreign
1091     *         key with the given <code>columnName</code>.
1092     */
1093    protected boolean hasForeignKey(
1094        String tableName,
1095        String columnName)
1096    {
1097        boolean hasForeignKey = false;
1098        if (StringUtils.isNotBlank(tableName) && StringUtils.isNotBlank(columnName))
1099        {
1100            Collection foreignKeys = (Collection)this.foreignKeys.get(tableName);
1101            if (foreignKeys != null)
1102            {
1103                hasForeignKey = foreignKeys.contains(columnName);
1104            }
1105        }
1106        return hasForeignKey;
1107    }
1108
1109    /**
1110     * Creates an attributes multiplicity, if <code>required</code> is true,
1111     * then multiplicity is set to 1, if <code>required</code> is false, then
1112     * multiplicity is set to 0..1.
1113     *
1114     * @param dataTypes used to create the Multiplicity
1115     * @param required whether or not the attribute is required therefore
1116     *        determining the multiplicity value created.
1117     * @return the new Multiplicity
1118     */
1119    protected Multiplicity createAttributeMultiplicity(
1120        DataTypesPackage dataTypes,
1121        boolean required)
1122    {
1123        Multiplicity mult = null;
1124        if (required)
1125        {
1126            mult = this.createMultiplicity(dataTypes, 1, 1);
1127        }
1128        else
1129        {
1130            mult = this.createMultiplicity(dataTypes, 0, 1);
1131        }
1132        return mult;
1133    }
1134
1135    /**
1136     * Creates a multiplicity, from <code>lower</code> and <code>upper</code>
1137     * ranges.
1138     *
1139     * @param dataTypes used to create the Multiplicity
1140     * @param lower the lower range of the multiplicity
1141     * @param upper the upper range of the multiplicity
1142     * @return the new Multiplicity
1143     */
1144    protected Multiplicity createMultiplicity(
1145        DataTypesPackage dataTypes,
1146        int lower,
1147        int upper)
1148    {
1149        Multiplicity mult = dataTypes.getMultiplicity().createMultiplicity();
1150        MultiplicityRange range = dataTypes.getMultiplicityRange().createMultiplicityRange(lower, upper);
1151        mult.getRange().add(range);
1152        return mult;
1153    }
1154}