/** * Licensed to JumpMind Inc under one or more contributor * license agreements. See the NOTICE file distributed * with this work for additional information regarding * copyright ownership. JumpMind Inc licenses this file * to you under the GNU General Public License, version 3.0 (GPLv3) * (the "License"); you may not use this file except in compliance * with the License. * * You should have received a copy of the GNU General Public License, * version 3.0 (GPLv3) along with this library; if not, see * <http://www.gnu.org/licenses/>. * * 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.jumpmind.symmetric.model; import java.io.Serializable; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.List; import java.util.StringTokenizer; import org.apache.commons.lang.StringUtils; import org.jumpmind.db.model.Column; import org.jumpmind.db.model.Table; import org.jumpmind.symmetric.common.Constants; import org.jumpmind.util.FormatUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Defines the trigger via which a table will be synchronized. */ public class Trigger implements Serializable { private static final long serialVersionUID = 1L; static final Logger log = LoggerFactory.getLogger(Trigger.class); private static final String DEFAULT_CONDITION = "1=1"; private String triggerId; private String sourceTableName; private String sourceSchemaName; private String sourceCatalogName; private String channelId = Constants.CHANNEL_DEFAULT; private String reloadChannelId = Constants.CHANNEL_RELOAD; private boolean syncOnUpdate = true; private boolean syncOnInsert = true; private boolean syncOnDelete = true; private boolean syncOnIncomingBatch = false; private boolean useStreamLobs = false; private boolean useCaptureLobs = false; private boolean useCaptureOldData = true; private boolean useHandleKeyUpdates = false; private String nameForInsertTrigger; private String nameForUpdateTrigger; private String nameForDeleteTrigger; private String syncOnUpdateCondition = DEFAULT_CONDITION; private String syncOnInsertCondition = DEFAULT_CONDITION; private String syncOnDeleteCondition = DEFAULT_CONDITION; private String channelExpression = null; private String customOnUpdateText; private String customOnInsertText; private String customOnDeleteText; private String excludedColumnNames = null; private String syncKeyNames = null; /** * This is a SQL expression that creates a unique id which the sync process * can use to 'group' events together and commit together. */ private String txIdExpression = null; private String externalSelect = null; private Date createTime; private Date lastUpdateTime; private String lastUpdateBy; public Trigger() { } public Trigger(String tableName, String channelId) { this.triggerId = tableName; this.sourceTableName = tableName; this.channelId = channelId; } public Trigger(String tableName, String channelId, boolean syncOnIncomingBatch) { this(tableName, channelId); this.syncOnIncomingBatch = syncOnIncomingBatch; } final public String qualifiedSourceTableName() { return qualifiedSourceTablePrefix() + sourceTableName; } final public String qualifiedSourceTablePrefix() { String schemaPlus = (getSourceSchemaName() != null ? getSourceSchemaName() + "." : ""); String catalogPlus = (getSourceCatalogName() != null ? getSourceCatalogName() + "." : "") + schemaPlus; return catalogPlus; } public void nullOutBlankFields() { if (StringUtils.isBlank(sourceCatalogName)) { sourceCatalogName = null; } if (StringUtils.isBlank(sourceSchemaName)) { sourceSchemaName = null; } } public Column[] filterExcludedColumns(Column[] src) { if (src != null) { List<String> excludedColumnNames = getExcludedColumnNamesAsList(); List<Column> filtered = new ArrayList<Column>(src.length); for (int i = 0; i < src.length; i++) { Column col = src[i]; if (!excludedColumnNames.contains(col.getName().toLowerCase())) { filtered.add(col); } } return filtered.toArray(new Column[filtered.size()]); } else { return new Column[0]; } } public Column[] getSyncKeysColumnsForTable(Table table) { List<String> syncKeys = getSyncKeyNamesAsList(); if (syncKeys.size() > 0) { List<Column> columns = new ArrayList<Column>(); for (String syncKey : syncKeys) { Column col = table.getColumnWithName(syncKey); if (col != null) { columns.add(col); } else { log.error("The sync key column '{}' was specified for the '{}' trigger but was not found in the table", syncKey, triggerId); } } if (columns.size() > 0) { return columns.toArray(new Column[columns.size()]); } else { return table.getPrimaryKeyColumns(); } } else { return table.getPrimaryKeyColumns(); } } /** * When dealing with columns, always use this method to order the columns so * that the primary keys are first. */ public Column[] orderColumnsForTable(Table table) { if (table != null) { Column[] pks = getSyncKeysColumnsForTable(table); Column[] cols = table.getColumns(); List<Column> orderedColumns = new ArrayList<Column>(cols.length); for (int i = 0; i < pks.length; i++) { orderedColumns.add(pks[i]); } for (int i = 0; i < cols.length; i++) { boolean syncKey = false; for (int j = 0; j < pks.length; j++) { if (cols[i].getName().equals(pks[j].getName())) { syncKey = true; break; } } if (!syncKey) { orderedColumns.add(cols[i]); } } Column[] result = orderedColumns.toArray(new Column[orderedColumns.size()]); return filterExcludedColumns(result); } else { return new Column[0]; } } @SuppressWarnings("unchecked") private List<String> getExcludedColumnNamesAsList() { if (excludedColumnNames != null && excludedColumnNames.length() > 0) { StringTokenizer tokenizer = new StringTokenizer(excludedColumnNames, ","); List<String> columnNames = new ArrayList<String>(tokenizer.countTokens()); while (tokenizer.hasMoreTokens()) { columnNames.add(tokenizer.nextToken().toLowerCase().trim()); } return columnNames; } else { return Collections.EMPTY_LIST; } } public boolean hasChangedSinceLastTriggerBuild(Date lastTriggerBuildTime) { return lastTriggerBuildTime == null || getLastUpdateTime() == null || lastTriggerBuildTime.before(getLastUpdateTime()); } public String getTriggerId() { return triggerId; } public void setTriggerId(String triggerId) { this.triggerId = triggerId; } public String getSourceTableName() { return sourceTableName; } public boolean isSourceTableNameWildCarded() { return sourceTableName != null && (sourceTableName.contains(FormatUtils.WILDCARD) || sourceTableName.contains(",")); } public boolean isSourceCatalogNameWildCarded() { return sourceCatalogName != null && (sourceCatalogName.contains(FormatUtils.WILDCARD) || sourceCatalogName.contains(",")); } public boolean isSourceSchemaNameWildCarded() { return sourceSchemaName != null && (sourceSchemaName.contains(FormatUtils.WILDCARD) || sourceSchemaName.contains(",")); } public String getChannelExpression() { return channelExpression; } public void setChannelExpression(String channelExpression) { this.channelExpression = channelExpression; } public void setSourceTableName(String sourceTableName) { this.sourceTableName = sourceTableName; } public String getSourceSchemaName() { return sourceSchemaName; } public void setSourceSchemaName(String sourceSchemaName) { this.sourceSchemaName = sourceSchemaName; } public String getSourceCatalogName() { return sourceCatalogName; } public void setSourceCatalogName(String sourceCatalogName) { this.sourceCatalogName = sourceCatalogName; } public String getChannelId() { return channelId; } public void setChannelId(String channelId) { this.channelId = channelId; } public String getReloadChannelId() { return reloadChannelId; } public void setReloadChannelId(String reloadChannelId) { this.reloadChannelId = reloadChannelId; } public boolean isSyncOnUpdate() { return syncOnUpdate; } public void setSyncOnUpdate(boolean syncOnUpdate) { this.syncOnUpdate = syncOnUpdate; } public boolean isSyncOnInsert() { return syncOnInsert; } public void setSyncOnInsert(boolean syncOnInsert) { this.syncOnInsert = syncOnInsert; } public boolean isSyncOnDelete() { return syncOnDelete; } public void setSyncOnDelete(boolean syncOnDelete) { this.syncOnDelete = syncOnDelete; } public boolean isSyncOnIncomingBatch() { return syncOnIncomingBatch; } public void setSyncOnIncomingBatch(boolean syncOnIncomingBatch) { this.syncOnIncomingBatch = syncOnIncomingBatch; } public String getNameForInsertTrigger() { return nameForInsertTrigger; } public void setNameForInsertTrigger(String nameForInsertTrigger) { this.nameForInsertTrigger = nameForInsertTrigger; } public String getNameForUpdateTrigger() { return nameForUpdateTrigger; } public void setNameForUpdateTrigger(String nameForUpdateTrigger) { this.nameForUpdateTrigger = nameForUpdateTrigger; } public String getNameForDeleteTrigger() { return nameForDeleteTrigger; } public void setNameForDeleteTrigger(String nameForDeleteTrigger) { this.nameForDeleteTrigger = nameForDeleteTrigger; } public String getSyncOnUpdateCondition() { return syncOnUpdateCondition; } public void setSyncOnUpdateCondition(String syncOnUpdateCondition) { this.syncOnUpdateCondition = syncOnUpdateCondition; } public String getSyncOnInsertCondition() { return syncOnInsertCondition; } public void setSyncOnInsertCondition(String syncOnInsertCondition) { this.syncOnInsertCondition = syncOnInsertCondition; } public String getSyncOnDeleteCondition() { return syncOnDeleteCondition; } public void setSyncOnDeleteCondition(String syncOnDeleteCondition) { this.syncOnDeleteCondition = syncOnDeleteCondition; } public String getCustomOnUpdateText() { return customOnUpdateText; } public void setCustomOnUpdateText(String customOnUpdateText) { this.customOnUpdateText = customOnUpdateText; } public String getCustomOnInsertText() { return customOnInsertText; } public void setCustomOnInsertText(String customOnInsertText) { this.customOnInsertText = customOnInsertText; } public String getCustomOnDeleteText() { return customOnDeleteText; } public void setCustomOnDeleteText(String customOnDeleteText) { this.customOnDeleteText = customOnDeleteText; } public String getExcludedColumnNames() { return excludedColumnNames; } public void setExcludedColumnNames(String excludedColumnNames) { this.excludedColumnNames = excludedColumnNames; } public String getTxIdExpression() { return txIdExpression; } public void setTxIdExpression(String txIdExpression) { this.txIdExpression = txIdExpression; } public String getExternalSelect() { return externalSelect; } public void setExternalSelect(String externalSelect) { this.externalSelect = externalSelect; } public void setLastUpdateBy(String updatedBy) { this.lastUpdateBy = updatedBy; } public String getLastUpdateBy() { return lastUpdateBy; } public Date getLastUpdateTime() { return lastUpdateTime; } public void setLastUpdateTime(Date lastModifiedOn) { this.lastUpdateTime = lastModifiedOn; } public Date getCreateTime() { return createTime; } public void setCreateTime(Date createdOn) { this.createTime = createdOn; } public void setUseStreamLobs(boolean useStreamLobs) { this.useStreamLobs = useStreamLobs; } public boolean isUseStreamLobs() { return useStreamLobs; } public void setUseCaptureLobs(boolean useCaptureLobs) { this.useCaptureLobs = useCaptureLobs; } public boolean isUseCaptureLobs() { return useCaptureLobs; } public boolean isUseHandleKeyUpdates() { return useHandleKeyUpdates; } public void setUseHandleKeyUpdates(boolean useHandleKeyUpdates) { this.useHandleKeyUpdates = useHandleKeyUpdates; } public void setUseCaptureOldData(boolean useCaptureOldData) { this.useCaptureOldData = useCaptureOldData; } public boolean isUseCaptureOldData() { return useCaptureOldData; } public void setSyncKeyNames(String syncKeys) { this.syncKeyNames = syncKeys; } public String getSyncKeyNames() { return syncKeyNames; } @SuppressWarnings("unchecked") private List<String> getSyncKeyNamesAsList() { if (syncKeyNames != null && syncKeyNames.length() > 0) { StringTokenizer tokenizer = new StringTokenizer(syncKeyNames, ","); List<String> columnNames = new ArrayList<String>(tokenizer.countTokens()); while (tokenizer.hasMoreTokens()) { columnNames.add(tokenizer.nextToken().toLowerCase().trim()); } return columnNames; } else { return Collections.EMPTY_LIST; } } public String getFullyQualifiedSourceTableName() { return Table.getFullyQualifiedTableName(sourceCatalogName, sourceSchemaName, sourceTableName); } public long toHashedValue() { long hashedValue = triggerId != null ? triggerId.hashCode() : 0; if (null != sourceTableName) { hashedValue += sourceTableName.hashCode(); } if (null != channelId) { hashedValue += channelId.hashCode(); } if (null != sourceSchemaName) { hashedValue += sourceSchemaName.hashCode(); } if (null != sourceCatalogName) { hashedValue += sourceCatalogName.hashCode(); } hashedValue += syncOnUpdate ? "syncOnUpdate".hashCode() : 0; hashedValue += syncOnInsert ? "syncOnInsert".hashCode() : 0; hashedValue += syncOnDelete ? "syncOnDelete".hashCode() : 0; hashedValue += syncOnIncomingBatch ? "syncOnIncomingBatch".hashCode() : 0; hashedValue += useStreamLobs ? "useStreamLobs".hashCode() : 0; hashedValue += useCaptureLobs ? "useCaptureLobs".hashCode() : 0; hashedValue += useCaptureOldData ? "useCaptureOldData".hashCode() : 0; hashedValue += useHandleKeyUpdates ? "useHandleKeyUpdates".hashCode() : 0; if (null != nameForInsertTrigger) { hashedValue += nameForInsertTrigger.hashCode(); } if (null != nameForUpdateTrigger) { hashedValue += nameForUpdateTrigger.hashCode(); } if (null != nameForDeleteTrigger) { hashedValue += nameForDeleteTrigger.hashCode(); } if (null != syncOnUpdateCondition) { hashedValue += syncOnUpdateCondition.hashCode(); } if (null != syncOnInsertCondition) { hashedValue += syncOnInsertCondition.hashCode(); } if (null != syncOnDeleteCondition) { hashedValue += syncOnDeleteCondition.hashCode(); } if (null != customOnUpdateText) { hashedValue += customOnUpdateText.hashCode(); } if (null != customOnInsertText) { hashedValue += customOnInsertText.hashCode(); } if (null != customOnDeleteText) { hashedValue += customOnDeleteText.hashCode(); } if (null != excludedColumnNames) { hashedValue += excludedColumnNames.hashCode(); } if (null != externalSelect) { hashedValue += externalSelect.hashCode(); } if (null != txIdExpression) { hashedValue += txIdExpression.hashCode(); } if (null != syncKeyNames) { hashedValue += syncKeyNames.hashCode(); } return hashedValue; } public boolean matchesCatalogName(String catalogName, boolean ignoreCase) { return matches(sourceCatalogName, catalogName, ignoreCase); } public boolean matchesSchemaName(String schemaName, boolean ignoreCase) { return matches(sourceSchemaName, schemaName, ignoreCase); } protected boolean matches(String match, String target, boolean ignoreCase) { boolean matches = false; String[] wildcardTokens = match.split(","); for (String wildcardToken : wildcardTokens) { if (FormatUtils.isWildCardMatch(target, wildcardToken, ignoreCase)) { if (!wildcardToken.startsWith(FormatUtils.NEGATE_TOKEN)) { matches = true; } else { matches = false; break; } } } return matches; } public boolean matches(Table table, String defaultCatalog, String defaultSchema, boolean ignoreCase) { boolean schemaAndCatalogMatch = (StringUtils.equals(sourceCatalogName, table.getCatalog()) || (StringUtils .isBlank(sourceCatalogName) && StringUtils.equals(defaultCatalog, table.getCatalog()))) && (StringUtils.equals(sourceSchemaName, table.getSchema()) || (StringUtils .isBlank(sourceSchemaName) && StringUtils.equals(defaultSchema, table.getSchema()))); boolean tableMatches = ignoreCase ? table.getName().equalsIgnoreCase(sourceTableName) : table.getName().equals(sourceTableName); if (!tableMatches && isSourceTableNameWildCarded()) { tableMatches = matches(sourceTableName, table.getName(), ignoreCase); } return schemaAndCatalogMatch && tableMatches; } public boolean matches(Trigger trigger) { return StringUtils.equals(sourceCatalogName, trigger.sourceCatalogName) && StringUtils.equals(sourceSchemaName, trigger.sourceSchemaName) && trigger.sourceTableName.equalsIgnoreCase(sourceTableName); } @Override public boolean equals(Object obj) { if (obj instanceof Trigger && triggerId != null) { return triggerId.equals(((Trigger) obj).triggerId); } else { return super.equals(obj); } } @Override public int hashCode() { return triggerId != null ? triggerId.hashCode() : super.hashCode(); } @Override public String toString() { if (triggerId != null) { return triggerId; } else { return super.toString(); } } }