/**
* Copyright (c) Codice Foundation
* <p>
* This is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser
* General Public License as published by the Free Software Foundation, either version 3 of the
* License, or any later version.
* <p>
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details. A copy of the GNU Lesser General Public License
* is distributed along with this program and can be found at
* <http://www.gnu.org/licenses/lgpl.html>.
*/
package ddf.security.expansion.impl;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ddf.security.expansion.Expansion;
/**
* Base class for all expansion services. Provides the generic setting/getting for attribute
* separator as well as the actual map of expansion rules. Defines an abstrace
* <code>doExpansion</code> method to be overridden with the appropriate logic.
*/
public abstract class AbstractExpansion implements Expansion {
/**
* Default string that separates individual attributes in the replacement strings.
*/
public static final String DEFAULT_VALUE_SEPARATOR = " ";
/**
* Separator for various parts of the rules when presented as a string.
*/
public static final String RULE_SPLIT_REGEX = ":";
/**
* String to mark the line as a comment in the configuration file
*/
public static final String CFG_COMMENT_STR = "#";
/**
* String to identify the line containing the attribute separator string in the configuration
* file.
*/
public static final String SEPARATOR_PREFIX = "separator=";
/**
* Default filename for the user attribute mapping configuration file.
*/
public static final String DEFAULT_CONFIG_FILE_NAME = "ddf-user-attribute-ruleset.cfg";
protected static final Logger LOGGER = LoggerFactory.getLogger(RegexExpansion.class);
private static final String ATTRIBUTE_SEPARATOR = "attributeSeparator";
private static final String EXPANSION_FILE_NAME = "expansionFileName";
protected Pattern rulePattern = Pattern.compile(RULE_SPLIT_REGEX); // ("\\[(.+)\\|(.*)\\]");
protected Map<String, List<String[]>> expansionTable;
private String attributeSeparator = DEFAULT_VALUE_SEPARATOR;
private String expansionFilename = DEFAULT_CONFIG_FILE_NAME;
/*
* @see ddf.security.expansion.Expansion#expand(Map<String, Set<String>>)
*/
@Override
public Map<String, Set<String>> expand(Map<String, Set<String>> map) {
if ((map == null) || (map.isEmpty())) {
return map;
}
Set<String> expandedSet;
for (Map.Entry<String, Set<String>> thisKey : map.entrySet()) {
expandedSet = expand(thisKey.getKey(), thisKey.getValue());
map.put(thisKey.getKey(), expandedSet);
}
return map;
}
/*
* @see ddf.security.expansion.Expansion#expand(String, Set<String>)
*/
@Override
public Set<String> expand(String key, Set<String> values) {
Set<String> result;
// if there's nothing to expand, just return
if ((values == null) || (values.isEmpty())) {
return values;
}
// if no rules have been established yet, return the original
if ((expansionTable == null) || (expansionTable.isEmpty())) {
return values;
}
// if they didn't specify a key value, just return the original string
if ((key == null) || (key.isEmpty())) {
LOGGER.debug("Expand called with a null key value - no expansion attempted.");
return values;
}
List<String[]> mappingRuleList = expansionTable.get(key);
// if there are not matching keys in the expansion table - return the original string
if (mappingRuleList == null) {
return values;
}
/*
* This expansion loop builds on itself, so the order of the rules is important - the
* expanded set of strings is processed for expansion by subsequent rules.
*
* Each list element in the expansion table is a two-element array with the regular
* expression to search for and the replacement value. The replacement value can be empty in
* which case the found value is deleted.
*/
result = values;
String original;
String expandedValue;
Set<String> temp;
Set<String> expandedSet = new HashSet<String>();
Set<String> currentSet = new HashSet<String>();
currentSet.addAll(values);
LOGGER.debug("Original key of {} with value[s]: {}", key, values);
for (String[] rule : mappingRuleList) {
expandedSet.clear();
if ((rule != null) && (rule.length == 2)) {
if ((rule[0] != null) && (!rule[0].isEmpty())) {
LOGGER.trace("Processing expansion entry: {} => {}", rule[0], rule[1]);
// now go through and expand each string in the passed in set
for (String s : currentSet) {
original = s;
expandedValue = doExpansion(s, rule);
LOGGER.debug("Expanded value from '{}' to '{}'", original, expandedValue);
expandedSet.addAll(split(expandedValue, attributeSeparator));
}
}
} else {
LOGGER.debug("Expansion table contains invalid entries - skipping.");
}
temp = currentSet;
currentSet = expandedSet;
expandedSet = temp;
}
LOGGER.debug("Expanded result for key {} is {}", key, currentSet);
// update the original set passed in for expansion
values.clear();
values.addAll(currentSet);
return currentSet;
}
/**
* This is the method that will do the actual expansion - interpreting the rules and expanding
* the values. It is abstract and will be overridden by each concrete implementation.
*
* @param original the original value of the attribute
* @param rule the rule that describes the expansion for one specific attribute value
* @return the (possibly) expanded result of applying the rule to the original value
*/
protected abstract String doExpansion(String original, String[] rule);
/*
* @see ddf.security.expansion.Expansion#getExpansionMap()
*/
@Override
public Map<String, List<String[]>> getExpansionMap() {
if (expansionTable == null) {
return new HashMap<String, List<String[]>>();
}
return Collections.unmodifiableMap(expansionTable);
}
/**
* Sets the expansion map (which includes a set of keys corresponding to attribute names, each
* with a corresponding list of rules that apply to that attribute. If the passed in table is
* null, an empty expansion map is created.
*
* @param table the complete map of attributes and their corresponding list of rules
*/
public void setExpansionMap(Map<String, List<String[]>> table) {
if (table == null) {
expansionTable = new HashMap<String, List<String[]>>();
} else {
expansionTable = table;
}
}
/**
* Adds an individual expansion rule to the existing (or newly created) expansion map. If an
* entry already exists for the given attribute key, this rule is added to that entry, if an
* entry doesn't exist, it is created and then this rule added. If invalid input is received,
* nothing is done.
*
* @param key the attribute for which the corresponding rule should be added
* @param rule the expansion rule to apply to the corresponding attribute
*/
public void addExpansionRule(String key, String[] rule) {
if ((key == null) || (key.isEmpty()) || (rule == null) || (rule.length != 2)) {
LOGGER.warn("Attempt to add expansion rule with null/empty key or incomplete rule.");
return;
}
if ((rule[0] == null) || (rule[0].isEmpty())) {
LOGGER.warn("Attempt to add expansion rule with null/empty search criteria: {}",
rule[0]);
return;
}
if (expansionTable == null) {
expansionTable = new HashMap<String, List<String[]>>();
}
List<String[]> list = expansionTable.get(key);
if (list == null) {
list = new ArrayList<String[]>();
expansionTable.put(key, list);
}
list.add(rule);
}
/**
* Removes a single rule from the expansion map (if it exists). Returns a boolean indicating if
* the specified rule was successfully removed.
*
* @param key the attribute for which the corresponding rule should be removed
* @param rule the rule to be removed from the list of rules for the corresponding attribute
* @return true if the rule was found and removed, false otherwise
*/
public boolean removeExpansionRule(String key, String[] rule) {
boolean result = false;
if ((key == null) || (key.isEmpty()) || (rule == null) || (rule.length != 2)) {
return false;
}
if (expansionTable == null) {
return false;
}
List<String[]> list = expansionTable.get(key);
if (list != null) {
result = list.remove(rule);
if (list.size() == 0) {
expansionTable.remove(key);
}
}
return result;
}
/**
* Adds a list of rules corresponding to the give key in the expansion map. Convenience method
* for adding each rule individually.
*
* @param key the attribute for which the corresponding list of rules will be added
* @param list the list of rules to be added to the corresponding attribute
*/
public void addExpansionList(String key, List<String[]> list) {
if ((key == null) || (key.isEmpty()) || (list == null) || (list.size() == 0)) {
LOGGER.warn(
"Attempt to add list of expansion rules with null/empty key or null/empty list.");
return;
}
if (expansionTable == null) {
expansionTable = new HashMap<String, List<String[]>>();
}
// put each rule in individually in order to validate each one
for (String[] rule : list) {
addExpansionRule(key, rule);
}
}
/**
* Adds a list of rules provided in String form. This is a convenience method for adding each
* rule individually.
*
* @param rulesList list of rules (in String form) to be added to the expansion map
*/
public void setExpansionRules(List<String> rulesList) {
if (expansionTable == null) {
expansionTable = new HashMap<String, List<String[]>>();
}
if ((rulesList == null) || (rulesList.isEmpty())) {
expansionTable.clear();
} else {
String key;
String[] rule;
for (String r : rulesList) {
addExpansionRule(r);
}
}
}
/**
* Adds an individual rule (expressed in String form) The String form of the rule is a
* three-part string with each part separated by a colon (:)<br/>
* <attribute name>:<original value>:<replacement value>
*
* @param ruleStr the rule to be added (in String form)
*/
private void addExpansionRule(String ruleStr) {
String key;
String[] rule;
String[] parts = rulePattern.split(ruleStr, -2); // allow for empty trailing strings
if (parts.length >= 2) {
key = parts[0];
rule = new String[2];
rule[0] = parts[1];
rule[1] = parts.length > 2 ? parts[2] : "";
if (parts.length > 3) {
LOGGER.warn(
"Rule being added with more than key:search:replace parts - ignoring the rest - rule: {}",
ruleStr);
}
addExpansionRule(key, rule);
} else {
LOGGER.warn(
"Attempt to add rule without enough data - format is key:search[:replace] - rule: '{}'",
ruleStr);
}
}
/**
* Takes a string with potentially multiple values and splits it into a collection of strings.
*
* @param source input source string potentially containing multiple tokens
* @param separator the sequence separating the individual tokens
* @return a collection containing the individual tokens extracted from the specified source
* string
*/
protected Collection<String> split(String source, String separator) {
List<String> tmpList = new ArrayList<String>();
if ((source != null) && (!source.isEmpty()) && (separator != null)) {
try {
String[] splitValues = source.split(separator);
for (String value : splitValues) {
String tmpValue = value.trim();
if (!tmpValue.isEmpty()) {
tmpList.add(tmpValue);
}
}
} catch (PatternSyntaxException e) {
LOGGER.warn("Invalid separator - causing splitting errors - separator: {}",
separator);
}
}
return tmpList;
}
/**
* Sets the separator to be used in splitting up replacement strings. If a null or empty value
* is passed in, the default separator (a space) is used.
*
* @param separator the separator to be used to split up replacement strings
*/
public void setAttributeSeparator(String separator) {
if ((separator == null) || (separator.isEmpty())) {
attributeSeparator = DEFAULT_VALUE_SEPARATOR;
} else {
attributeSeparator = separator;
}
}
/**
* Sets the name of the configuration file defining the attribute separator and the mapping of
* attributes to their expanded values. This file is read initially, and whenever the file name
* is set. If the name is null or empty, the existing map of rules is cleared.
*
* @param filename the name of the configuration file to be loaded into the expansion service
*/
public void setExpansionFileName(String filename) {
if ((filename != null) && (!filename.isEmpty())) {
expansionFilename = filename;
LOGGER.info("Loading mapping rulesets from configuration file: {}", filename);
loadConfiguration(expansionFilename);
} else {
LOGGER.warn("Null or empty mapping configuration file name: {} - clearing existing map.",
filename);
expansionTable.clear();
}
}
public void update(Map<String, String> properties) {
LOGGER.debug("Updating Expansion Properties.");
if (properties != null) {
if (properties.containsKey(ATTRIBUTE_SEPARATOR)) {
setAttributeSeparator(properties.get(ATTRIBUTE_SEPARATOR));
}
if (properties.containsKey(EXPANSION_FILE_NAME)) {
setExpansionFileName(properties.get(EXPANSION_FILE_NAME));
}
}
}
/**
* Does the work of reading the configuration file and configuring the expansion map and
* attribute separator.
*
* @param filename the name of the file to be read and processed
*/
protected void loadConfiguration(String filename) {
if (filename == null) {
setExpansionMap(null);
return;
}
// first clear out the existing table
if (expansionTable != null) {
expansionTable.clear();
}
File file = null;
filename = StringUtils.strip(filename);
if (!Paths.get(filename)
.isAbsolute()) {
// relative path
String relPath = System.getProperty("ddf.home");
if (StringUtils.isBlank(relPath)) {
LOGGER.warn(
"ddf.home property was not set or is NULL, loading of properties may be impacted.");
}
file = new File(relPath, filename);
} else {
// absolute path
file = new File(filename);
}
try (BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(file),
StandardCharsets.UTF_8.name()))) {
String line;
while ((line = br.readLine()) != null) {
if ((line.length() > 0) && (!line.startsWith(CFG_COMMENT_STR))) {
if (line.startsWith(SEPARATOR_PREFIX)) {
if (line.length() > SEPARATOR_PREFIX.length()) {
attributeSeparator = line.substring(SEPARATOR_PREFIX.length());
} else {
attributeSeparator = DEFAULT_VALUE_SEPARATOR;
}
} else {
addExpansionRule(line);
}
}
}
LOGGER.debug("Finished loading mapping configuration file.");
} catch (IOException e) {
LOGGER.warn("Unexpected exception reading mapping configuration file {}", filename, e);
setExpansionMap(null);
}
}
}