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: <type 041 * name>[_<column size>[_<column precision>]]. 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}