tools.xor.providers.jdbc.JDBCDataModel Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of xor Show documentation
Show all versions of xor Show documentation
Empowering Model Driven Architecture in J2EE applications
/**
* XOR, empowering Model Driven Architecture in J2EE applications
*
* Copyright (c) 2012, Dilip Dalton
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
* See the License for the specific language governing permissions and limitations
* under the License.
*/
package tools.xor.providers.jdbc;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.sql.DataSource;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import tools.xor.JDBCType;
import tools.xor.RelationshipType;
import tools.xor.Type;
import tools.xor.TypeMapper;
import tools.xor.service.AbstractDataModel;
import tools.xor.service.DataModelFactory;
import tools.xor.service.DataStore;
import tools.xor.service.PersistenceProvider;
import tools.xor.service.SchemaExtension;
import tools.xor.service.Shape;
import tools.xor.util.ClassUtil;
/**
* This class is part of the Data Access Service framework.
* It allows DB specific build of the object structure based on the tables, columns
* and foreign keys
*
* dotted notation join syntax has two containment specific behavior (CASCADE DELETE)
* 1. If the foreign key is between 2 tables connecting their primary keys
* then that relationship represents an inheritance relationship
* NOTE: A foreign key between two primary keys is supported on both
* Oracle and HANA
* 2. Else it represents a containment relationship between 2 entities
*
* @author Dilip Dalton
*/
public abstract class JDBCDataModel extends AbstractDataModel
{
private static final Logger logger = LogManager.getLogger(new Exception().getStackTrace()[0].getClassName());
public static class ColumnInfo {
private String name;
private Class javaType;
private String dataType;
private boolean nullable;
private boolean generated; // Does INSERT/UPDATE need to populate this column
private int length;
public String getName() {
return this.name;
}
public Class getType() {
return this.javaType;
}
public String getDataType() {
return this.dataType;
}
public boolean isNullable() {
return this.nullable;
}
public ColumnInfo(String name, boolean nullable, Class javaType, String dataType, Boolean generated, int length) {
this.name = name;
this.nullable = nullable;
this.javaType = javaType;
this.dataType = dataType;
this.generated = generated;
this.length = length;
}
public boolean isGenerated() {
return this.generated;
}
public int getLength() {
return this.length;
}
}
public static class SequenceInfo {
private String name;
private String dataType;
private long min;
private long max;
private int incrementBy;
private long startWith;
private boolean cycle;
public SequenceInfo(String name,
String dataType,
long min,
long max,
int incrementBy,
long startWith,
boolean cycle) {
this.name = name;
this.dataType = dataType;
this.min = min;
this.max = max;
this.incrementBy = incrementBy;
this.startWith = startWith;
this.cycle = cycle;
}
public String getName() {
return this.name;
}
}
public static class TableInfo {
private String name;
private List columns;
private List primaryKeys;
private List foreignKeys;
public String getName() {
return this.name;
}
public void setColumns(List columns) {
this.columns = columns;
}
public List getColumnInfo(List columnssubset) {
Map columnInfoMap = new HashMap<>();
for(ColumnInfo ci: columns) {
columnInfoMap.put(ci.getName(), ci);
}
List result = new LinkedList<>();
for(String column: columnssubset) {
if(columnInfoMap.containsKey(column)) {
result.add(columnInfoMap.get(column));
}
}
return result;
}
public ForeignKey getParentFK() {
ForeignKey parentFK = null;
if(this.foreignKeys != null) {
next: for (ForeignKey fk : foreignKeys) {
if(fk.getReferencingColumns().size() == primaryKeys.size()) {
for(int i = 0; i < primaryKeys.size(); i++) {
String keyPart = primaryKeys.get(i);
if(!keyPart.equals(fk.getReferencingColumns().get(i))) {
continue next;
}
}
// also check that the referenced end is the primary key of the
// referenced table
TableInfo rt = fk.getReferencedTable();
if(fk.getReferencedColumns().size() == rt.getPrimaryKeys().size()) {
for(int i = 0; i < primaryKeys.size(); i++) {
String keyPart = rt.getPrimaryKeys().get(i);
if(!keyPart.equals(fk.getReferencedColumns().get(i))) {
continue next;
}
}
}
parentFK = fk;
break;
}
}
}
return parentFK;
}
/**
* Return the name of the referenced table where
* there is a foreign key between the 2 primary keys
* @return parent table name
*/
public String getParentTable() {
ForeignKey parentFK = getParentFK();
if(parentFK != null) {
return parentFK.getReferencedTable().getName();
}
return null;
}
public TableInfo(String name) {
this.name = name;
}
public void setPrimaryKeys(List primaryKeys) {
this.primaryKeys = primaryKeys;
}
public List getForeignKeys() {
return this.foreignKeys;
}
public void setForeignKeys(List foreignKeys) {
this.foreignKeys = foreignKeys;
}
public List getPrimaryKeys() {
return this.primaryKeys;
}
public void initNoPrimaryKey() {
// All the fields comprise to form the primary key
List keys = new LinkedList<>();
for(ColumnInfo ci: columns) {
keys.add(ci.getName());
}
this.primaryKeys = keys;
}
public List getBasicColumns() {
Map all = new HashMap<>();
for(ColumnInfo ci: columns) {
all.put(ci.getName(), ci);
}
Set fkColumns = new HashSet<>();
if(foreignKeys != null) {
for (ForeignKey fk : foreignKeys) {
fkColumns.addAll(fk.getReferencingColumns());
}
}
Set basicColumns = new HashSet(all.keySet());
basicColumns.removeAll(fkColumns);
List result = new LinkedList<>();
// basic columns should always contain the identifier property
if(primaryKeys != null && primaryKeys.size() == 1) {
basicColumns.addAll(primaryKeys);
}
for(String basicCol: basicColumns) {
result.add(all.get(basicCol));
}
return result;
}
}
public static enum ForeignKeyRule {
CASCADE,
RESTRICT,
NO_ACTION,
SET_NULL,
SET_DEFAULT
}
public static class ForeignKey {
/* Delimiter to get inverse relationship name
* Useful to rename relationships
* format:
* Needs to format the below format if the name contains either _1__1_ or
* _1__N_
* _1__1___
* _1__N___
*
* unique prefix - A prefix to uniquely identify this foreign key.
* relationship name - Represents the user facing foreign key relationship name.
* inverse relationship name - Represents the collection relationship name if _1__N_ else
* represents the entity relationship name (_1__1_)
* NOTE: all 3 parts are required for a multi-column foreign key relationship
*/
private static final String DELIM = "__";
private static final String TO_ONE = "_1__1_";
private static final String TO_MANY = "_1__N_";
private String nameInDatabase; // original foreign key name
private TableInfo referencingTable; // table representing source of the relationship
private TableInfo referencedTable; // table representing target of the relationship
private String name; // name of the foreign key.
private String inverseName;
private List referencingColumns;
private List referencedColumns;
private RelationshipType type = RelationshipType.TO_MANY; // default
private ForeignKeyRule deleteRule;
private ForeignKeyRule updateRule;
private boolean composition;
private boolean inheritance; // Does this foreign key model an inheritance
// relationship?
@Override
public String toString() {
StringBuilder builder = new StringBuilder(String.format("Foreign key: %s [logical name: %s]", nameInDatabase, name) );
builder.append(String.format("\n ReferencingTable: %s ---> Referenced Table: %s", referencingTable.getName(), referencedTable.getName()));
for(int i = 0; i < referencingColumns.size(); i++) {
builder.append(String.format("\n Key: %s ---> %s", referencingColumns.get(i), referencedColumns.get(i)));
}
return builder.toString();
}
public String getName() {
return this.name;
}
public String getInverseRelationshipName() {
return this.inverseName;
}
/**
* Represents the name of the JDBCProperty representing this foreign key relationship.
*
* If the foreign key is comprised of a single column, then the property name is the
* the name of the column, else the property name is the name of the foreign key,
* or a name overridden by the user (ForeignKeyEnhancer).
*
* @return column name as property name
*/
public String getPropertyName() {
if(referencingColumns.size() == 1) {
return referencingColumns.get(0);
}
return name;
}
public TableInfo getReferencingTable() {
return this.referencingTable;
}
public TableInfo getReferencedTable() {
return this.referencedTable;
}
public ForeignKey(String name, TableInfo referencing, TableInfo referenced, ForeignKeyRule deleteRule, ForeignKeyRule updateRule) {
this.nameInDatabase = name;
this.referencingTable = referencing;
this.referencedTable = referenced;
this.deleteRule = deleteRule;
this.updateRule = updateRule;
this.name = this.nameInDatabase;
if(this.nameInDatabase.indexOf(TO_ONE) != -1 || this.nameInDatabase.indexOf(TO_MANY) != -1) {
if(this.nameInDatabase.indexOf(TO_ONE) != -1) {
this.type = RelationshipType.TO_ONE;
parseNames(this.nameInDatabase.substring(this.nameInDatabase.indexOf(TO_ONE)+TO_ONE.length()));
} else {
parseNames(this.nameInDatabase.substring(this.nameInDatabase.indexOf(TO_MANY)+TO_MANY.length()));
}
}
}
private void parseNames(String fkname) {
if(fkname.indexOf(DELIM) != -1) {
this.name = fkname.substring(0, fkname.indexOf(DELIM));
this.inverseName = fkname.substring(fkname.indexOf(DELIM)+DELIM.length());
} else {
this.inverseName = fkname;
}
}
public RelationshipType getType() {
return this.type;
}
public List getReferencingColumns() {
return this.referencingColumns;
}
public void setReferencingColumns(List columns) {
this.referencingColumns = columns;
}
public List getReferencedColumns() {
return this.referencedColumns;
}
public void setReferencedColumns(List columns) {
this.referencedColumns = columns;
}
public boolean isContainment() {
return deleteRule == ForeignKeyRule.CASCADE;
}
public boolean isInheritance() {
return this.inheritance;
}
public boolean isComposition() {
return this.composition;
}
public void init() {
if(this.referencingTable.getPrimaryKeys() != null && this.referencedTable.getPrimaryKeys() != null) {
if (this.referencingTable.getPrimaryKeys().equals(this.referencingColumns) &&
this.referencedTable.getPrimaryKeys().equals(this.referencedColumns)) {
// @see initComposition() difference
this.inheritance = true;
}
}
}
/**
* The primary key is the same between two tables, but they do not participate in
* a inheritance relationship.
* So the PK values just needs to be copied to the referencing table from the
* referenced table instead of it being generated.
*/
public void makeComposition() {
this.setReferencingColumns(this.referencingTable.getPrimaryKeys());
this.setReferencedColumns(this.referencedTable.getPrimaryKeys());
this.composition = true;
}
}
public JDBCDataModel(DataModelFactory dasFactory, TypeMapper typeMapper) {
super(dasFactory, typeMapper);
}
/**
* Return the columns and their corresponding JAVA types from the database
*
* @param tableName RDBMS table name
* @return map of columns and their types
*/
public TableInfo getTable(String tableName) {
try(Connection c = getDataSource().getConnection()) {
return DBTranslator.instance(c).getTable(c, getAggregateManager().getForeignKeyEnhancer(), tableName);
}
catch (SQLException e) {
throw ClassUtil.wrapRun(e);
}
}
public Map> getPrimaryKeys() {
try(Connection c = getDataSource().getConnection()) {
return DBTranslator.instance(c).getPrimaryKeys(c);
}
catch (SQLException e) {
throw ClassUtil.wrapRun(e);
}
}
public List getTables() {
try(Connection c = getDataSource().getConnection()) {
List tables = DBTranslator.instance(c).getTables(c, getAggregateManager().getForeignKeyEnhancer());
return tables;
}
catch (SQLException e) {
throw ClassUtil.wrapRun(e);
}
}
public List getRelationalTables() {
try(Connection c = getDataSource().getConnection()) {
List tables = DBTranslator.instance(c).getTables(c, getAggregateManager().getForeignKeyEnhancer());
for(TableInfo table: tables) {
table.setForeignKeys(null);
}
return tables;
}
catch (SQLException e) {
throw ClassUtil.wrapRun(e);
}
}
public abstract DataSource getDataSource();
@Override
public Type getType(Shape shape, String typeName, Type type) {
// The typeName is JSONObject, so it is better to use type as fallback
// as it is more specific
return type;
}
@Override public Shape createShape (String name, SchemaExtension extension, Shape.Inheritance typeInheritance)
{
Shape shape = super.createShape(name, extension, typeInheritance);
List tables = name.equals(RELATIONAL_SHAPE) ? getRelationalTables() : getTables();
for(TableInfo table: tables){
JDBCType dataType = new JDBCType(table.getName(), table);
shape.addType(dataType.getName(), dataType);
}
for(TableInfo table: tables) {
setSuperType(table, shape);
}
// Define the properties for the Types
// This will end up defining the simple types
defineProperties(shape, shape.getUniqueTypes());
postProcess(shape, extension, shape.getUniqueTypes(), false);
return shape;
}
private List defineTypes(Shape shape, Set entityNames) {
List tables = shape.getName().equals(RELATIONAL_SHAPE) ? getRelationalTables() : getTables();
List filteredTables = new ArrayList();
if(entityNames != null && !entityNames.isEmpty()) {
Map providerEntityMap = new HashMap<>();
for(TableInfo entityType: tables) {
providerEntityMap.put(entityType.getName(), entityType);
}
for(String entityName: entityNames) {
if(providerEntityMap.containsKey(entityName)) {
filteredTables.add(providerEntityMap.get(entityName));
}
}
} else {
filteredTables = tables;
}
for(TableInfo table: filteredTables){
JDBCType dataType = new JDBCType(table.getName(), table);
shape.addType(dataType.getName(), dataType);
}
return filteredTables;
}
@Override public void processShape(Shape shape, SchemaExtension extension, Set entityNames) {
List tables = defineTypes(shape, entityNames);
// Only if the table is denormalized do we do this
for(TableInfo table: tables) {
setSuperType(table, shape);
}
// Define the properties for the Types
// This will end up defining the simple types
defineProperties(shape, shape.getUniqueTypes());
postProcess(shape, extension, shape.getUniqueTypes(), false);
}
private void setSuperType(TableInfo table, Shape shape) {
String parentName = table.getParentTable();
if(parentName != null) {
JDBCType child = (JDBCType)shape.getType(table.getName());
JDBCType parent = (JDBCType)shape.getType(parentName);
child.setParentType(parent);
}
}
public void addNewTypes(Shape shape) {
String name = shape.getName();
List tables = name.equals(RELATIONAL_SHAPE) ? getRelationalTables() : getTables();
List newTables = new ArrayList<>();
List newTypes = new ArrayList<>();
for(TableInfo table: tables) {
if(shape.getType(table.getName()) != null) {
continue;
}
newTables.add(table);
JDBCType dataType = new JDBCType(table.getName(), table);
shape.addType(dataType.getName(), dataType);
newTypes.add(dataType);
}
for(TableInfo newTable: newTables) {
setSuperType(newTable, shape);
}
// Create the properties for the new types
defineProperties(shape, newTypes);
// We don't do a full post-process as not all steps are necessary
postProcess(shape, null, newTypes, true);
}
protected void defineProperties(Shape shape, Collection types) {
for(Type type: types) {
if(JDBCType.class.isAssignableFrom(type.getClass())) {
JDBCType jdbcType = (JDBCType) type;
jdbcType.defineProperties(shape);
}
}
// Create and Link the bi-directional relationship between the properties
for(Type type: types) {
if(JDBCType.class.isAssignableFrom(type.getClass())) {
JDBCType jdbcType = (JDBCType) type;
jdbcType.setOpposite(shape);
}
}
}
@Override
public PersistenceProvider getPersistenceProvider() {
if(super.getPersistenceProvider() == null) {
this.persistenceProvider = new PersistenceProvider() {
@Override
public DataStore createDS(Object sessionContext, Object data) {
JDBCDataStore po = new JDBCDataStore((JDBCSessionContext)sessionContext, data);
po.setDataSource(getDataSource());
return po;
}
};
}
return this.persistenceProvider;
}
}