/**
* 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.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang.StringUtils;
import org.jumpmind.db.sql.ISqlRowMapper;
import org.jumpmind.db.sql.ISqlTemplate;
import org.jumpmind.db.sql.Row;
import org.jumpmind.extension.IBuiltInExtensionPoint;
import org.jumpmind.symmetric.SyntaxParsingException;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A data router that uses a lookup table to map data to nodes
*/
public class LookupTableDataRouter extends AbstractDataRouter implements IDataRouter, IBuiltInExtensionPoint {
final static Logger log = LoggerFactory.getLogger(LookupTableDataRouter.class);
public final static String PARAM_TABLE = "LOOKUP_TABLE";
public final static String PARAM_KEY_COLUMN = "KEY_COLUMN";
public final static String PARAM_MAPPED_KEY_COLUMN = "LOOKUP_KEY_COLUMN";
public final static String PARAM_EXTERNAL_ID_COLUMN = "EXTERNAL_ID_COLUMN";
final static String EXPRESSION_KEY = String.format("%s.Expression.",
LookupTableDataRouter.class.getName());
final static String LOOKUP_TABLE_KEY = String.format("%s.Table.",
LookupTableDataRouter.class.getName());
private ISymmetricDialect symmetricDialect;
public LookupTableDataRouter(ISymmetricDialect symmetricDialect) {
this.symmetricDialect = symmetricDialect;
}
public LookupTableDataRouter() {
}
public Set<String> routeToNodes(SimpleRouterContext routingContext, DataMetaData dataMetaData,
Set<Node> nodes, boolean initialLoad, boolean initialLoadSelectUsed, TriggerRouter triggerRouter) {
Set<String> nodeIds = null;
Router router = dataMetaData.getRouter();
Map<String, String> params = null;
params = getParams(router, routingContext);
Map<String, String> dataMap = getDataMap(dataMetaData, symmetricDialect);
Map<String, Set<String>> lookupTable = getLookupTable(params, router, routingContext);
String column = params.get(PARAM_KEY_COLUMN);
if (dataMap.containsKey(column)) {
String keyData = dataMap.get(column);
Set<String> externalIds = lookupTable.get(keyData);
if (externalIds != null) {
for (Node node : nodes) {
if (externalIds.contains(node.getExternalId())) {
nodeIds = addNodeId(node.getNodeId(), nodeIds, nodes);
}
}
}
} else {
log.error(
"Could not route data with an id of {} using the {} router because the column {} was not captured for the {} table",
new Object[] { dataMetaData.getData().getDataId(), getClass().getSimpleName(),
column, dataMetaData.getTable().getName() });
}
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 Map<String, String> getParams(Router router, SimpleRouterContext routingContext) {
final String KEY = EXPRESSION_KEY + router.getRouterId();
Map<String, String> params = (Map<String, String>) routingContext.getContextCache()
.get(KEY);
if (params == null) {
params = parse(router.getRouterExpression());
routingContext.getContextCache().put(KEY, params);
}
return params;
}
public Map<String, String> parse(String routerExpression) throws SyntaxParsingException {
boolean valid = true;
Map<String, String> params = new HashMap<String, String>();
if (!StringUtils.isBlank(routerExpression)) {
String[] expTokens = routerExpression.split("\r\n|\r|\n|\\s");
if (expTokens != null) {
for (String t : expTokens) {
if (!StringUtils.isBlank(t)) {
String[] tokens = t.split("=");
/*
* tokens must be trimmed, removing leading and trailing spaces
*/
if (tokens.length == 2 && !params.containsKey(tokens[0].trim())) {
params.put(tokens[0].trim(), tokens[1].trim());
} else {
valid = false;
break;
}
}
}
if (!valid ||
params.size() != 4 ||
!params.containsKey(PARAM_TABLE) ||
!params.containsKey(PARAM_KEY_COLUMN) ||
!params.containsKey(PARAM_MAPPED_KEY_COLUMN) ||
!params.containsKey(PARAM_EXTERNAL_ID_COLUMN)) {
log.warn("The provided lookup table router expression was invalid. The full expression is " + routerExpression + ".");
throw new SyntaxParsingException("The provided lookup table router expression was invalid. The full expression is " + routerExpression + ".");
}
}
}
else {
log.warn("The provided lookup table router expression is empty");
}
return params;
}
@SuppressWarnings("unchecked")
protected Map<String, Set<String>> getLookupTable(final Map<String, String> params, Router router,
SimpleRouterContext routingContext) {
final String CTX_CACHE_KEY = LOOKUP_TABLE_KEY + "." + params.get(PARAM_TABLE);
Map<String, Set<String>> lookupMap = (Map<String, Set<String>>) routingContext
.getContextCache().get(CTX_CACHE_KEY);
if (lookupMap == null) {
ISqlTemplate template = symmetricDialect.getPlatform().getSqlTemplate();
final Map<String, Set<String>> fillMap = new HashMap<String, Set<String>>();
template.query(String.format("select %s, %s from %s",
params.get(PARAM_MAPPED_KEY_COLUMN), params.get(PARAM_EXTERNAL_ID_COLUMN),
params.get(PARAM_TABLE)), new ISqlRowMapper<Object>() {
public Object mapRow(Row rs) {
String key = rs.getString(params.get(PARAM_MAPPED_KEY_COLUMN));
String value = rs.getString(params.get(PARAM_EXTERNAL_ID_COLUMN));
Set<String> ids = fillMap.get(key);
if (ids == null) {
ids = new HashSet<String>();
fillMap.put(key, ids);
}
ids.add(value);
return value;
}
});
lookupMap = fillMap;
routingContext.getContextCache().put(CTX_CACHE_KEY, lookupMap);
}
return lookupMap;
}
}