/*
* Copyright 2007 - 2017 the original author or authors.
*
* 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 net.sf.jailer.datamodel;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import net.sf.jailer.restrictionmodel.RestrictionModel;
import net.sf.jailer.util.SqlUtil;
/**
* An association between database-tables.
*
* @author Ralf Wisser
*/
public class Association extends ModelElement {
/**
* The source table.
*/
public final Table source;
/**
* The destination table.
*/
public final Table destination;
/**
* The join-condition for joining source with destination table.
*/
private String joinCondition;
/**
* The cardinality.
*/
private final Cardinality cardinality;
/**
* Whether or not to insert source-rows before destination rows in order to
* prevent foreign-key-constraint violation.
*/
private final boolean insertSourceBeforeDestination;
/**
* Whether or not to insert destination-rows before source-rows in order to
* prevent foreign-key-constraint violation.
*/
private final boolean insertDestinationBeforeSource;
/**
* <code>true</code> for reversed association.
*/
public final boolean reversed;
/**
* The name of the association.
*/
private String name;
/**
* The counterpart of the association for the reversal direction.
*/
public Association reversalAssociation = null;
/**
* The XML aggregation schema.
*/
private AggregationSchema aggregationSchema = AggregationSchema.NONE;
/**
* Name of XML-tag used for aggregation.
*/
private String aggregationTagName;
/**
* Data-model containing this association.
*/
private final DataModel dataModel;
/**
* Unique association ID. -1 if id is not yet defined.
*/
int id = -1;
/**
* Constructor.
*
* @param source
* the source table
* @param destination
* the destination table
* @param joinCondition
* the join-condition for join with destination table
* @param insertSourceBeforeDestination
* whether or not to insert source-rows before destination rows
* in order to prevent foreign-key-constraint violation
* @param insertDestinationBeforeSource
* whether or not to insert destination-rows before source-rows
* in order to prevent foreign-key-constraint violation
* @param dataModel
* data-model containing this association
* @param reversed
* <code>true</code> for reversed association
* @param cardinality
* the cardinality (optional)
*/
public Association(Table source, Table destination, boolean insertSourceBeforeDestination,
boolean insertDestinationBeforeSource, String joinCondition, DataModel dataModel, boolean reversed,
Cardinality cardinality) {
this(source, destination, insertSourceBeforeDestination, insertDestinationBeforeSource, joinCondition,
dataModel, reversed, cardinality, null);
}
/**
* Constructor.
*
* @param source
* the source table
* @param destination
* the destination table
* @param joinCondition
* the join-condition for join with destination table
* @param insertSourceBeforeDestination
* whether or not to insert source-rows before destination rows
* in order to prevent foreign-key-constraint violation
* @param insertDestinationBeforeSource
* whether or not to insert destination-rows before source-rows
* in order to prevent foreign-key-constraint violation
* @param dataModel
* data-model containing this association
* @param reversed
* <code>true</code> for reversed association
* @param cardinality
* the cardinality (optional)
* @param author
* the author
*/
public Association(Table source, Table destination, boolean insertSourceBeforeDestination,
boolean insertDestinationBeforeSource, String joinCondition, DataModel dataModel, boolean reversed,
Cardinality cardinality, String author) {
this.source = source;
this.destination = destination;
this.insertSourceBeforeDestination = insertSourceBeforeDestination;
this.insertDestinationBeforeSource = insertDestinationBeforeSource;
this.joinCondition = joinCondition;
this.dataModel = dataModel;
this.reversed = reversed;
this.cardinality = cardinality;
if (author != null) {
setAuthor(author);
}
}
/**
* Gets the restricted join-condition for joining source with destination
* table.
*
* @return the restricted join-condition for joining source with destination
* table, <code>null</code> if association must be ignored
*/
public String getJoinCondition() {
if (dataModel.getRestrictionModel() != null) {
String restriction = dataModel.getRestrictionModel().getRestriction(this);
if (restriction != null) {
if (restriction == RestrictionModel.IGNORE) {
return null;
}
return "(" + joinCondition + ") and " + restriction;
}
}
return joinCondition;
}
/**
* Is this association ignored?
*
* @return <code>true</code> iff this association is ignored
*/
public boolean isIgnored() {
RestrictionModel restrictionModel = dataModel.getRestrictionModel();
String restriction = "";
if (restrictionModel != null) {
restriction = restrictionModel.getRestriction(this);
if (restriction == RestrictionModel.IGNORE) {
return true;
}
}
return false;
}
/**
* Gets the cardinality.
*
* @return the cardinality. <code>null</code> if cardinality is not known.
*/
public Cardinality getCardinality() {
return cardinality;
}
/**
* Stringifies the association.
*/
public String toString() {
return toString(30, false);
}
/**
* Stringifies the association.
*/
public String toString(int maxGab, boolean useDisplayName) {
RestrictionModel restrictionModel = dataModel.getRestrictionModel();
String restriction = "";
if (restrictionModel != null) {
String r = restrictionModel.getRestriction(this);
if (r != null && r != RestrictionModel.IGNORE) {
restriction = " restricted by " + r.replace('\n', ' ').replace('\r', ' ');
}
}
String gap = "";
String aName = useDisplayName ? dataModel.getDisplayName(destination) : destination.getName();
if (name != null) {
aName += " (" + name + ")";
}
while ((aName + gap).length() < maxGab) {
gap += " ";
}
String jc = joinCondition;
String r = restriction;
if (reversed) {
jc = SqlUtil.reversRestrictionCondition(jc);
r = SqlUtil.reversRestrictionCondition(r);
}
String card = " ";
if (cardinality != null) {
card = cardinality.toString();
}
return aName + gap + " " + card + " on " + jc + r;
}
/**
* Stringifies the join condition.
*
* @param restrictionSeparator
* separates join-condition from restriction condition in the
* result
*/
public String renderJoinCondition(String restrictionSeparator) {
RestrictionModel restrictionModel = dataModel.getRestrictionModel();
String restriction = "";
if (restrictionModel != null) {
String r = restrictionModel.getRestriction(this);
if (r != null && r != RestrictionModel.IGNORE) {
restriction = " " + restrictionSeparator + " " + r;
}
}
String jc = joinCondition;
String r = restriction;
if (reversed) {
jc = SqlUtil.reversRestrictionCondition(jc);
r = SqlUtil.reversRestrictionCondition(r);
}
return jc + r;
}
/**
* Gets join-condition without any restrictions.
*
* @return join-condition as defined in data model
*/
public String getUnrestrictedJoinCondition() {
return joinCondition;
}
/**
* Gets restriction-condition.
*
* @return restriction-condition, <code>null</code> if association is not
* restricted
*/
public String getRestrictionCondition() {
RestrictionModel restrictionModel = dataModel.getRestrictionModel();
if (restrictionModel != null) {
String r = restrictionModel.getRestriction(this);
if (r != null) {
if (r == RestrictionModel.IGNORE) {
return "false";
}
if (reversed) {
r = SqlUtil.reversRestrictionCondition(r);
}
if (r.startsWith("(") && r.endsWith(")")) {
r = r.substring(1, r.length() - 1);
}
return r;
}
}
return null;
}
/**
* Sets the name of the association.
*
* @param name
* the name of the association
*/
public void setName(String name) {
this.name = name;
if (dataModel != null) {
dataModel.version++;
}
}
/**
* Gets the name of the association.
*
* @return the name of the association
*/
public String getName() {
return name;
}
/**
* Whether or not to insert source-rows before destination rows in order to
* prevent foreign-key-constraint violation.
*
* @return the insertSourceBeforeDestination
*/
public boolean isInsertSourceBeforeDestination() {
if (dataModel.getRestrictionModel() != null && dataModel.getRestrictionModel().isTransposed()) {
return reversalAssociation.insertSourceBeforeDestination;
}
return insertSourceBeforeDestination;
}
/**
* Whether or not to insert destination-rows before source-rows in order to
* prevent foreign-key-constraint violation.
*
* @return the insertDestinationBeforeSource
*/
public boolean isInsertDestinationBeforeSource() {
if (dataModel.getRestrictionModel() != null && dataModel.getRestrictionModel().isTransposed()) {
return reversalAssociation.insertDestinationBeforeSource;
}
return insertDestinationBeforeSource;
}
/**
* Whether there is any restriction of this association.
*/
public boolean isRestricted() {
RestrictionModel restrictionModel = dataModel.getRestrictionModel();
String restriction = "";
if (restrictionModel != null) {
restriction = restrictionModel.getRestriction(this);
if (restriction != null) {
return true;
}
}
return false;
}
/**
* Appends condition to join-condition.
*
* @param condition
* the condition
*/
public void appendCondition(String condition) {
if (joinCondition == null) {
joinCondition = condition;
} else {
joinCondition = "(" + joinCondition + ") and (" + condition + ")";
}
}
/**
* Gets the XML aggregation schema.
*
* @return the XML aggregation schema
*/
public AggregationSchema getAggregationSchema() {
return aggregationSchema;
}
/**
* Gets name of XML-tag used for aggregation.
*
* @return name of XML-tag used for aggregation
*/
public String getAggregationTagName() {
String tag;
if (aggregationTagName == null) {
if (name.startsWith("inverse-")) {
tag = destination.getUnqualifiedName().toLowerCase();
} else {
tag = name.toLowerCase();
}
} else {
tag = aggregationTagName;
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < tag.length(); ++i) {
char c = tag.charAt(i);
if (Character.isUpperCase(c) || Character.isLowerCase(c) || Character.isDigit(c) || c == '-' || c == '_') {
sb.append(c);
}
}
return sb.toString();
}
/**
* Sets the XML aggregation schema.
*
* @param aggregationSchema
* the XML aggregation schema
*/
public void setAggregationSchema(AggregationSchema aggregationSchema) {
this.aggregationSchema = aggregationSchema;
if (dataModel != null) {
dataModel.version++;
}
}
/**
* Sets name of XML-tag used for aggregation.
*
* @param aggregationTagName
* name of XML-tag used for aggregation
*/
public void setAggregationTagName(String aggregationTagName) {
this.aggregationTagName = aggregationTagName;
if (dataModel != null) {
dataModel.version++;
}
}
/**
* Gets unique ID.
*
* @return unique ID
*/
public int getId() {
if (id < 0) {
dataModel.assignAssociationIDs();
}
return id;
}
/**
* Gets data-model to which this association belongs to.
*/
public DataModel getDataModel() {
return dataModel;
}
/**
* Maps source-columns to destination-columns, if this represents an
* equi-join. Otherwise it returns an empty map.
*
* @return map from source-columns to destination-columns, if this
* represents an equi-join
*/
public Map<Column, Column> createSourceToDestinationKeyMapping() {
String[] equations = getUnrestrictedJoinCondition().trim().replaceAll("\\(|\\)", " ")
.split("\\s*\\b(a|A)(n|N)(d|D)\\b\\s*");
Map<Column, Column> mapping = new HashMap<Column, Column>();
for (String equation: equations) {
String hs[] = equation.split("\\s*=\\s*");
if (hs.length != 2) {
return Collections.emptyMap();
}
String lhs[] = hs[0].split("\\s*\\.\\s*");
String rhs[] = hs[1].split("\\s*\\.\\s*");
if (lhs.length != 2 || rhs.length != 2 || lhs[0].length() != 1 || rhs[0].length() != 1) {
return Collections.emptyMap();
}
String dColumn = null, sColumn = null;
if ("A".equalsIgnoreCase(lhs[0])) {
sColumn = lhs[1];
}
if ("B".equalsIgnoreCase(lhs[0])) {
dColumn = lhs[1];
}
if ("A".equalsIgnoreCase(rhs[0])) {
sColumn = rhs[1];
}
if ("B".equalsIgnoreCase(rhs[0])) {
dColumn = rhs[1];
}
if (sColumn == null || dColumn == null) {
return Collections.emptyMap();
}
if (reversed) {
String h = sColumn;
sColumn = dColumn;
dColumn = h;
}
Column sourceColumn = null;
for (Column c : source.getColumns()) {
if (c.name.equals(sColumn)) {
sourceColumn = c;
break;
}
}
Column destinationColumn = null;
for (Column c : destination.getColumns()) {
if (c.name.equals(dColumn)) {
destinationColumn = c;
break;
}
}
if (sourceColumn == null || destinationColumn == null) {
return Collections.emptyMap();
}
mapping.put(sourceColumn, destinationColumn);
}
return mapping;
}
}