001/*
002 * Copyright c 2018 Rusi Popov, MDA Tools.net All rights reserved.
003 *
004 * This program and the accompanying materials are made available under the terms of the
005 * Eclipse Public License v2.0 which accompanies this distribution, and is available at
006 * http://www.eclipse.org/legal/epl-v20.html
007 */
008package net.mdatools.modelant.uml13.reverse;
009
010import java.sql.Connection;
011import java.sql.DatabaseMetaData;
012import java.sql.ResultSet;
013import java.sql.ResultSetMetaData;
014import java.sql.SQLException;
015import java.util.ArrayList;
016import java.util.HashMap;
017import java.util.List;
018import java.util.Map;
019import java.util.logging.Level;
020import java.util.logging.Logger;
021
022import javax.jmi.reflect.RefPackage;
023
024import org.omg.uml13.foundation.core.Attribute;
025import org.omg.uml13.foundation.core.DataType;
026import org.omg.uml13.foundation.core.UmlClass;
027import org.omg.uml13.foundation.datatypes.Expression;
028import org.omg.uml13.foundation.datatypes.VisibilityKindEnum;
029
030import net.mdatools.modelant.core.api.Function;
031import net.mdatools.modelant.repository.api.ModelFactory;
032import net.mdatools.modelant.repository.api.ModelRepository;
033
034/**
035 * Reverse engineering logic for database schemas and storing the results as
036 * UML 1.3 objects. The model produced is in fact a Platform Specific Model, which might need
037 * additional processing and tuning.
038 * Conventions for the model produced:
039 * <ol>
040 * <li>The database column types are converted to DataType instances named: &lt;type
041 * name&gt;[_&lt;column size&gt;[_&lt;column precision&gt;]]. Additionally as tagged values named
042 * {@link net.mdatools.modelant.uml13.metamodel.Convention.TAG_VALUE_DATA_LENGTH} and {@link net.mdatools.modelant.uml13.metamodel.Convention.TAG_VALUE_DATA_TYPE_PRECISION}
043 * these values are bound to the concrete data type.
044 * <li>The {@link net.mdatools.modelant.uml13.metamodel.Convention.TAG_VALUE_DATA_TYPE_PRECISION} tagged value is optional. When not provided, the precision
045 * should be treated as 0
046 * <li>The {@link net.mdatools.modelant.uml13.metamodel.Convention.TAG_VALUE_DATA_LENGTH} tagged value is mandatory.
047 * <li>Any comments found while reverse engineering the database are bound as 'documentation' tagged
048 * values. These tagged values are compatible with the Rose's approach to documentation. They are
049 * optional.
050 * <li>Each attribute pertaining to the table's primary key is bound a {@link net.mdatools.modelant.uml13.metamodel.Convention.TAG_VALUE_PRIMARY_KEY} tagged value
051 * with "Primaty Key" value. Its value is the sequence order of the column in the tible's primary key.
052 * </ol>
053 *
054 * @author Rusi Popov (popovr@mdatools.net)
055 */
056public class ReverseDatabaseOperation implements Function<Connection, RefPackage>{
057
058  private static final Logger LOGGER = Logger.getLogger( ReverseDatabaseOperation.class.getName() );
059
060  private static final String LEGAL_TABLE_NAME_REGEX = "^[a-z$#A-Z0-9_]+$";
061
062  /**
063   * Column name that contains the decimal digits for the column in the result set that describes a
064   * column in a table
065   *
066   * @see java.sql.DatabaseMetaData#getColumns(java.lang.String,java.lang.String,java.lang.String,java.lang.String)
067   */
068  private static final String JDBC_COLUMN_DESCRIPTION_DECIMAL_DIGITS = "DECIMAL_DIGITS";
069
070  /**
071   * Column name that contains the column size for the column in the result set that describes a
072   * column in a table
073   *
074   * @see java.sql.DatabaseMetaData#getColumns(java.lang.String,java.lang.String,java.lang.String,java.lang.String)
075   */
076  private static final String JDBC_COLUMN_DESCRIPTION_COLUMN_SIZE = "COLUMN_SIZE";
077
078  /**
079   * Column name that contains the DB type name of the column in the result set that describes a
080   * column
081   *
082   * @see java.sql.DatabaseMetaData#getColumns(java.lang.String,java.lang.String,java.lang.String,java.lang.String)
083   */
084  private static final String JDBC_COLUMN_DESCRIPTION_TYPE_NAME = "TYPE_NAME";
085
086  /**
087   * Column name that contains the column name in the result set that describes a column
088   *
089   * @see java.sql.DatabaseMetaData#getColumns(java.lang.String,java.lang.String,java.lang.String,java.lang.String)
090   */
091  private static final String JDBC_COLUMN_DESCRIPTION_NAME = "COLUMN_NAME";
092
093  /**
094   * Column name that contains the column comments in the result set that describes a column
095   *
096   * @see java.sql.DatabaseMetaData#getColumns(java.lang.String,java.lang.String,java.lang.String,java.lang.String)
097   */
098  private static final String JDBC_COLUMN_DESCRIPTION_REMARKS = "REMARKS";
099
100  /**
101   * Column name that contains the column's default value (expression) in the result set that
102   * describes a column
103   *
104   * @see java.sql.DatabaseMetaData#getColumns(java.lang.String,java.lang.String,java.lang.String,java.lang.String)
105   */
106  private static final String JDBC_COLUMN_DESCRIPTION_COLUMN_DEFAULT = "COLUMN_DEF";
107
108  /**
109   * Column name that contains the table name in the result set that describes a database table
110   *
111   * @see java.sql.DatabaseMetaData#getTables(java.lang.String,java.lang.String,java.lang.String,java.lang.String[])
112   */
113  private static final String JDBC_TABLE_DESCRIPTION_TABLE_NAME = "TABLE_NAME";
114
115  /**
116   * Column name that contains comments in the result set that describes a database table
117   *
118   * @see java.sql.DatabaseMetaData#getTables(java.lang.String,java.lang.String,java.lang.String,java.lang.String[])
119   */
120  private static final String JDBC_TABLE_DESCRIPTION_REMARKS = "REMARKS";
121
122  /**
123   * Column name that holds the name of the primary key field in the result set that describes a
124   * table's primary keys
125   *
126   * @see java.sql.DatabaseMetaData#getPrimaryKeys(java.lang.String, java.lang.String,
127   *      java.lang.String)
128   */
129  private static final String JDBC_PK_DESCRIPTION_COLUMN_NAME = "COLUMN_NAME";
130
131  /**
132   * Column name that holds the name of the order of the column in the primary in the result set
133   * that describes a table's primary keys
134   *
135   * @see java.sql.DatabaseMetaData#getPrimaryKeys(java.lang.String, java.lang.String,
136   *      java.lang.String)
137   */
138  private static final String JDBC_PK_DESCRIPTION_SEQENCE = "KEY_SEQ";
139
140  /**
141   * Column name that holds the name of the primary key in a relation
142   *
143   * @see java.sql.DatabaseMetaData#getImportedKeys(java.lang.String, java.lang.String,
144   *      java.lang.String)
145   */
146  private static final String JDBC_RELATION_DESCRIPTION_PK_TABLE_NAME = "PKTABLE_NAME";
147
148  /**
149   * Column name that holds the name of the foreign key (field) in a relation
150   *
151   * @see java.sql.DatabaseMetaData#getImportedKeys(java.lang.String, java.lang.String,
152   *      java.lang.String)
153   */
154  private static final String JDBC_RELATION_DESCRIPTION_FK_FIELD = "FKCOLUMN_NAME";
155
156  /**
157   * Column name that indicates what would happen if the primary key is deleted. If it contains
158   * DatabaseMetaData.importedKeyCascade this indicates composition
159   *
160   * @see java.sql.DatabaseMetaData#getImportedKeys(java.lang.String, java.lang.String,
161   *      java.lang.String)
162   * @see java.sql.DatabaseMetaData#importedKeyCascade
163   */
164  private static final String JDBC_RELATION_DESCRIPTION_DELETE_RULE = "DELETE_RULE";
165
166  /**
167   * Type of the tables to reverse engineer, as of the specification of JDBC
168   *
169   * @see java.sql.DatabaseMetaData#getTableTypes()
170   */
171  private static final String[] TABLE_TYPES_TO_REVERSE = new String[] { "TABLE", "VIEW", "SYSTEM TABLE",
172                                                                        // "GLOBAL TEMPORARY", "LOCAL TEMPORARY",
173                                                                       "ALIAS", "SYNONYM"};
174
175  private final ModelRepository modelRepository;
176  private final String[] schemes;
177
178  private Uml13ModelFactory factory;
179
180  /**
181   * @param modelRepository not null
182   * @param schemes not null, not empty name of the schemes to reverse engineer.
183   * NOTE: It might be case sensitive.
184   */
185  public ReverseDatabaseOperation(ModelRepository modelRepository, String... schemes) {
186    this.modelRepository = modelRepository;
187    this.schemes = schemes;
188  }
189
190  /**
191   * This method processes all tables in the schemes and registers classes for them into the extent
192   * provided
193   *
194   * @param metadata is the database metadata to reverse engineer
195   * @param schema the name of the DB schema to reverse engineer
196   * @return non-null list of registered tables
197   * @throws SQLException
198   */
199  protected List<UmlClass> processTables(DatabaseMetaData metadata, String schema) throws SQLException {
200    List<UmlClass> result;
201    ResultSet tableDescriptions;
202    String tableName;
203    String remarks;
204    UmlClass umlClass;
205
206    result = new ArrayList<>();
207
208    // retrieve all tables in the schemes (no catalog restrictions)
209    tableDescriptions = metadata.getTables( null, schema.toUpperCase(), "%", TABLE_TYPES_TO_REVERSE );
210    try {
211      while ( tableDescriptions.next() ) {
212        // create the class
213        tableName = tableDescriptions.getString( JDBC_TABLE_DESCRIPTION_TABLE_NAME );
214
215        LOGGER.log(Level.INFO,"Table: {0}", tableName);
216
217        if ( isValidTableName( tableName ) ) {
218          umlClass = factory.constructClass( tableName );
219          result.add( umlClass );
220
221          remarks = tableDescriptions.getString( JDBC_TABLE_DESCRIPTION_REMARKS );
222
223          factory.constructTagDocumentation( umlClass, remarks );
224          factory.constructTagPersistent( umlClass );
225
226          defineAttributes( metadata, umlClass, schema, tableName );
227        } else {
228          LOGGER.log(Level.INFO,"  skipped" );
229        }
230        LOGGER.log(Level.INFO,"");
231      }
232    } finally {
233      tableDescriptions.close();
234    }
235    return result;
236  }
237
238  /**
239   * Returns true if the table name is a legal one (otherwise JDBC functions would fail)
240   * @param tableName
241   */
242  private boolean isValidTableName(String tableName) {
243    return tableName.matches(LEGAL_TABLE_NAME_REGEX);
244  }
245
246  /**
247   * This method processes all relationhips in the schemes and registers associations for them into
248   * the extent provided. The relationships are identified on class-by-class basis.
249   * @param tables non-null list of regitered tables
250   * @param metadata is the database metadata to reverse engineer
251   * @param schema the name of the DB schemes to reverse engineer
252   * @throws SQLException
253   */
254  protected void processRelationships(List<UmlClass> tables, DatabaseMetaData metadata, String schema) throws SQLException {
255    ResultSet relationDescriptions;
256    String tableName;
257    String otherTable;
258    String thisField;
259    UmlClass otherClass;
260    boolean isComposite;
261
262    // for each class/table find its relationships.
263    for (UmlClass thisClass : tables) {
264
265      tableName = thisClass.getName();
266
267      LOGGER.log(Level.INFO, "Relations of: {0}", tableName );
268
269      // retrieve the relationships - the tables this class/table refers through foreign keys
270      relationDescriptions = metadata.getImportedKeys( null, schema, tableName );
271      try {
272        while ( relationDescriptions.next() ) {
273          otherTable = relationDescriptions.getString( JDBC_RELATION_DESCRIPTION_PK_TABLE_NAME );
274          thisField  = relationDescriptions.getString( JDBC_RELATION_DESCRIPTION_FK_FIELD ); // FK
275
276          // find the other class
277          try {
278            otherClass = findClass( otherTable );
279
280            // establish association
281
282            // other end - the referenced table (the primary key one)
283            // the name of the role of the other end (the PK one) is stated in the foreign key name at
284            // this send
285            // multiplicity: to-one
286
287            // this end (the foreign key table)
288            // the name of the role of this end is simply the name of this (FK) table
289            // multiplicity: from-more
290
291            isComposite = relationDescriptions.getShort( JDBC_RELATION_DESCRIPTION_DELETE_RULE )
292                          == DatabaseMetaData.importedKeyCascade; // "Cascade delete" in the constraint means composition
293
294            factory.constructAssociation( otherClass, thisField, 1, isComposite, true,
295                                          thisClass, "", net.mdatools.modelant.uml13.metamodel.Convention.UNLIMITED_UPPER_MULTIPLICITY,
296                                          thisClass.getNamespace(),
297                                          "" );
298          } catch (IllegalArgumentException ex) {
299            LOGGER.log(Level.SEVERE, ex.getMessage());
300          }
301        }
302      } finally {
303        relationDescriptions.close();
304      }
305    }
306  }
307
308  /**
309   * Finds the class with the name provided
310   *
311   * @param className
312   * @return the non-null class with the name provided
313   * @throws IllegalArgumentException when class with the name cannot be found
314   */
315  private UmlClass findClass(String className) throws IllegalArgumentException {
316    return (UmlClass) factory.locateModelElement(className);
317  }
318
319  /**
320   * Create an UML class in the extent provided that correspnds to the table. This
321   * method binds the attributes with tagged values for documentation, non-null and primary key
322   * indication. See the conventions stated in the class documentation.
323   *
324   * @param metadata
325   * @param umlClass
326   * @param schemes to search columns in
327   * @param tableName
328   * @throws SQLException
329   */
330  private void defineAttributes(DatabaseMetaData metadata, UmlClass umlClass, String schema, String tableName) throws SQLException {
331      ResultSet attributesRS;
332      String remark;
333      String defaultValue;
334      String columnName;
335      Attribute attribute;
336      Expression expression;
337      DataType attributeType;
338      int size;
339      int precision;
340
341      Map<String, Short> primaryKeys; // Maps PK column names to Short sequence order
342      Short sequence;
343
344      // list the primary keys of this table
345      primaryKeys = definePrimaryKey( metadata, schema, tableName );
346
347      // retrieve all columns of this table (no catalog restrictions)
348      attributesRS = metadata.getColumns( null, schema, tableName, "%" );
349      try {
350        while ( attributesRS.next() ) {
351          columnName = attributesRS.getString( JDBC_COLUMN_DESCRIPTION_NAME );
352
353          // bind the attribute
354          attribute = factory.constructAttribute( columnName );
355          attribute.setOwner( umlClass );
356          attribute.setVisibility( VisibilityKindEnum.VK_PUBLIC );
357
358          attributeType = factory.constructDataType( attributesRS.getString( JDBC_COLUMN_DESCRIPTION_TYPE_NAME ));
359          attribute.setType( attributeType );
360
361          // Bind size & precision as tagged values of this attribute
362          size      = attributesRS.getInt( JDBC_COLUMN_DESCRIPTION_COLUMN_SIZE );
363          precision = attributesRS.getInt( JDBC_COLUMN_DESCRIPTION_DECIMAL_DIGITS );
364
365          factory.constructTagSize( attribute, size );
366          if ( precision > 0 ) {
367            factory.constructTagFieldPrecision( attribute, precision );
368          }
369
370          // set the comments found
371          remark = attributesRS.getString( JDBC_COLUMN_DESCRIPTION_REMARKS );
372          factory.constructTagDocumentation( attribute, remark );
373
374          // set default value found
375          defaultValue = attributesRS.getString( JDBC_COLUMN_DESCRIPTION_COLUMN_DEFAULT );
376          if ( defaultValue != null && !defaultValue.equals( "" ) ) {
377            expression = factory.constructExpression( defaultValue );
378            attribute.setInitialValue( expression );
379          }
380
381  //        // is the column nullable
382  //        nullable = (attributesRS.getInt( JDBC_COLUMN_DESCRIPTION_NULLABLE ) == DatabaseMetaData.columnNullable);
383  //        manager.instantiateTagNullable( attribute, nullable );
384
385          // check for primary key
386          sequence = primaryKeys.get( columnName );
387          if ( sequence != null ) { // the column pertains to the primary key of this table
388            factory.constructTagPrimaryKey( attribute, sequence.intValue() );
389          }
390        }
391      } finally {
392        attributesRS.close();
393      }
394    }
395
396  /**
397   * Indicates the primary key columns (attributes) of the class that corresponds to the
398   * table (name) provided. Requires all attributes to have been added
399   *
400   * @param metadata
401   * @param schemes
402   * @param tableName
403   * @return a Map of String column names to Short-s indicating the column order in the primary key
404   * @throws SQLException
405   */
406  private Map<String, Short> definePrimaryKey(DatabaseMetaData metadata, String schema, String tableName) throws SQLException {
407    Map<String, Short> result = new HashMap<>( 11 );
408    ResultSet primaryKeys;
409    String columnName;
410    short sequence;
411
412    // Retrieve the primary keys for the table
413    primaryKeys = metadata.getPrimaryKeys( null, schema, tableName );
414    try {
415      while ( primaryKeys.next() ) {
416        columnName = primaryKeys.getString( JDBC_PK_DESCRIPTION_COLUMN_NAME );
417        sequence = primaryKeys.getShort( JDBC_PK_DESCRIPTION_SEQENCE );
418        result.put( columnName, new Short( sequence ) );
419      }
420    } finally {
421      primaryKeys.close();
422    }
423    return result;
424  }
425
426  /**
427   * Prints the contents of the result set provided and closes it.
428   *
429   * @param resultSet
430   * @throws SQLException
431   */
432  protected void dumpResultSet(ResultSet resultSet) throws SQLException {
433    ResultSetMetaData resultSetMeta;
434    StringBuilder line;
435
436    resultSetMeta = resultSet.getMetaData();
437
438    // dump the column names
439    LOGGER.log(Level.INFO, "Column names:" );
440
441    line = new StringBuilder(256);
442    for (int i = 1; i <= resultSetMeta.getColumnCount(); i++) {
443      if ( i > 1 ) {
444        line.append( ", " );
445      }
446      line.append( resultSetMeta.getColumnLabel( i ) );
447    }
448    LOGGER.log(Level.INFO, line.toString() );
449    LOGGER.log(Level.INFO, line.toString().replaceAll( ".", "-" ));
450
451    // dump the contents column-by-column
452    while ( resultSet.next() ) {
453      line = new StringBuilder(256);
454      for (int i = 1; i <= resultSetMeta.getColumnCount(); i++) {
455        if ( i > 1 ) {
456          line.append( ", " );
457        }
458        line.append( resultSet.getObject( i ) );
459      }
460      LOGGER.log(Level.INFO, line.toString() );
461    }
462    resultSet.close();
463  }
464
465  /**
466   * @param connection not null
467   * @return not null extent with the model of the database schemes
468   * @throws IllegalArgumentException
469   * @see net.mdatools.modelant.core.api.Function#execute(java.lang.Object)
470   */
471  public RefPackage execute(Connection connection) throws IllegalArgumentException {
472    ModelFactory modelFactory;
473    RefPackage result;
474    DatabaseMetaData metadata;
475    List<UmlClass> tables;
476
477    modelFactory = modelRepository.loadMetamodel("UML13");
478    result = modelFactory.instantiate("model");
479
480    factory = new Uml13ModelFactory( result );
481    factory.setModelName( schemes[0] );
482
483    try {
484      metadata = connection.getMetaData();
485
486      for (String schema : schemes) {
487        // process all tables and register corresponding classes
488        tables = processTables( metadata, schema );
489
490        // describe the relations as associations
491        processRelationships( tables, metadata, schema );
492      }
493    } catch (SQLException ex) {
494      throw new IllegalArgumentException(ex);
495    }
496    return result;
497  }
498
499}