blob: f21b9f3c90cb64fad4a8a3e2c73fe6ffbec4b929 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.netbeans.modules.j2ee.persistence.entitygenerator;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.netbeans.modules.dbschema.ColumnElement;
import org.netbeans.modules.dbschema.ColumnPairElement;
import org.netbeans.modules.dbschema.DBIdentifier;
import org.netbeans.modules.dbschema.ForeignKeyElement;
import org.netbeans.modules.dbschema.SchemaElement;
import org.netbeans.modules.dbschema.TableElement;
import org.netbeans.modules.dbschema.UniqueKeyElement;
import org.netbeans.modules.j2ee.persistence.entitygenerator.CMPMappingModel.ColumnData;
import org.netbeans.modules.j2ee.persistence.entitygenerator.EntityRelation.CollectionType;
/**
* This class provides an algorithm to produce a set of cmp beans and relations
* from a dbschema element.
*
* @author Chris Webster, Martin Adamek, Andrei Badea
*/
public class DbSchemaEjbGenerator {
private GeneratedTables genTables;
private Map beans = new HashMap();
private List relations = new ArrayList();
private SchemaElement schemaElement;
private Set<String> tablesReferecedByOtherTables;
private Set<String> primaryKeyIsForeignKeyTables;
private final CollectionType colectionType;
private final static Logger LOGGER = Logger.getLogger(DbSchemaEjbGenerator.class.getName());
private boolean useColumNamesInRelations = false;
//private ArrayList<String> warningMessages;
private final boolean generateUnresolvedRelationships;
private final boolean useDefaults;
/**
* Creates a generator for a set of beans.
*
* @param genTables contains the tables to generate and their respective locations.
* @param schemaElement the dbschema containing the tables to generate beans for.
*/
public DbSchemaEjbGenerator(GeneratedTables genTables, SchemaElement schemaElement) {
this(genTables, schemaElement, CollectionType.COLLECTION, false, false, false);
}
/**
* Creates a generator for a set of beans.
*
* @param genTables contains the tables to generate and their respective locations.
* @param schemaElement the dbschema containing the tables to generate beans for.
* @param collectionType collection type is used in some names generation
*/
public DbSchemaEjbGenerator(GeneratedTables genTables, SchemaElement schemaElement, CollectionType collectionType, boolean useColumnNamesInRelationships, boolean useDefaults, boolean generateUnresolvedRelationships) {
this.schemaElement = schemaElement;
this.genTables = genTables;
this.colectionType = collectionType;
this.useColumNamesInRelations = useColumnNamesInRelationships;
this.generateUnresolvedRelationships = generateUnresolvedRelationships;
this.useDefaults = useDefaults;
//warningMessages = new ArrayList<String>();
tablesReferecedByOtherTables = getTablesReferecedByOtherTables(schemaElement);
primaryKeyIsForeignKeyTables = getTablesReferencesOtherTablesWithPrimaryKeyMatch(schemaElement);
buildCMPSet();
}
/**
*
* @param schemaElement The schema
* @return A set of tables that are referenced by at least one another table
*/
public static Set<String> getTablesReferecedByOtherTables(SchemaElement schemaElement) {
Set<String> tableNames = new HashSet<String>();
TableElement[] allTables = schemaElement.getTables();
for(int i = 0; i < allTables.length; i ++ ) {
ForeignKeyElement[] fkElements = allTables[i].getForeignKeys();
for(int fkix = 0; fkix < fkElements.length; fkix ++ ) {
tableNames.add(fkElements[fkix].getReferencedTable().getName().getName());
}
}
return tableNames;
}
/**
*
* @param schemaElement The schema
* @return A set of tables that reference another tables with primary key to promary key reference
*/
public static Set<String> getTablesReferencesOtherTablesWithPrimaryKeyMatch(SchemaElement schemaElement) {
Set<String> tableNames = new HashSet<String>();
TableElement[] allTables = schemaElement.getTables();
for(int i = 0; i < allTables.length; i ++ ) {
TableElement table0 = allTables[i];
UniqueKeyElement pk0 = table0.getPrimaryKey();
if(pk0 != null){//it may be join table or other without pk
ForeignKeyElement[] fkElements = table0.getForeignKeys();
for(int fkix = 0; fkix < fkElements.length; fkix ++ ) {
ForeignKeyElement fk = fkElements[fkix];
TableElement table = fk.getReferencedTable();
UniqueKeyElement pk = table.getPrimaryKey();
//at first step support 1-1 keys (no composite yet).
if(pk != null && 1 == pk0.getColumns().length && fk.getLocalColumns().length == 1 && pk.getColumns().length==1){
if(fk.getLocalColumns()[0].equals(pk0.getColumns()[0])){
tableNames.add(table0.getName().getName());
continue;
}
}
}
}
}
return tableNames;
}
/**
* Returns true if the table is a join table. A table is considered
* a join table regardless of whether the tables it joins are
* included in the tables to generate.
*/
public static boolean isJoinTable(TableElement e, Set<String> tablesReferecedByOtherTables) {
ForeignKeyElement[] foreignKeys = e.getForeignKeys();
if (foreignKeys == null ||
foreignKeys.length != 2) {
return false;
}
int foreignKeySize = foreignKeys[0].getColumns().length +
foreignKeys[1].getColumns().length;
if (foreignKeySize < e.getColumns().length) {
return false;
}
// issue 89576: a table which references itself is not a join table
String tableName = e.getName().getName();
for (int i = 0; i < 2; i++) {
if (tableName.equals(foreignKeys[i].getReferencedTable().getName().getName())) {
return false;
}
}
// issue 90962: a table whose foreign keys are unique is not a join table
if (isFkUnique(foreignKeys[0]) || isFkUnique(foreignKeys[1])) {
return false;
}
// issue 111397: a table which is referenced by another table is not a join table.
if(tablesReferecedByOtherTables.contains(e.getName().getName())) {
return false;
}
return true;
}
private boolean isForeignKey(ForeignKeyElement[] fks,
ColumnElement col) {
if (fks == null) {
return false;
}
for (int i = 0; i < fks.length; i++) {
if (fks[i].getColumn(col.getName()) != null) {
return true;
}
}
return false;
}
public EntityClass[] getBeans() {
return (EntityClass[])beans.values().toArray(new EntityClass[beans.size()]);
}
public EntityRelation[] getRelations() {
return (EntityRelation[])relations.toArray(new EntityRelation[relations.size()]);
}
private EntityClass getBean(String tableName) {
return (EntityClass)beans.get(tableName);
}
private EntityClass addBean(String tableName) {
EntityClass bean = getBean(tableName);
if (bean != null) {
return bean;
}
bean = new EntityClass(
genTables.getCatalog(),
genTables.getSchema(),
tableName,
genTables.getRootFolder(tableName),
genTables.getPackageName(tableName),
genTables.getClassName(tableName),
genTables.getUpdateType(tableName),
useDefaults,
genTables.getUniqueConstraints(tableName) );
beans.put(tableName, bean);
return bean;
}
private void addAllTables() {
List<TableElement> joinTables = new LinkedList<TableElement>();
for (String tableName : genTables.getTableNames()) {
TableElement tableElement =
schemaElement.getTable(DBIdentifier.create(tableName));
if (isJoinTable(tableElement, tablesReferecedByOtherTables)) {
joinTables.add(tableElement);
} else {
addBean(tableName);
}
}
for (TableElement joinTable : joinTables) {
addJoinTable(joinTable);
}
}
private ColumnData[] getLocalColumnData(ForeignKeyElement key) {
ColumnPairElement[] pkPairs = key.getColumnPairs();
ColumnData[] localColumns = new ColumnData[pkPairs.length];
for (int i = 0; i < pkPairs.length; i++) {
localColumns[i] = new ColumnData(pkPairs[i].getLocalColumn().getName().getName(),
pkPairs[i].getLocalColumn().isNullable());
}
return localColumns;
}
private ColumnData[] getReferencedColumnData(ForeignKeyElement key) {
ColumnPairElement[] pkPairs = key.getColumnPairs();
ColumnData[] refColumns = new ColumnData[pkPairs.length];
for (int i = 0; i < pkPairs.length; i++) {
refColumns[i] = new ColumnData(pkPairs[i].getReferencedColumn().getName().getName(),
pkPairs[i].getReferencedColumn().isNullable());
}
return refColumns;
}
/**
* Provide a role name based on the foreign key column.
* @return role name based on foreign key column or default name
*/
private String getRoleName(ForeignKeyElement fk, String defaultName) {
ColumnPairElement[] pkPairs = fk.getColumnPairs();
if (pkPairs == null || pkPairs.length > 1) {
return defaultName;
}
return EntityMember.makeClassName(
pkPairs[0].getLocalColumn().getName().getName());
}
private void addJoinTable(TableElement table) {
ForeignKeyElement[] foreignKeys = table.getForeignKeys();
//different db may return keys in different orders, see discussion in #237965
if(foreignKeys[0].getKeyName() != null && foreignKeys[1].getKeyName()!= null){
if(foreignKeys[0].getKeyName().compareTo(foreignKeys[1].getKeyName()) > 0) {//reorder apphabetically
ForeignKeyElement tmp = foreignKeys[0];
foreignKeys[0] = foreignKeys[1];
foreignKeys[1] = tmp;
}
}
String tableAName = foreignKeys[0].getReferencedTable().getName().getName();
String tableBName = foreignKeys[1].getReferencedTable().getName().getName();
// create role A
EntityClass roleAHelper = getBean(tableAName);
EntityClass roleBHelper = getBean(tableBName);
String roleAClassName = roleAHelper!=null ? roleAHelper.getClassName() : null;
String roleBClassName = roleBHelper!=null ? roleBHelper.getClassName() : null;
// some tables may not be generate in this sessin (either not selected in wizard or pregenerated in libraries), see issue #173160
if(roleAClassName == null || roleBClassName == null)
{
//as it's not in this generation process, skip addition of relationship to missed/preexistent entities
//in some cases it's impossible to generate relationship as there are no classes or classes are in library(read only)
//TODO: later it's good to not skip in case if both entities exist and at least one is not read only, need additional evaluation.
//TODO: consider to add message to ui or visible log
LOGGER.log(Level.INFO,
"Skip relationships generation for \""+table.getName().getName()+"\" join table, next referenced tables was not selected in new wizard: "//NOI18N
+ (roleAClassName == null ? tableAName : "") + (roleAClassName == null && roleBClassName == null ? ", " : "") + (roleBClassName == null ? tableBName : ""));//NOI18N
//warningMessages.add("There was a problem relationships generation from join table[s].");//NOI18N
return;
}
String roleAname = getRoleName(foreignKeys[0], roleAClassName);
String roleBname = getRoleName(foreignKeys[1], roleBClassName);
String roleACmr = EntityMember.makeRelationshipFieldName(roleBClassName, colectionType, true);
String roleBCmr = EntityMember.makeRelationshipFieldName(roleAClassName, colectionType, true);
roleACmr = uniqueAlgorithm(getFieldNames(roleAHelper), roleACmr, null);
List roleBFieldNames = getFieldNames(roleBHelper);
if (tableAName.equals(tableBName)) {
// Handle the special case when both parts of the join table reference
// the same table -- in that case both roleACmr and roleBCmr
// will be added to the same class, but they have the same name.
// So pretend roleACmr was already added when computing an unique
// name for roleBCmr
roleBFieldNames.add(roleACmr);
}
roleBCmr = uniqueAlgorithm(roleBFieldNames, roleBCmr, null);
RelationshipRole roleA = new RelationshipRole(
roleAname,
roleAHelper.getClassName(),
roleACmr,
true,
true,
false);
roleA.setEntityPkgName(roleAHelper.getPackage());
roleAHelper.addRole(roleA);
RelationshipRole roleB = new RelationshipRole(
roleBname,
roleBHelper.getClassName(),
roleBCmr,
true,
true,
false);
roleB.setEntityPkgName(roleBHelper.getPackage());
roleBHelper.addRole(roleB);
EntityRelation relation = new EntityRelation(roleA, roleB);
relations.add(relation);
relation.setRelationName(EntityMember.makeClassName(table.getName().getName()));
roleAHelper.getCMPMapping().getJoinTableMapping().put(roleACmr, table.getName().getName());
CMPMappingModel.JoinTableColumnMapping joinColMapA = new CMPMappingModel.JoinTableColumnMapping();
joinColMapA.setColumns(getColumnData(foreignKeys[0].getColumns()));
joinColMapA.setReferencedColumns(getColumnData(foreignKeys[0].getReferencedColumns()));
joinColMapA.setInverseColumns(getColumnData(foreignKeys[1].getColumns()));
joinColMapA.setReferencedInverseColumns(getColumnData(foreignKeys[1].getReferencedColumns()));
roleAHelper.getCMPMapping().getJoinTableColumnMppings().put(roleACmr, joinColMapA);
roleBHelper.getCMPMapping().getJoinTableMapping().put(roleBCmr, table.getName().getName());
CMPMappingModel.JoinTableColumnMapping joinColMapB = new CMPMappingModel.JoinTableColumnMapping();
joinColMapB.setColumns(getColumnData(foreignKeys[1].getColumns()));
joinColMapB.setReferencedColumns(getColumnData(foreignKeys[1].getReferencedColumns()));
joinColMapB.setInverseColumns(getColumnData(foreignKeys[0].getColumns()));
joinColMapB.setReferencedInverseColumns(getColumnData(foreignKeys[0].getReferencedColumns()));
roleBHelper.getCMPMapping().getJoinTableColumnMppings().put(roleBCmr, joinColMapB);
}
private ColumnData[] getColumnData(ColumnElement[] cols) {
ColumnData[] columns = new ColumnData[cols.length];
for (int i = 0; i < cols.length; i++) {
columns [i] = new ColumnData(cols[i].getName().getName(), cols[i].isNullable());
}
return columns;
}
private static boolean containsSameColumns(ColumnElement[] fkColumns,
UniqueKeyElement uk) {
if (fkColumns.length == uk.getColumns().length) {
for (int i = 0; i < fkColumns.length; i++) {
if (uk.getColumn(fkColumns[i].getName())==null) {
return false;
}
}
return true;
}
return false;
}
private boolean containsColumns(ColumnElement[] fkColumns,
UniqueKeyElement uk) {
if (uk == null) {
return false;
}
for (int i = 0; i < fkColumns.length; i++) {
if (uk.getColumn(fkColumns[i].getName())!=null) {
return true;
}
}
return false;
}
private static boolean isFkUnique(ForeignKeyElement key) {
UniqueKeyElement[] uk = key.getDeclaringTable().getUniqueKeys();
if (uk == null) {
return false;
}
ColumnElement[] columns = key.getColumns();
for (int uin=0; uin < uk.length; uin++) {
if (containsSameColumns(columns, uk[uin])) {
return true;
}
}
return false;
}
// returns true if all of the columns are nullable
private boolean isNullable(ForeignKeyElement key) {
ColumnElement[] columns = key.getColumns();
int i, count = ((columns != null) ? columns.length : 0);
for (i=0; i < count; i++) {
if (!columns[i].isNullable()) {
return false;
}
}
return true;
}
private static UniqueKeyElement getPrimaryOrCandidateKey(TableElement table) {
UniqueKeyElement pk = table.getPrimaryKey();
if (pk != null) {
return pk;
}
UniqueKeyElement[] keys = table.getUniqueKeys();
if (keys == null || keys.length == 0) {
return null;
}
pk = keys[0];
for (int i = 1; i < keys.length; i++) {
if (keys[i].getColumns().length < pk.getColumns().length) {
pk = keys[i];
}
}
return pk;
}
private void generatePkField(ColumnElement column, boolean inPk, boolean pkField) {
EntityMember m = EntityMember.create(column);
m.setPrimaryKey(inPk, pkField);
EntityClass bean = getBean(column.getDeclaringTable().getName().getName());
if(primaryKeyIsForeignKeyTables.contains(column.getDeclaringTable().getName().getName())){
//derived id usage candidate
bean.setDerivedIdCandidate(true);
}
m.setMemberName(uniqueAlgorithm(getFieldNames(bean), m.getMemberName(), null));
bean.getFields().add(m);
}
private void generateRelationship(ForeignKeyElement key) {
String keyTableName = key.getDeclaringTable().getName().getName();
String keyRefName = key.getReferencedTable().getName().getName();
boolean oneToOne = isFkUnique(key);
EntityClass roleAHelper = getBean(keyTableName);
if (roleAHelper == null) {
return;
}
EntityClass roleBHelper = getBean(keyRefName);
if (roleBHelper == null) {
if(generateUnresolvedRelationships){
//we may want to generate field instead of skip
for(ColumnElement col:key.getLocalColumns()){
generatePkField(col, false, false);
}
}
return;
}
// create role B (it's the table which contains the foreign key)
String roleBCmr = EntityMember.makeRelationshipFieldName(
roleAHelper.getClassName(), colectionType, !oneToOne);
roleBCmr = uniqueAlgorithm(getFieldNames(roleBHelper), roleBCmr, null);
RelationshipRole roleB = new RelationshipRole(
//TODO ask generator for default role name, do not assume it is EJB name
getRoleName(key, roleBHelper.getClassName()),
roleBHelper.getClassName(),
roleBCmr,
false,
!oneToOne,
!isNullable(key),
isNullable(key));
roleB.setEntityPkgName(roleBHelper.getPackage());
roleBHelper.addRole(roleB);
// role A
String roleACmr = EntityMember.makeRelationshipFieldName(
roleBHelper.getClassName(), colectionType, false);
/* only use database column name if a column is not required by the
primary key. If a column is already required by the primary key
then executing this code would cause the cmr-field name to be
named cmp-fieldname1. Therefore, we do not change the cmr-field
name and instead use the name of the other ejb (default).
*/
// #185253 I don't see a good reason to have one case when it's possible to use column name and anothe case when it's not possible
// comment code below for now, may need review or deletion in next release if there will be any negative feedback
// #188550 add backward compartible option to have column names instead of tables names
if (useColumNamesInRelations && !containsColumns(key.getColumns(), getPrimaryOrCandidateKey(key.getDeclaringTable()))) {
roleACmr = EntityMember.makeRelationshipFieldName(roleB.getRoleName(), colectionType, false);
}
roleACmr = uniqueAlgorithm(getFieldNames(roleAHelper), roleACmr, null);
RelationshipRole roleA = new RelationshipRole(
//TODO ask generator for default role name, do not assume it is EJB name
getRoleName(key, roleAHelper.getClassName()),
roleAHelper.getClassName(),
roleACmr,
!oneToOne,
false,
false,
isNullable(key));
roleA.setEntityPkgName(roleAHelper.getPackage());
roleAHelper.addRole(roleA);
EntityRelation relation = new EntityRelation(roleA, roleB);
relation.setRelationName(roleA.getEntityName() + '-' + roleB.getEntityName()); // NOI18N
relations.add(relation);
roleAHelper.getCMPMapping().getCmrFieldMapping().put(roleACmr, getLocalColumnData(key));
roleBHelper.getCMPMapping().getCmrFieldMapping().put(roleBCmr, getReferencedColumnData(key));
}
private void reset() {
beans.clear();
relations.clear();
}
private void buildCMPSet() {
reset();
addAllTables();
for (Iterator it = beans.keySet().iterator(); it.hasNext();) {
String tableName = it.next().toString();
TableElement table = schemaElement.getTable(DBIdentifier.create(tableName));
ColumnElement[] cols = table.getColumns();
UniqueKeyElement pk = getPrimaryOrCandidateKey(table);
ForeignKeyElement[] fkeys = table.getForeignKeys();
//sometimes database may contain duplicating foreign keys (or it may be an issue in db schema generation)
fkeys = removeDuplicateFK(fkeys);
for (int col = 0; col < cols.length; col++) {
if (pk != null &&
pk.getColumn(cols[col].getName()) != null) {
generatePkField(cols[col],true, pk.getColumns().length==1);
} else {
// TODO add check to see if table is included
if (!isForeignKey(fkeys, cols[col])){
generatePkField(cols[col], false, false);
}
}
}
for (int fk = 0 ; fkeys != null && fkeys.length > fk; fk++) {
generateRelationship(fkeys[fk]);
}
EntityClass helperData = getBean(tableName);
helperData.usePkField(pk!= null && pk.getColumns().length == 1);
helperData.setIsForTable(table.isTable());
}
makeRelationsUnique();
}
private List getFieldNames(EntityClass bean) {
List result = new ArrayList();
for (Iterator i = bean.getFields().iterator(); i.hasNext();) {
EntityMember member = (EntityMember)i.next();
result.add(member.getMemberName());
}
for (Iterator i = bean.getRoles().iterator(); i.hasNext();) {
RelationshipRole role = (RelationshipRole)i.next();
result.add(role.getFieldName());
}
return result;
}
/**
* This method will make the relationships unique
*/
private EntityRelation[] makeRelationsUnique() {
EntityRelation[] r = getRelations();
List relationNames = new ArrayList(r.length);
for (int i = 0; i < r.length; i++) {
r[i].makeRoleNamesUnique();
String baseName = r[i].getRelationName();
r[i].setRelationName(uniqueAlgorithm(relationNames, baseName, "-")); // NOI18N
}
return r;
}
/**
* return name generated or base name if this was ok
*/
private static String uniqueAlgorithm(List names, String baseName, String sep) {
String newName = baseName;
int unique = 0;
while (names.contains(newName)) {
String ins = (sep == null? "":sep); // NOI18N
newName = baseName + ins + String.valueOf(++unique);
}
names.add(newName);
return newName;
}
/*
* may be used for issue 177341 fix later
*/
private ForeignKeyElement[] removeDuplicateFK(ForeignKeyElement[] fkeys) {
if(fkeys==null || fkeys.length==0) return fkeys;
HashMap<ComparableFK, ForeignKeyElement> ret = new HashMap<ComparableFK, ForeignKeyElement>();
for(int i=0;i<fkeys.length;i++)
{
ForeignKeyElement key=fkeys[i];
ComparableFK fkc=new ComparableFK(key);
if(ret.get(fkc)!=null){//we already have the same key
LOGGER.log(Level.INFO,key.getName().getFullName()+" key in "+key.getDeclaringTable().getName().getFullName() + " is considered as a duplicate, you may need to verify your schema or database structure.");//NOI18N
continue;
} else {
ret.put(fkc, key);
}
}
return (ForeignKeyElement[]) ret.values().toArray(new ForeignKeyElement[]{});
}
/**
* @return the useDefaults
*/
public boolean isUseDefaults() {
return useDefaults;
}
/**
* consider equal if refernced from/to the same tables with the same set of columns, fk name do not matter
*/
private class ComparableFK
{
private ForeignKeyElement key;
private String tableName;
private String refName;
private ColumnElement[] lc;
private ColumnElement[] rc;
ComparableFK(ForeignKeyElement fk)
{
key=fk;
tableName = key.getDeclaringTable().getName().getName();
refName = key.getReferencedTable().getName().getName();
lc = key.getLocalColumns();
rc = key.getReferencedColumns();
Arrays.sort(lc);
Arrays.sort(rc);
}
@Override
public int hashCode() {
return tableName.hashCode();
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final ComparableFK other = (ComparableFK) obj;
if ((this.tableName == null) ? (other.tableName != null) : !this.tableName.equals(other.tableName)) {
return false;
}
if ((this.refName == null) ? (other.refName != null) : !this.refName.equals(other.refName)) {
return false;
}
if (!Arrays.deepEquals(this.lc, other.lc)) {
return false;
}
if (!Arrays.deepEquals(this.rc, other.rc)) {
return false;
}
return true;
}
}
}