/**
* 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.route;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.jumpmind.extension.IBuiltInExtensionPoint;
import org.jumpmind.symmetric.SyntaxParsingException;
import org.jumpmind.symmetric.common.TokenConstants;
import org.jumpmind.symmetric.db.ISymmetricDialect;
import org.jumpmind.symmetric.model.DataMetaData;
import org.jumpmind.symmetric.model.Node;
import org.jumpmind.symmetric.model.Router;
import org.jumpmind.symmetric.model.TriggerRouter;
import org.jumpmind.symmetric.service.IConfigurationService;
/**
* This data router is invoked when the router_type='column'. The
* router_expression is always a name value pair of a column on the table that
* is being synchronized to the value it should be matched with.
* <P>
* The value can be a constant. In the data router the value of the new data is
* always represented by a string so all comparisons are done in the format that
* SymmetricDS transmits.
* <P>
* The column name used for the match is the upper case column name if the
* current value is being compared. The upper case column name prefixed by OLD_
* can be used if the comparison is being done of the old data.
* <P>
* For example, if the column on a table is named STATUS you can specify that
* you want to router when STATUS=OK by specifying such for the
* router_expression. If you wanted to route when only the old value for
* STATUS=OK you would specify OLD_STATUS=OK.
* <P>
* The value can also be one of the following expressions:
* <ol>
* <li>:NODE_ID</li>
* <li>:EXTERNAL_ID</li>
* <li>:NODE_GROUP_ID</li>
* <li>:REDIRECT_NODE</li>
* <li>:{column name}</li>
* </ol>
* NODE_ID, EXTERNAL_ID, and NODE_GROUP_ID are instructions for the column
* matcher to select nodes that have a NODE_ID, EXTERNAL_ID or NODE_GROUP_ID
* that are equal to the value on the column.
* <P>
* REDIRECT_NODE is an instruction to match the specified column to a
* registrant_external_id on registration_redirect and return the associated
* registration_node_id in the list of node id to route to. For example, if the
* 'price' table was being routed to to a region 1 node based on the store_id,
* the store_id would be the external_id of a node in the registration_redirect
* table and the router_expression for trigger entry for the 'price' table would
* be 'store_id=:REDIRECT_NODE' and the router_type would be 'column'.
*/
public class ColumnMatchDataRouter extends AbstractDataRouter implements IDataRouter, IBuiltInExtensionPoint {
private static final String NULL_VALUE = "NULL";
private IConfigurationService configurationService;
private ISymmetricDialect symmetricDialect;
final static String EXPRESSION_KEY = String.format("%s.Expression.", ColumnMatchDataRouter.class
.getName());
public ColumnMatchDataRouter() {
}
public ColumnMatchDataRouter(IConfigurationService configurationService, ISymmetricDialect symmetricDialect) {
this.configurationService = configurationService;
this.symmetricDialect = symmetricDialect;
}
public Set<String> routeToNodes(SimpleRouterContext routingContext,
DataMetaData dataMetaData, Set<Node> nodes, boolean initialLoad, boolean initialLoadSelectUsed, TriggerRouter triggerRouter) {
Set<String> nodeIds = null;
if (initialLoadSelectUsed && initialLoad) {
nodeIds = toNodeIds(nodes, null);
} else {
List<Expression> expressions = getExpressions(dataMetaData.getRouter(), routingContext);
Map<String, String> columnValues = getDataMap(dataMetaData, symmetricDialect);
if (columnValues != null) {
for (Expression e : expressions) {
String column = e.tokens[0].trim();
String value = e.tokens[1];
String columnValue = columnValues.get(column);
if (value.equalsIgnoreCase(TokenConstants.NODE_ID)) {
for (Node node : nodes) {
nodeIds = runExpression(e, columnValue, node.getNodeId(), nodes,
nodeIds, node);
}
} else if (value.equalsIgnoreCase(TokenConstants.EXTERNAL_ID)) {
for (Node node : nodes) {
nodeIds = runExpression(e, columnValue, node.getExternalId(), nodes,
nodeIds, node);
}
} else if (value.equalsIgnoreCase(TokenConstants.NODE_GROUP_ID)) {
for (Node node : nodes) {
nodeIds = runExpression(e, columnValue, node.getNodeGroupId(), nodes,
nodeIds, node);
}
} else if (e.hasEquals && value.equalsIgnoreCase(TokenConstants.REDIRECT_NODE)) {
Map<String, String> redirectMap = getRedirectMap(routingContext);
String nodeId = redirectMap.get(columnValue);
if (nodeId != null) {
nodeIds = addNodeId(nodeId, nodeIds, nodes);
}
} else {
String compareValue = value;
if (value.equalsIgnoreCase(TokenConstants.EXTERNAL_DATA)) {
compareValue = dataMetaData.getData().getExternalData();
} else if (value.startsWith(":")) {
compareValue = columnValues.get(value.substring(1));
} else if (value.equals(NULL_VALUE)) {
compareValue = null;
}
nodeIds = runExpression(e, columnValue, compareValue, nodes, nodeIds, null);
}
}
} else {
log.warn("There were no columns to match for the data_id of {}", dataMetaData
.getData().getDataId());
}
}
if(nodeIds != null) {
nodeIds.remove(null);
} else {
nodeIds = Collections.emptySet();
}
return nodeIds;
}
protected Set<String> runExpression(Expression e, String columnValue, String compareValue, Set<Node> nodes, Set<String> nodeIds, Node node) {
boolean result = false;
if (e.hasEquals && ((columnValue == null && compareValue == null) ||
(columnValue != null && columnValue.equals(compareValue)))) {
result = true;
} else if (e.hasNotEquals && ((columnValue == null && compareValue != null) ||
(columnValue != null && !columnValue.equals(compareValue)))) {
result = true;
} else if (e.hasContains && columnValue != null && compareValue != null &&
ArrayUtils.contains(columnValue.split(","), compareValue)) {
result = true;
} else if (e.hasNotContains && columnValue != null && compareValue != null &&
!ArrayUtils.contains(columnValue.split(","), compareValue)) {
result = true;
}
if (result) {
if (node != null) {
nodeIds = addNodeId(node.getNodeId(), nodeIds, nodes);
} else {
nodeIds = toNodeIds(nodes, nodeIds);
}
}
return nodeIds;
}
/**
* Cache parsed expressions in the context to minimize the amount of parsing
* we have to do when we have lots of throughput.
*/
@SuppressWarnings("unchecked")
protected List<Expression> getExpressions(Router router, SimpleRouterContext context) {
final String KEY = EXPRESSION_KEY + router.getRouterId();
List<Expression> expressions = (List<Expression>) context.getContextCache().get(
KEY);
if (expressions == null) {
expressions = parse(router.getRouterExpression());
context.getContextCache().put(KEY, expressions);
}
return expressions;
}
public List<Expression> parse(String routerExpression) throws SyntaxParsingException {
List<Expression> expressions = new ArrayList<Expression>();
if (!StringUtils.isBlank(routerExpression)) {
String[] operators = { Expression.NOT_EQUALS, Expression.EQUALS, Expression.NOT_CONTAINS, Expression.CONTAINS};
String[] expTokens = routerExpression.split("\\s*(\\s+or|\\s+OR)?(\r\n|\r|\n)(or\\s+|OR\\s+)?\\s*" +
"|\\s+or\\s+" +
"|\\s+OR\\s+");
if (expTokens != null) {
for (String t : expTokens) {
if (!StringUtils.isBlank(t)) {
boolean isFound = false;
for (String operator : operators) {
if (t.contains(operator)) {
String[] tokens = t.split(operator);
if (tokens.length == 2) {
tokens[0] = parseColumn(tokens[0]);
tokens[1] = parseValue(tokens[1]);
expressions.add(new Expression(operator, tokens));
isFound = true;
break;
}
}
}
if (!isFound) {
log.warn("The provided column match expression was invalid: {}. The full expression is {}.", t, routerExpression);
throw new SyntaxParsingException("The provided column match expression was invalid: " + t + ". The full expression is " + routerExpression + ".");
}
}
}
}
} else {
log.warn("The provided column match expression is empty");
}
return expressions;
}
/**
* Parse a column (the first half of a column match expression).
*/
private String parseColumn(String value) {
return value.trim();
}
/**
* Parse a value (the second half of a column match expression).
*/
private String parseValue(String value) {
value = value.trim();
// Check for ticks around the value.
if (value.charAt(0) == '\''
&& value.charAt(value.length() - 1) == '\'') {
// remove first and last tick
value = value.substring(1,value.length()-1);
// replace all double ticks with a single tick only if value was surrounded with ticks
value = value.replaceAll("''", "'");
}
return value;
}
@SuppressWarnings("unchecked")
protected Map<String, String> getRedirectMap(SimpleRouterContext ctx) {
final String CTX_CACHE_KEY = ColumnMatchDataRouter.class.getSimpleName() + "RouterMap";
Map<String, String> redirectMap = (Map<String, String>) ctx.getContextCache().get(
CTX_CACHE_KEY);
if (redirectMap == null) {
redirectMap = configurationService.getRegistrationRedirectMap();
ctx.getContextCache().put(CTX_CACHE_KEY, redirectMap);
}
return redirectMap;
}
public class Expression {
public static final String EQUALS = "=";
public static final String NOT_EQUALS = "!=";
public static final String CONTAINS = "contains";
public static final String NOT_CONTAINS = "not contains";
boolean hasEquals;
boolean hasNotEquals;
boolean hasContains;
boolean hasNotContains;
String[] tokens;
String operator;
public Expression(String operator, String[] tokens) {
this.tokens = tokens;
this.operator = operator;
if (operator.equals(EQUALS)) hasEquals = true;
else if (operator.equals(NOT_EQUALS)) hasNotEquals = true;
else if (operator.equals(CONTAINS)) hasContains = true;
else if (operator.equals(NOT_CONTAINS)) hasNotContains = true;
}
public String[] getTokens() {
return tokens;
}
public String getOperator() {
return operator;
}
public boolean hasEquals() {
return hasEquals;
}
public boolean hasNotEquals() {
return hasEquals;
}
public boolean hasContains() {
return hasEquals;
}
public boolean hasNotContains() {
return hasEquals;
}
}
}