All Downloads are FREE. Search and download functionalities are using the official Maven repository.

tools.xor.providers.jdbc.JDBCDataModel Maven / Gradle / Ivy

There is a newer version: 2.4.1
Show newest version
/**
 * 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;
    }     
}





© 2015 - 2024 Weber Informatics LLC | Privacy Policy