diff options
Diffstat (limited to 'odl-aaa-moon/aaa/aaa-idp-mapping/src/main/java/org')
9 files changed, 2216 insertions, 0 deletions
diff --git a/odl-aaa-moon/aaa/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/Activator.java b/odl-aaa-moon/aaa/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/Activator.java new file mode 100644 index 00000000..7342485e --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/Activator.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2014 Red Hat, Inc. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ + +package org.opendaylight.aaa.idpmapping; + +import org.apache.felix.dm.DependencyActivatorBase; +import org.apache.felix.dm.DependencyManager; +import org.osgi.framework.BundleContext; + +public class Activator extends DependencyActivatorBase { + + @Override + public void init(BundleContext context, DependencyManager manager) throws Exception { + } + + @Override + public void destroy(BundleContext context, DependencyManager manager) throws Exception { + } + +} diff --git a/odl-aaa-moon/aaa/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/IdpJson.java b/odl-aaa-moon/aaa/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/IdpJson.java new file mode 100644 index 00000000..00328b60 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/IdpJson.java @@ -0,0 +1,248 @@ +/* + * Copyright (c) 2014 Red Hat, Inc. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ + +package org.opendaylight.aaa.idpmapping; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import javax.json.Json; +import javax.json.JsonValue; +import javax.json.stream.JsonGenerator; +import javax.json.stream.JsonGeneratorFactory; +import javax.json.stream.JsonLocation; +import javax.json.stream.JsonParser; +import javax.json.stream.JsonParser.Event; + +/** + * Converts between JSON and the internal data structures used in the + * RuleProcessor. + * + * @author John Dennis <jdennis@redhat.com> + */ + +public class IdpJson { + + public IdpJson() { + } + + public Object loadJson(java.io.Reader in) { + JsonParser parser = Json.createParser(in); + Event event = null; + + // Prime the pump. Get the first item from the parser. + event = parser.next(); + + // Act on first item. + return loadJsonItem(parser, event); + } + + public Object loadJson(Path filename) throws IOException { + BufferedReader reader = Files.newBufferedReader(filename, StandardCharsets.UTF_8); + return loadJson(reader); + } + + public Object loadJson(String string) { + StringReader reader = new StringReader(string); + return loadJson(reader); + } + + /* + * Process current parser item indicated by event. Consumes exactly the + * number of parser events necessary to load the item. Caller must advance + * the parser via parser.next() after this method returns. + */ + private Object loadJsonItem(JsonParser parser, Event event) { + switch (event) { + case START_OBJECT: { + return loadJsonObject(parser, event); + } + case START_ARRAY: { + return loadJsonArray(parser, event); + } + case VALUE_NULL: { + return null; + } + case VALUE_NUMBER: { + if (parser.isIntegralNumber()) { + return parser.getLong(); + } else { + return parser.getBigDecimal().doubleValue(); + } + } + case VALUE_STRING: { + return parser.getString(); + } + case VALUE_TRUE: { + return Boolean.TRUE; + } + case VALUE_FALSE: { + return Boolean.FALSE; + } + default: { + JsonLocation location = parser.getLocation(); + throw new IllegalStateException(String.format( + "unknown JSON parsing event %s, location(line=%d column=%d offset=%d)", event, + location.getLineNumber(), location.getColumnNumber(), + location.getStreamOffset())); + } + } + } + + private List<Object> loadJsonArray(JsonParser parser, Event event) { + List<Object> list = new ArrayList<Object>(); + + if (event != Event.START_ARRAY) { + JsonLocation location = parser.getLocation(); + throw new IllegalStateException( + String.format( + "expected JSON parsing event to be START_ARRAY, not %s location(line=%d column=%d offset=%d)", + event, location.getLineNumber(), location.getColumnNumber(), + location.getStreamOffset())); + } + event = parser.next(); // consume START_ARRAY + while (event != Event.END_ARRAY) { + Object obj; + + obj = loadJsonItem(parser, event); + list.add(obj); + event = parser.next(); // next array item or END_ARRAY + } + return list; + } + + private Map<String, Object> loadJsonObject(JsonParser parser, Event event) { + Map<String, Object> map = new LinkedHashMap<String, Object>(); + + if (event != Event.START_OBJECT) { + JsonLocation location = parser.getLocation(); + throw new IllegalStateException(String.format( + "expected JSON parsing event to be START_OBJECT, not %s, ", + "location(line=%d column=%d offset=%d)", event, location.getLineNumber(), + location.getColumnNumber(), location.getStreamOffset())); + } + event = parser.next(); // consume START_OBJECT + while (event != Event.END_OBJECT) { + if (event == Event.KEY_NAME) { + String key; + Object value; + + key = parser.getString(); + event = parser.next(); // consume key + value = loadJsonItem(parser, event); + map.put(key, value); + } else { + JsonLocation location = parser.getLocation(); + throw new IllegalStateException( + String.format( + "expected JSON parsing event to be KEY_NAME, not %s, location(line=%d column=%d offset=%d)", + event, location.getLineNumber(), location.getColumnNumber(), + location.getStreamOffset())); + + } + event = parser.next(); // next key or END_OBJECT + } + return map; + } + + public String dumpJson(Object obj) { + Map<String, Object> properties = new HashMap<String, Object>(1); + properties.put(JsonGenerator.PRETTY_PRINTING, true); + JsonGeneratorFactory generatorFactory = Json.createGeneratorFactory(properties); + StringWriter stringWriter = new StringWriter(); + JsonGenerator generator = generatorFactory.createGenerator(stringWriter); + + dumpJsonItem(generator, obj); + generator.close(); + return stringWriter.toString(); + } + + private void dumpJsonItem(JsonGenerator generator, Object obj) { + // ordered by expected occurrence + if (obj instanceof String) { + generator.write((String) obj); + } else if (obj instanceof List) { + generator.writeStartArray(); + @SuppressWarnings("unchecked") + List<Object> list = (List<Object>) obj; + dumpJsonArray(generator, list); + } else if (obj instanceof Map) { + generator.writeStartObject(); + @SuppressWarnings("unchecked") + Map<String, Object> map = (Map<String, Object>) obj; + dumpJsonObject(generator, map); + } else if (obj instanceof Long) { + generator.write(((Long) obj).longValue()); + } else if (obj instanceof Boolean) { + generator.write(((Boolean) obj).booleanValue()); + } else if (obj == null) { + generator.writeNull(); + } else if (obj instanceof Double) { + generator.write(((Double) obj).doubleValue()); + } else { + throw new IllegalStateException( + String.format( + "unsupported data type, must be String, Long, Double, Boolean, List, Map, or null, not %s", + obj.getClass().getSimpleName())); + } + } + + private void dumpJsonArray(JsonGenerator generator, List<Object> list) { + for (Object obj : list) { + dumpJsonItem(generator, obj); + } + generator.writeEnd(); + } + + private void dumpJsonObject(JsonGenerator generator, Map<String, Object> map) { + + for (Map.Entry<String, Object> entry : map.entrySet()) { + String key = entry.getKey(); + Object obj = entry.getValue(); + + // ordered by expected occurrence + if (obj instanceof String) { + generator.write(key, (String) obj); + } else if (obj instanceof List) { + generator.writeStartArray(key); + @SuppressWarnings("unchecked") + List<Object> list = (List<Object>) obj; + dumpJsonArray(generator, list); + } else if (obj instanceof Map) { + generator.writeStartObject(key); + @SuppressWarnings("unchecked") + Map<String, Object> map1 = (Map<String, Object>) obj; + dumpJsonObject(generator, map1); + } else if (obj instanceof Long) { + generator.write(key, ((Long) obj).longValue()); + } else if (obj instanceof Boolean) { + generator.write(key, ((Boolean) obj).booleanValue()); + } else if (obj == null) { + generator.write(key, JsonValue.NULL); + } else if (obj instanceof Double) { + generator.write(key, ((Double) obj).doubleValue()); + } else { + throw new IllegalStateException( + String.format( + "unsupported data type, must be String, Long, Double, Boolean, List, Map, or null, not %s", + obj.getClass().getSimpleName())); + } + } + generator.writeEnd(); + } + +} diff --git a/odl-aaa-moon/aaa/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/InvalidRuleException.java b/odl-aaa-moon/aaa/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/InvalidRuleException.java new file mode 100644 index 00000000..1e42f4f2 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/InvalidRuleException.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2014 Red Hat, Inc. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ + +package org.opendaylight.aaa.idpmapping; + +/** + * Exception thrown when a mapping rule is improperly defined. + * + * @author John Dennis <jdennis@redhat.com> + */ + +public class InvalidRuleException extends RuntimeException { + + private static final long serialVersionUID = 1948891573270429630L; + + public InvalidRuleException() { + } + + public InvalidRuleException(String message) { + super(message); + } + + public InvalidRuleException(Throwable cause) { + super(cause); + } + + public InvalidRuleException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/odl-aaa-moon/aaa/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/InvalidTypeException.java b/odl-aaa-moon/aaa/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/InvalidTypeException.java new file mode 100644 index 00000000..fb8b132f --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/InvalidTypeException.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2014 Red Hat, Inc. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ + +package org.opendaylight.aaa.idpmapping; + +/** + * Exception thrown when the type of a value is incorrect for a given context. + * + * @author John Dennis <jdennis@redhat.com> + */ + +public class InvalidTypeException extends RuntimeException { + + private static final long serialVersionUID = 4437011247503994368L; + + public InvalidTypeException() { + } + + public InvalidTypeException(String message) { + super(message); + } + + public InvalidTypeException(Throwable cause) { + super(cause); + } + + public InvalidTypeException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/odl-aaa-moon/aaa/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/InvalidValueException.java b/odl-aaa-moon/aaa/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/InvalidValueException.java new file mode 100644 index 00000000..2f83c13f --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/InvalidValueException.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2014 Red Hat, Inc. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ + +package org.opendaylight.aaa.idpmapping; + +/** + * Exception thrown when a value cannot be used in a given context. + * + * @author John Dennis <jdennis@redhat.com> + */ + +public class InvalidValueException extends RuntimeException { + + private static final long serialVersionUID = -2351651535772692180L; + + public InvalidValueException() { + } + + public InvalidValueException(String message) { + super(message); + } + + public InvalidValueException(Throwable cause) { + super(cause); + } + + public InvalidValueException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/odl-aaa-moon/aaa/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/RuleProcessor.java b/odl-aaa-moon/aaa/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/RuleProcessor.java new file mode 100644 index 00000000..0f86fde6 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/RuleProcessor.java @@ -0,0 +1,1368 @@ +/* + * Copyright (c) 2014 Red Hat, Inc. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ + +package org.opendaylight.aaa.idpmapping; + +import java.io.IOException; +import java.io.StringWriter; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +enum ProcessResult { + RULE_FAIL, RULE_SUCCESS, BLOCK_CONTINUE, STATEMENT_CONTINUE +} + +/** + * Evaluate a set of rules against an assertion from an external Identity + * Provider (IdP) mapping those assertion values to local values. + * + * @author John Dennis <jdennis@redhat.com> + */ + +public class RuleProcessor { + private static final Logger LOG = LoggerFactory.getLogger(RuleProcessor.class); + + public String ruleIdFormat = "<rule [${rule_number}:\"${rule_name}\"]>"; + public String statementIdFormat = "<rule [${rule_number}:\"${rule_name}\"] block [${block_number}:\"${block_name}\"] statement ${statement_number}>"; + + /* + * Reserved variables + */ + public static final String ASSERTION = "assertion"; + public static final String RULE_NUMBER = "rule_number"; + public static final String RULE_NAME = "rule_name"; + public static final String BLOCK_NUMBER = "block_number"; + public static final String BLOCK_NAME = "block_name"; + public static final String STATEMENT_NUMBER = "statement_number"; + public static final String REGEXP_ARRAY_VARIABLE = "regexp_array"; + public static final String REGEXP_MAP_VARIABLE = "regexp_map"; + + private static final String REGEXP_NAMED_GROUP_PAT = "\\(\\?<([a-zA-Z][a-zA-Z0-9]*)>"; + private static final Pattern REGEXP_NAMED_GROUP_RE = Pattern.compile(REGEXP_NAMED_GROUP_PAT); + + List<Map<String, Object>> rules = null; + boolean success = true; + Map<String, Map<String, Object>> mappings = null; + + public RuleProcessor(java.io.Reader rulesIn, Map<String, Map<String, Object>> mappings) { + this.mappings = mappings; + IdpJson json = new IdpJson(); + @SuppressWarnings("unchecked") + List<Map<String, Object>> loadJson = (List<Map<String, Object>>) json.loadJson(rulesIn); + rules = loadJson; + } + + public RuleProcessor(Path rulesIn, Map<String, Map<String, Object>> mappings) + throws IOException { + this.mappings = mappings; + IdpJson json = new IdpJson(); + @SuppressWarnings("unchecked") + List<Map<String, Object>> loadJson = (List<Map<String, Object>>) json.loadJson(rulesIn); + rules = loadJson; + } + + public RuleProcessor(String rulesIn, Map<String, Map<String, Object>> mappings) { + this.mappings = mappings; + IdpJson json = new IdpJson(); + @SuppressWarnings("unchecked") + List<Map<String, Object>> loadJson = (List<Map<String, Object>>) json.loadJson(rulesIn); + rules = loadJson; + } + + /* + * For some odd reason the Java Regular Expression API does not include a + * way to retrieve a map of the named groups and their values. The API only + * permits us to retrieve a named group if we already know the group names. + * So instead we parse the pattern string looking for named groups, extract + * the name, look up the value of the named group and build a map from that. + */ + + private Map<String, String> regexpGroupMap(String pattern, Matcher matcher) { + Map<String, String> groupMap = new HashMap<String, String>(); + Matcher groupMatcher = REGEXP_NAMED_GROUP_RE.matcher(pattern); + + while (groupMatcher.find()) { + String groupName = groupMatcher.group(1); + + groupMap.put(groupName, matcher.group(groupName)); + } + return groupMap; + } + + static public String join(List<Object> list, String conjunction) { + StringBuilder sb = new StringBuilder(); + boolean first = true; + for (Object item : list) { + if (first) { + first = false; + } else { + sb.append(conjunction); + } + sb.append(item.toString()); + } + return sb.toString(); + } + + private List<String> regexpGroupList(Matcher matcher) { + List<String> groupList = new ArrayList<String>(matcher.groupCount() + 1); + groupList.add(0, matcher.group(0)); + for (int i = 1; i < matcher.groupCount() + 1; i++) { + groupList.add(i, matcher.group(i)); + } + return groupList; + } + + private String objToString(Object obj) { + StringWriter sw = new StringWriter(); + objToStringItem(sw, obj); + return sw.toString(); + } + + private void objToStringItem(StringWriter sw, Object obj) { + // ordered by expected occurrence + if (obj instanceof String) { + sw.write('"'); + sw.write(((String) obj).replaceAll("\"", "\\\"")); + sw.write('"'); + } else if (obj instanceof List) { + @SuppressWarnings("unchecked") + List<Object> list = (List<Object>) obj; + boolean first = true; + + sw.write('['); + for (Object item : list) { + if (first) { + first = false; + } else { + sw.write(", "); + } + objToStringItem(sw, item); + } + sw.write(']'); + } else if (obj instanceof Map) { + @SuppressWarnings("unchecked") + Map<String, Object> map = (Map<String, Object>) obj; + boolean first = true; + + sw.write('{'); + for (Map.Entry<String, Object> entry : map.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + + if (first) { + first = false; + } else { + sw.write(", "); + } + + objToStringItem(sw, key); + sw.write(": "); + objToStringItem(sw, value); + + } + sw.write('}'); + } else if (obj instanceof Long) { + sw.write(((Long) obj).toString()); + } else if (obj instanceof Boolean) { + sw.write(((Boolean) obj).toString()); + } else if (obj == null) { + sw.write("null"); + } else if (obj instanceof Double) { + sw.write(((Double) obj).toString()); + } else { + throw new IllegalStateException( + String.format( + "unsupported data type, must be String, Long, Double, Boolean, List, Map, or null, not %s", + obj.getClass().getSimpleName())); + } + } + + private Object deepCopy(Object obj) { + // ordered by expected occurrence + if (obj instanceof String) { + return obj; // immutable + } else if (obj instanceof List) { + List<Object> new_list = new ArrayList<Object>(); + @SuppressWarnings("unchecked") + List<Object> list = (List<Object>) obj; + for (Object item : list) { + new_list.add(deepCopy(item)); + } + return new_list; + } else if (obj instanceof Map) { + Map<String, Object> new_map = new LinkedHashMap<String, Object>(); + @SuppressWarnings("unchecked") + Map<String, Object> map = (Map<String, Object>) obj; + for (Map.Entry<String, Object> entry : map.entrySet()) { + String key = entry.getKey(); // immutable + Object value = entry.getValue(); + new_map.put(key, deepCopy(value)); + } + return new_map; + } else if (obj instanceof Long) { + return obj; // immutable + } else if (obj instanceof Boolean) { + return obj; // immutable + } else if (obj == null) { + return null; + } else if (obj instanceof Double) { + return obj; // immutable + } else { + throw new IllegalStateException( + String.format( + "unsupported data type, must be String, Long, Double, Boolean, List, Map, or null, not %s", + obj.getClass().getSimpleName())); + } + } + + public String ruleId(Map<String, Object> namespace) { + return substituteVariables(ruleIdFormat, namespace); + } + + public String statementId(Map<String, Object> namespace) { + return substituteVariables(statementIdFormat, namespace); + } + + public String substituteVariables(String string, Map<String, Object> namespace) { + StringBuffer sb = new StringBuffer(); + Matcher matcher = Token.VARIABLE_RE.matcher(string); + + while (matcher.find()) { + Token token = new Token(matcher.group(0), namespace); + token.load(); + String replacement; + if (token.type == TokenType.STRING) { + replacement = token.getStringValue(); + } else { + replacement = objToString(token.getObjectValue()); + } + + matcher.appendReplacement(sb, replacement); + } + matcher.appendTail(sb); + return sb.toString(); + } + + Map<String, Object> getMapping(Map<String, Object> namespace, Map<String, Object> rule) { + Map<String, Object> mapping = null; + String mappingName = null; + + try { + @SuppressWarnings("unchecked") + Map<String, Object> map = (Map<String, Object>) rule.get("mapping"); + mapping = map; + } catch (java.lang.ClassCastException e) { + throw new InvalidRuleException(String.format( + "%s rule defines 'mapping' but it is not a Map", this.ruleId(namespace), e)); + } + if (mapping != null) { + return mapping; + } + try { + mappingName = (String) rule.get("mapping_name"); + } catch (java.lang.ClassCastException e) { + throw new InvalidRuleException(String.format( + "%s rule defines 'mapping_name' but it is not a string", + this.ruleId(namespace), e)); + } + if (mappingName == null) { + throw new InvalidRuleException(String.format( + "%s rule does not define mapping nor mapping_name unable to load mapping", + this.ruleId(namespace))); + } + mapping = this.mappings.get(mappingName); + if (mapping == null) { + throw new InvalidRuleException( + String.format( + "%s rule specifies mapping_name '%s' but a mapping by that name does not exist, unable to load mapping", + this.ruleId(namespace))); + } + LOG.debug(String.format("using named mapping '%s' from rule %s mapping=%s", mappingName, + this.ruleId(namespace), mapping)); + return mapping; + } + + private String getVerb(List<Object> statement) { + Token verb; + + if (statement.size() < 1) { + throw new InvalidRuleException("statement has no verb"); + } + + try { + verb = new Token(statement.get(0), null); + } catch (Exception e) { + throw new InvalidRuleException(String.format( + "statement first member (i.e. verb) error %s", e)); + } + + if (verb.type != TokenType.STRING) { + throw new InvalidRuleException(String.format( + "statement first member (i.e. verb) must be a string, not %s", verb.type)); + } + + return (verb.getStringValue()).toLowerCase(); + } + + private Token getToken(String verb, List<Object> statement, int index, + Map<String, Object> namespace, Set<TokenStorageType> storageTypes, + Set<TokenType> tokenTypes) { + Object item; + Token token; + + try { + item = statement.get(index); + } catch (IndexOutOfBoundsException e) { + throw new InvalidRuleException(String.format( + "verb '%s' requires at least %d items but only %d are available.", verb, + index + 1, statement.size(), e)); + } + + try { + token = new Token(item, namespace); + } catch (Exception e) { + throw new StatementErrorException(String.format("parameter %d, %s", index, e)); + } + + if (storageTypes != null) { + if (!storageTypes.contains(token.storageType)) { + throw new InvalidTypeException( + String.format( + "verb '%s' requires parameter #%d to have storage types %s not %s. statement=%s", + verb, index, storageTypes, statement)); + } + } + + if (tokenTypes != null) { + token.load(); // Note, Token.load() sets the Token.type + + if (!tokenTypes.contains(token.type)) { + throw new InvalidTypeException(String.format( + "verb '%s' requires parameter #%d to have types %s, not %s. statement=%s", + verb, index, tokenTypes, statement)); + } + } + + return token; + } + + private Token getParameter(String verb, List<Object> statement, int index, + Map<String, Object> namespace, Set<TokenType> tokenTypes) { + Object item; + Token token; + + try { + item = statement.get(index); + } catch (IndexOutOfBoundsException e) { + throw new InvalidRuleException(String.format( + "verb '%s' requires at least %d items but only %d are available.", verb, + index + 1, statement.size(), e)); + } + + try { + token = new Token(item, namespace); + } catch (Exception e) { + throw new StatementErrorException(String.format("parameter %d, %s", index, e)); + } + + token.load(); + + if (tokenTypes != null) { + try { + token.get(); // Note, Token.get() sets the Token.type + } catch (UndefinedValueException e) { + // OK if not yet defined + } + if (!tokenTypes.contains(token.type)) { + throw new InvalidTypeException(String.format( + "verb '%s' requires parameter #%d to have types %s, not %s. statement=%s", + verb, index, tokenTypes, item.getClass().getSimpleName(), statement)); + } + } + + return token; + } + + private Object getRawParameter(String verb, List<Object> statement, int index, + Set<TokenType> tokenTypes) { + Object item; + + try { + item = statement.get(index); + } catch (IndexOutOfBoundsException e) { + throw new InvalidRuleException(String.format( + "verb '%s' requires at least %d items but only %d are available.", verb, + index + 1, statement.size(), e)); + } + + if (tokenTypes != null) { + TokenType itemType = Token.classify(item); + + if (!tokenTypes.contains(itemType)) { + throw new InvalidTypeException(String.format( + "verb '%s' requires parameter #%d to have types %s, not %s. statement=%s", + verb, index, tokenTypes, statement)); + } + } + + return item; + } + + private Token getVariable(String verb, List<Object> statement, int index, + Map<String, Object> namespace) { + Object item; + Token token; + + try { + item = statement.get(index); + } catch (IndexOutOfBoundsException e) { + throw new InvalidRuleException(String.format( + "verb '%s' requires at least %d items but only %d are available.", verb, + index + 1, statement.size(), e)); + } + + try { + token = new Token(item, namespace); + } catch (Exception e) { + throw new StatementErrorException(String.format("parameter %d, %s", index, e)); + } + + if (token.storageType != TokenStorageType.VARIABLE) { + throw new InvalidTypeException(String.format( + "verb '%s' requires parameter #%d to be a variable not %s. statement=%s", verb, + index, token.storageType, statement)); + } + + return token; + } + + public Map<String, Object> process(String assertionJson) { + ProcessResult result; + IdpJson json = new IdpJson(); + @SuppressWarnings("unchecked") + Map<String, Object> assertion = (Map<String, Object>) json.loadJson(assertionJson); + LOG.info("Assertion JSON: {}", json.dumpJson(assertion)); + this.success = true; + + for (int ruleNumber = 0; ruleNumber < this.rules.size(); ruleNumber++) { + Map<String, Object> namespace = new HashMap<String, Object>(); + Map<String, Object> rule = (Map<String, Object>) this.rules.get(ruleNumber); + namespace.put(RULE_NUMBER, Long.valueOf(ruleNumber)); + namespace.put(RULE_NAME, ""); + namespace.put(ASSERTION, deepCopy(assertion)); + + result = processRule(namespace, rule); + + if (result == ProcessResult.RULE_SUCCESS) { + Map<String, Object> mapped = new LinkedHashMap<String, Object>(); + Map<String, Object> mapping = getMapping(namespace, rule); + for (Map.Entry<String, Object> entry : ((Map<String, Object>) mapping).entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + Object newValue = null; + try { + Token token = new Token(value, namespace); + newValue = token.get(); + } catch (Exception e) { + throw new InvalidRuleException(String.format( + "%s unable to get value for mapping %s=%s, %s", ruleId(namespace), + key, value, e), e); + } + mapped.put(key, newValue); + } + return mapped; + } + } + return null; + } + + private ProcessResult processRule(Map<String, Object> namespace, Map<String, Object> rule) { + ProcessResult result = ProcessResult.BLOCK_CONTINUE; + @SuppressWarnings("unchecked") + List<List<List<Object>>> statementBlocks = (List<List<List<Object>>>) rule.get("statement_blocks"); + if (statementBlocks == null) { + throw new InvalidRuleException("rule missing 'statement_blocks'"); + + } + for (int blockNumber = 0; blockNumber < statementBlocks.size(); blockNumber++) { + List<List<Object>> block = (List<List<Object>>) statementBlocks.get(blockNumber); + namespace.put(BLOCK_NUMBER, Long.valueOf(blockNumber)); + namespace.put(BLOCK_NAME, ""); + + result = processBlock(namespace, block); + if (EnumSet.of(ProcessResult.RULE_SUCCESS, ProcessResult.RULE_FAIL).contains(result)) { + break; + } else if (result == ProcessResult.BLOCK_CONTINUE) { + continue; + } else { + throw new IllegalStateException(String.format("%s unexpected statement result: %s", + result)); + } + } + if (EnumSet.of(ProcessResult.RULE_SUCCESS, ProcessResult.BLOCK_CONTINUE).contains(result)) { + return ProcessResult.RULE_SUCCESS; + } else { + return ProcessResult.RULE_FAIL; + } + } + + private ProcessResult processBlock(Map<String, Object> namespace, List<List<Object>> block) { + ProcessResult result = ProcessResult.STATEMENT_CONTINUE; + + for (int statementNumber = 0; statementNumber < block.size(); statementNumber++) { + List<Object> statement = (List<Object>) block.get(statementNumber); + namespace.put(STATEMENT_NUMBER, Long.valueOf(statementNumber)); + + try { + result = processStatement(namespace, statement); + } catch (Exception e) { + throw new IllegalStateException(String.format("%s statement=%s %s", + statementId(namespace), statement, e), e); + } + if (EnumSet.of(ProcessResult.BLOCK_CONTINUE, ProcessResult.RULE_SUCCESS, + ProcessResult.RULE_FAIL).contains(result)) { + break; + } else if (result == ProcessResult.STATEMENT_CONTINUE) { + continue; + } else { + throw new IllegalStateException(String.format("%s unexpected statement result: %s", + result)); + } + } + if (result == ProcessResult.STATEMENT_CONTINUE) { + result = ProcessResult.BLOCK_CONTINUE; + } + return result; + } + + private ProcessResult processStatement(Map<String, Object> namespace, List<Object> statement) { + ProcessResult result = ProcessResult.STATEMENT_CONTINUE; + String verb = getVerb(statement); + + switch (verb) { + case "set": + result = verbSet(verb, namespace, statement); + break; + case "length": + result = verbLength(verb, namespace, statement); + break; + case "interpolate": + result = verbInterpolate(verb, namespace, statement); + break; + case "append": + result = verbAppend(verb, namespace, statement); + break; + case "unique": + result = verbUnique(verb, namespace, statement); + break; + case "split": + result = verbSplit(verb, namespace, statement); + break; + case "join": + result = verbJoin(verb, namespace, statement); + break; + case "lower": + result = verbLower(verb, namespace, statement); + break; + case "upper": + result = verbUpper(verb, namespace, statement); + break; + case "in": + result = verbIn(verb, namespace, statement); + break; + case "not_in": + result = verbNotIn(verb, namespace, statement); + break; + case "compare": + result = verbCompare(verb, namespace, statement); + break; + case "regexp": + result = verbRegexp(verb, namespace, statement); + break; + case "regexp_replace": + result = verbRegexpReplace(verb, namespace, statement); + break; + case "exit": + result = verbExit(verb, namespace, statement); + break; + case "continue": + result = verbContinue(verb, namespace, statement); + break; + default: + throw new InvalidRuleException(String.format("unknown verb '%s'", verb)); + } + + return result; + } + + private ProcessResult verbSet(String verb, Map<String, Object> namespace, List<Object> statement) { + Token variable = getVariable(verb, statement, 1, namespace); + Token parameter = getParameter(verb, statement, 2, namespace, null); + + variable.set(parameter.getObjectValue()); + this.success = true; + + if (LOG.isDebugEnabled()) { + LOG.debug(String.format("%s verb='%s' success=%s variable: %s=%s", + statementId(namespace), verb, this.success, variable, variable.get())); + } + return ProcessResult.STATEMENT_CONTINUE; + } + + private ProcessResult verbLength(String verb, Map<String, Object> namespace, + List<Object> statement) { + Token variable = getVariable(verb, statement, 1, namespace); + Token parameter = getParameter(verb, statement, 2, namespace, + EnumSet.of(TokenType.ARRAY, TokenType.MAP, TokenType.STRING)); + long length; + + switch (parameter.type) { + case ARRAY: { + length = parameter.getListValue().size(); + } + break; + case MAP: { + length = parameter.getMapValue().size(); + } + break; + case STRING: { + length = parameter.getStringValue().length(); + } + break; + default: + throw new IllegalStateException(String.format("unexpected token type: %s", + parameter.type)); + } + + variable.set(length); + this.success = true; + + if (LOG.isDebugEnabled()) { + LOG.debug(String.format("%s verb='%s' success=%s variable: %s=%s parameter=%s", + statementId(namespace), verb, this.success, variable, variable.get(), + parameter.getObjectValue())); + } + return ProcessResult.STATEMENT_CONTINUE; + } + + private ProcessResult verbInterpolate(String verb, Map<String, Object> namespace, + List<Object> statement) { + Token variable = getVariable(verb, statement, 1, namespace); + String string = (String) getRawParameter(verb, statement, 2, EnumSet.of(TokenType.STRING)); + String newValue = null; + + try { + newValue = substituteVariables(string, namespace); + } catch (Exception e) { + throw new InvalidValueException(String.format( + "verb '%s' failed, variable='%s' string='%s': %s", verb, variable, string, e)); + } + variable.set(newValue); + this.success = true; + + if (LOG.isDebugEnabled()) { + LOG.debug(String.format("%s verb='%s' success=%s variable: %s=%s string='%s'", + statementId(namespace), verb, this.success, variable, variable.get(), string)); + } + + return ProcessResult.STATEMENT_CONTINUE; + } + + private ProcessResult verbAppend(String verb, Map<String, Object> namespace, + List<Object> statement) { + Token variable = getToken(verb, statement, 1, namespace, + EnumSet.of(TokenStorageType.VARIABLE), EnumSet.of(TokenType.ARRAY)); + Token item = getParameter(verb, statement, 2, namespace, null); + + try { + List<Object> list = variable.getListValue(); + list.add(item.getObjectValue()); + } catch (Exception e) { + throw new InvalidValueException(String.format( + "verb '%s' failed, variable='%s' item='%s': %s", verb, + variable.getObjectValue(), item.getObjectValue(), e)); + } + this.success = true; + + if (LOG.isDebugEnabled()) { + LOG.debug(String.format("%s verb='%s' success=%s variable: %s=%s item=%s", + statementId(namespace), verb, this.success, variable, variable.get(), + item.getObjectValue())); + } + + return ProcessResult.STATEMENT_CONTINUE; + } + + private ProcessResult verbUnique(String verb, Map<String, Object> namespace, + List<Object> statement) { + Token variable = getVariable(verb, statement, 1, namespace); + Token array = getParameter(verb, statement, 2, namespace, EnumSet.of(TokenType.ARRAY)); + + List<Object> newValue = new ArrayList<Object>(); + Set<Object> seen = new HashSet<Object>(); + + for (Object member : array.getListValue()) { + if (seen.contains(member)) { + continue; + } else { + newValue.add(member); + seen.add(member); + } + } + + variable.set(newValue); + this.success = true; + + if (LOG.isDebugEnabled()) { + LOG.debug(String.format("%s verb='%s' success=%s variable: %s=%s array=%s", + statementId(namespace), verb, this.success, variable, variable.get(), + array.getObjectValue())); + } + + return ProcessResult.STATEMENT_CONTINUE; + } + + private ProcessResult verbSplit(String verb, Map<String, Object> namespace, + List<Object> statement) { + Token variable = getVariable(verb, statement, 1, namespace); + Token string = getParameter(verb, statement, 2, namespace, EnumSet.of(TokenType.STRING)); + Token pattern = getParameter(verb, statement, 3, namespace, EnumSet.of(TokenType.STRING)); + + Pattern regexp; + List<String> newValue; + + try { + regexp = Pattern.compile(pattern.getStringValue()); + } catch (Exception e) { + throw new InvalidValueException(String.format( + "verb '%s' failed, bad regular expression pattern '%s', %s", verb, + pattern.getObjectValue(), e)); + } + try { + newValue = new ArrayList<String>( + Arrays.asList(regexp.split((String) string.getStringValue()))); + } catch (Exception e) { + throw new InvalidValueException(String.format( + "verb '%s' failed, string='%s' pattern='%s', %s", verb, + string.getObjectValue(), pattern.getObjectValue(), e)); + } + + variable.set(newValue); + this.success = true; + + if (LOG.isDebugEnabled()) { + LOG.debug(String.format( + "%s verb='%s' success=%s variable: %s=%s string='%s' pattern='%s'", + statementId(namespace), verb, this.success, variable, variable.get(), + string.getObjectValue(), pattern.getObjectValue())); + } + + return ProcessResult.STATEMENT_CONTINUE; + } + + private ProcessResult verbJoin(String verb, Map<String, Object> namespace, + List<Object> statement) { + Token variable = getVariable(verb, statement, 1, namespace); + Token array = getParameter(verb, statement, 2, namespace, EnumSet.of(TokenType.ARRAY)); + Token conjunction = getParameter(verb, statement, 3, namespace, + EnumSet.of(TokenType.STRING)); + String newValue; + + try { + newValue = join(array.getListValue(), conjunction.getStringValue()); + } catch (Exception e) { + throw new InvalidValueException(String.format( + "verb '%s' failed, array=%s conjunction='%s', %s", verb, + array.getObjectValue(), conjunction.getObjectValue(), e)); + } + + variable.set(newValue); + this.success = true; + + if (LOG.isDebugEnabled()) { + LOG.debug(String.format( + "%s verb='%s' success=%s variable: %s=%s array='%s' conjunction='%s'", + statementId(namespace), verb, this.success, variable, variable.get(), + array.getObjectValue(), conjunction.getObjectValue())); + } + + return ProcessResult.STATEMENT_CONTINUE; + } + + private ProcessResult verbLower(String verb, Map<String, Object> namespace, + List<Object> statement) { + Token variable = getVariable(verb, statement, 1, namespace); + Token parameter = getParameter(verb, statement, 2, namespace, + EnumSet.of(TokenType.STRING, TokenType.ARRAY, TokenType.MAP)); + + try { + switch (parameter.type) { + case STRING: { + String oldValue = parameter.getStringValue(); + String newValue; + newValue = oldValue.toLowerCase(); + variable.set(newValue); + } + break; + case ARRAY: { + List<Object> oldValue = parameter.getListValue(); + List<Object> newValue = new ArrayList<Object>(oldValue.size()); + String oldItem; + String newItem; + + for (Object item : oldValue) { + try { + oldItem = (String) item; + } catch (ClassCastException e) { + throw new InvalidValueException(String.format( + "verb '%s' failed, array item (%s) is not a string, array=%s", + verb, item, parameter.getObjectValue(), e)); + } + newItem = oldItem.toLowerCase(); + newValue.add(newItem); + } + variable.set(newValue); + } + break; + case MAP: { + Map<String, Object> oldValue = parameter.getMapValue(); + Map<String, Object> newValue = new LinkedHashMap<String, Object>(oldValue.size()); + + for (Map.Entry<String, Object> entry : oldValue.entrySet()) { + String oldKey; + String newKey; + Object value = entry.getValue(); + + oldKey = entry.getKey(); + newKey = oldKey.toLowerCase(); + newValue.put(newKey, value); + } + variable.set(newValue); + } + break; + default: + throw new IllegalStateException(String.format("unexpected token type: %s", + parameter.type)); + } + } catch (Exception e) { + throw new InvalidValueException(String.format( + "verb '%s' failed, variable='%s' parameter='%s': %s", verb, variable, + parameter.getObjectValue(), e), e); + } + this.success = true; + + if (LOG.isDebugEnabled()) { + LOG.debug(String.format("%s verb='%s' success=%s variable: %s=%s parameter=%s", + statementId(namespace), verb, this.success, variable, variable.get(), + parameter.getObjectValue())); + } + return ProcessResult.STATEMENT_CONTINUE; + } + + private ProcessResult verbUpper(String verb, Map<String, Object> namespace, + List<Object> statement) { + Token variable = getVariable(verb, statement, 1, namespace); + Token parameter = getParameter(verb, statement, 2, namespace, + EnumSet.of(TokenType.STRING, TokenType.ARRAY, TokenType.MAP)); + + try { + switch (parameter.type) { + case STRING: { + String oldValue = parameter.getStringValue(); + String newValue; + newValue = oldValue.toUpperCase(); + variable.set(newValue); + } + break; + case ARRAY: { + List<Object> oldValue = parameter.getListValue(); + List<Object> newValue = new ArrayList<Object>(oldValue.size()); + String oldItem; + String newItem; + + for (Object item : oldValue) { + try { + oldItem = (String) item; + } catch (ClassCastException e) { + throw new InvalidValueException(String.format( + "verb '%s' failed, array item (%s) is not a string, array=%s", + verb, item, parameter.getObjectValue(), e)); + } + newItem = oldItem.toUpperCase(); + newValue.add(newItem); + } + variable.set(newValue); + } + break; + case MAP: { + Map<String, Object> oldValue = parameter.getMapValue(); + Map<String, Object> newValue = new LinkedHashMap<String, Object>(oldValue.size()); + + for (Map.Entry<String, Object> entry : oldValue.entrySet()) { + String oldKey; + String newKey; + Object value = entry.getValue(); + + oldKey = entry.getKey(); + newKey = oldKey.toUpperCase(); + newValue.put(newKey, value); + } + variable.set(newValue); + } + break; + default: + throw new IllegalStateException(String.format("unexpected token type: %s", + parameter.type)); + } + } catch (Exception e) { + throw new InvalidValueException(String.format( + "verb '%s' failed, variable='%s' parameter='%s': %s", verb, variable, + parameter.getObjectValue(), e), e); + } + this.success = true; + + if (LOG.isDebugEnabled()) { + LOG.debug(String.format("%s verb='%s' success=%s variable: %s=%s parameter=%s", + statementId(namespace), verb, this.success, variable, variable.get(), + parameter.getObjectValue())); + } + return ProcessResult.STATEMENT_CONTINUE; + } + + private ProcessResult verbIn(String verb, Map<String, Object> namespace, List<Object> statement) { + Token member = getParameter(verb, statement, 1, namespace, null); + Token collection = getParameter(verb, statement, 2, namespace, + EnumSet.of(TokenType.ARRAY, TokenType.MAP, TokenType.STRING)); + + switch (collection.type) { + case ARRAY: { + this.success = collection.getListValue().contains(member.getObjectValue()); + } + break; + case MAP: { + if (member.type != TokenType.STRING) { + throw new InvalidTypeException(String.format( + "verb '%s' requires parameter #1 to be a %swhen parameter #2 is a %s", + TokenType.STRING, collection.type)); + } + this.success = collection.getMapValue().containsKey(member.getObjectValue()); + } + break; + case STRING: { + if (member.type != TokenType.STRING) { + throw new InvalidTypeException(String.format( + "verb '%s' requires parameter #1 to be a %swhen parameter #2 is a %s", + TokenType.STRING, collection.type)); + } + this.success = (collection.getStringValue()).contains(member.getStringValue()); + } + break; + default: + throw new IllegalStateException(String.format("unexpected token type: %s", + collection.type)); + } + + if (LOG.isDebugEnabled()) { + LOG.debug(String.format("%s verb='%s' success=%s member=%s collection=%s", + statementId(namespace), verb, this.success, member.getObjectValue(), + collection.getObjectValue())); + } + return ProcessResult.STATEMENT_CONTINUE; + } + + private ProcessResult verbNotIn(String verb, Map<String, Object> namespace, + List<Object> statement) { + Token member = getParameter(verb, statement, 1, namespace, null); + Token collection = getParameter(verb, statement, 2, namespace, + EnumSet.of(TokenType.ARRAY, TokenType.MAP, TokenType.STRING)); + + switch (collection.type) { + case ARRAY: { + this.success = !collection.getListValue().contains(member.getObjectValue()); + } + break; + case MAP: { + if (member.type != TokenType.STRING) { + throw new InvalidTypeException(String.format( + "verb '%s' requires parameter #1 to be a %swhen parameter #2 is a %s", + TokenType.STRING, collection.type)); + } + this.success = !collection.getMapValue().containsKey(member.getObjectValue()); + } + break; + case STRING: { + if (member.type != TokenType.STRING) { + throw new InvalidTypeException(String.format( + "verb '%s' requires parameter #1 to be a %swhen parameter #2 is a %s", + TokenType.STRING, collection.type)); + } + this.success = !(collection.getStringValue()).contains(member.getStringValue()); + } + break; + default: + throw new IllegalStateException(String.format("unexpected token type: %s", + collection.type)); + } + + if (LOG.isDebugEnabled()) { + LOG.debug(String.format("%s verb='%s' success=%s member=%s collection=%s", + statementId(namespace), verb, this.success, member.getObjectValue(), + collection.getObjectValue())); + } + + return ProcessResult.STATEMENT_CONTINUE; + } + + private ProcessResult verbCompare(String verb, Map<String, Object> namespace, + List<Object> statement) { + Token left = getParameter(verb, statement, 1, namespace, null); + Token op = getParameter(verb, statement, 2, namespace, EnumSet.of(TokenType.STRING)); + Token right = getParameter(verb, statement, 3, namespace, null); + String invalidOp = "operator %s not supported for type %s"; + TokenType tokenType; + String opValue = op.getStringValue(); + boolean result; + + if (left.type != right.type) { + throw new InvalidTypeException(String.format( + "verb '%s' both items must have the same type left is %s and right is %s", + verb, left.type, right.type)); + } else { + tokenType = left.type; + } + + switch (opValue) { + case "==": + case "!=": { + switch (tokenType) { + case STRING: { + String leftValue = left.getStringValue(); + String rightValue = right.getStringValue(); + result = leftValue.equals(rightValue); + } + break; + case INTEGER: { + Long leftValue = left.getLongValue(); + Long rightValue = right.getLongValue(); + result = leftValue.equals(rightValue); + } + break; + case REAL: { + Double leftValue = left.getDoubleValue(); + Double rightValue = right.getDoubleValue(); + result = leftValue.equals(rightValue); + } + break; + case ARRAY: { + List<Object> leftValue = left.getListValue(); + List<Object> rightValue = right.getListValue(); + result = leftValue.equals(rightValue); + } + break; + case MAP: { + Map<String, Object> leftValue = left.getMapValue(); + Map<String, Object> rightValue = right.getMapValue(); + result = leftValue.equals(rightValue); + } + break; + case BOOLEAN: { + Boolean leftValue = left.getBooleanValue(); + Boolean rightValue = right.getBooleanValue(); + result = leftValue.equals(rightValue); + } + break; + case NULL: { + result = (left.getNullValue() == right.getNullValue()); + } + break; + default: { + throw new IllegalStateException(String.format("unexpected token type: %s", + tokenType)); + } + } + if (opValue.equals("!=")) { // negate the sense of the test + result = !result; + } + } + break; + case "<": + case ">=": { + switch (tokenType) { + case STRING: { + String leftValue = left.getStringValue(); + String rightValue = right.getStringValue(); + result = leftValue.compareTo(rightValue) < 0; + } + break; + case INTEGER: { + Long leftValue = left.getLongValue(); + Long rightValue = right.getLongValue(); + result = leftValue < rightValue; + } + break; + case REAL: { + Double leftValue = left.getDoubleValue(); + Double rightValue = right.getDoubleValue(); + result = leftValue < rightValue; + } + break; + case ARRAY: + case MAP: + case BOOLEAN: + case NULL: { + throw new InvalidRuleException(String.format(invalidOp, opValue, tokenType)); + } + default: { + throw new IllegalStateException(String.format("unexpected token type: %s", + tokenType)); + } + } + if (opValue.equals(">=")) { // negate the sense of the test + result = !result; + } + } + break; + case ">": + case "<=": { + switch (tokenType) { + case STRING: { + String leftValue = left.getStringValue(); + String rightValue = right.getStringValue(); + result = leftValue.compareTo(rightValue) > 0; + } + break; + case INTEGER: { + Long leftValue = left.getLongValue(); + Long rightValue = right.getLongValue(); + result = leftValue > rightValue; + } + break; + case REAL: { + Double leftValue = left.getDoubleValue(); + Double rightValue = right.getDoubleValue(); + result = leftValue > rightValue; + } + break; + case ARRAY: + case MAP: + case BOOLEAN: + case NULL: { + throw new InvalidRuleException(String.format(invalidOp, opValue, tokenType)); + } + default: { + throw new IllegalStateException(String.format("unexpected token type: %s", + tokenType)); + } + } + if (opValue.equals("<=")) { // negate the sense of the test + result = !result; + } + } + break; + default: { + throw new InvalidRuleException(String.format( + "verb '%s' has unknown comparison operator '%s'", verb, op.getObjectValue())); + } + } + this.success = result; + + if (LOG.isDebugEnabled()) { + LOG.debug(String.format("%s verb='%s' success=%s left=%s op='%s' right=%s", + statementId(namespace), verb, this.success, left.getObjectValue(), + op.getObjectValue(), right.getObjectValue())); + } + return ProcessResult.STATEMENT_CONTINUE; + } + + private ProcessResult verbRegexp(String verb, Map<String, Object> namespace, + List<Object> statement) { + Token string = getParameter(verb, statement, 1, namespace, EnumSet.of(TokenType.STRING)); + Token pattern = getParameter(verb, statement, 2, namespace, EnumSet.of(TokenType.STRING)); + + Pattern regexp; + Matcher matcher; + + try { + regexp = Pattern.compile(pattern.getStringValue()); + } catch (Exception e) { + throw new InvalidValueException(String.format( + "verb '%s' failed, bad regular expression pattern '%s', %s", verb, + pattern.getObjectValue(), e)); + } + matcher = regexp.matcher(string.getStringValue()); + + if (matcher.find()) { + this.success = true; + namespace.put(REGEXP_ARRAY_VARIABLE, regexpGroupList(matcher)); + namespace.put(REGEXP_MAP_VARIABLE, regexpGroupMap(pattern.getStringValue(), matcher)); + } else { + this.success = false; + namespace.put(REGEXP_ARRAY_VARIABLE, new ArrayList<Object>()); + namespace.put(REGEXP_MAP_VARIABLE, new HashMap<String, Object>()); + } + + if (LOG.isDebugEnabled()) { + LOG.debug(String.format( + "%s verb='%s' success=%s string='%s' pattern='%s' %s=%s %s=%s", + statementId(namespace), verb, this.success, string.getObjectValue(), + pattern.getObjectValue(), REGEXP_ARRAY_VARIABLE, + namespace.get(REGEXP_ARRAY_VARIABLE), REGEXP_MAP_VARIABLE, + namespace.get(REGEXP_MAP_VARIABLE))); + } + + return ProcessResult.STATEMENT_CONTINUE; + } + + private ProcessResult verbRegexpReplace(String verb, Map<String, Object> namespace, + List<Object> statement) { + Token variable = getVariable(verb, statement, 1, namespace); + Token string = getParameter(verb, statement, 2, namespace, EnumSet.of(TokenType.STRING)); + Token pattern = getParameter(verb, statement, 3, namespace, EnumSet.of(TokenType.STRING)); + Token replacement = getParameter(verb, statement, 4, namespace, + EnumSet.of(TokenType.STRING)); + + Pattern regexp; + Matcher matcher; + String newValue; + + try { + regexp = Pattern.compile(pattern.getStringValue()); + } catch (Exception e) { + throw new InvalidValueException(String.format( + "verb '%s' failed, bad regular expression pattern '%s', %s", verb, + pattern.getObjectValue(), e)); + } + matcher = regexp.matcher(string.getStringValue()); + + newValue = matcher.replaceAll(replacement.getStringValue()); + variable.set(newValue); + this.success = true; + + if (LOG.isDebugEnabled()) { + LOG.debug(String.format( + "%s verb='%s' success=%s variable: %s=%s string='%s' pattern='%s' replacement='%s'", + statementId(namespace), verb, this.success, variable, variable.get(), + string.getObjectValue(), pattern.getObjectValue(), replacement.getObjectValue())); + } + + return ProcessResult.STATEMENT_CONTINUE; + } + + private ProcessResult verbExit(String verb, Map<String, Object> namespace, + List<Object> statement) { + ProcessResult statementResult = ProcessResult.STATEMENT_CONTINUE; + + Token exitStatusParam = getParameter(verb, statement, 1, namespace, + EnumSet.of(TokenType.STRING)); + Token criteriaParam = getParameter(verb, statement, 2, namespace, + EnumSet.of(TokenType.STRING)); + String exitStatus = (exitStatusParam.getStringValue()).toLowerCase(); + String criteria = (criteriaParam.getStringValue()).toLowerCase(); + ProcessResult result; + boolean doExit; + + if (exitStatus.equals("rule_succeeds")) { + result = ProcessResult.RULE_SUCCESS; + } else if (exitStatus.equals("rule_fails")) { + result = ProcessResult.RULE_FAIL; + } else { + throw new InvalidRuleException(String.format("verb='%s' unknown exit status '%s'", + verb, exitStatus)); + } + + if (criteria.equals("if_success")) { + if (this.success) { + doExit = true; + } else { + doExit = false; + } + } else if (criteria.equals("if_not_success")) { + if (!this.success) { + doExit = true; + } else { + doExit = false; + } + } else if (criteria.equals("always")) { + doExit = true; + } else if (criteria.equals("never")) { + doExit = false; + } else { + throw new InvalidRuleException(String.format("verb='%s' unknown exit criteria '%s'", + verb, criteria)); + } + + if (doExit) { + statementResult = result; + } + + if (LOG.isDebugEnabled()) { + LOG.debug(String.format( + "%s verb='%s' success=%s status=%s criteria=%s exiting=%s result=%s", + statementId(namespace), verb, this.success, exitStatus, criteria, doExit, + statementResult)); + } + + return statementResult; + } + + private ProcessResult verbContinue(String verb, Map<String, Object> namespace, + List<Object> statement) { + ProcessResult statementResult = ProcessResult.STATEMENT_CONTINUE; + Token criteriaParam = getParameter(verb, statement, 1, namespace, + EnumSet.of(TokenType.STRING)); + String criteria = (criteriaParam.getStringValue()).toLowerCase(); + boolean doContinue; + + if (criteria.equals("if_success")) { + if (this.success) { + doContinue = true; + } else { + doContinue = false; + } + } else if (criteria.equals("if_not_success")) { + if (!this.success) { + doContinue = true; + } else { + doContinue = false; + } + } else if (criteria.equals("always")) { + doContinue = true; + } else if (criteria.equals("never")) { + doContinue = false; + } else { + throw new InvalidRuleException(String.format( + "verb='%s' unknown continue criteria '%s'", verb, criteria)); + } + + if (doContinue) { + statementResult = ProcessResult.BLOCK_CONTINUE; + } + + if (LOG.isDebugEnabled()) { + LOG.debug(String.format( + "%s verb='%s' success=%s criteria=%s continuing=%s result=%s", + statementId(namespace), verb, this.success, criteria, doContinue, + statementResult)); + } + + return statementResult; + } + +} diff --git a/odl-aaa-moon/aaa/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/StatementErrorException.java b/odl-aaa-moon/aaa/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/StatementErrorException.java new file mode 100644 index 00000000..6abab3ee --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/StatementErrorException.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2014 Red Hat, Inc. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ + +package org.opendaylight.aaa.idpmapping; + +/** + * Exception thrown when a mapping rule statement fails. + * + * @author John Dennis <jdennis@redhat.com> + */ + +public class StatementErrorException extends RuntimeException { + + private static final long serialVersionUID = 8312665727576018327L; + + public StatementErrorException() { + } + + public StatementErrorException(String message) { + super(message); + } + + public StatementErrorException(Throwable cause) { + super(cause); + } + + public StatementErrorException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/odl-aaa-moon/aaa/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/Token.java b/odl-aaa-moon/aaa/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/Token.java new file mode 100644 index 00000000..402fb064 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/Token.java @@ -0,0 +1,401 @@ +/* + * Copyright (c) 2014 Red Hat, Inc. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ + +package org.opendaylight.aaa.idpmapping; + +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +enum TokenStorageType { + UNKNOWN, CONSTANT, VARIABLE +} + +enum TokenType { + STRING, // java String + ARRAY, // java List + MAP, // java Map + INTEGER, // java Long + BOOLEAN, // java Boolean + NULL, // java null + REAL, // java Double + UNKNOWN, // undefined +} + +/** + * Rule statements can contain variables or constants, this class encapsulates + * those values, enforces type handling and supports reading and writing of + * those values. + * + * Technically at the syntactic level these are not tokens. A token would have + * finer granularity such as identifier, operator, etc. I just couldn't think of + * a better name for how they're used here and thought token was a reasonable + * compromise as a name. + * + * @author John Dennis <jdennis@redhat.com> + */ + +class Token { + + /* + * Regexp to identify a variable beginning with $ Supports array notation, + * e.g. $foo[bar] Optional delimiting braces may be used to separate + * variable from surrounding text. + * + * Examples: $foo ${foo} $foo[bar] ${foo[bar] where foo is the variable name + * and bar is the array index. + * + * Identifer is any alphabetic followed by alphanumeric or underscore + */ + private static final String VARIABLE_PAT = "(?<!\\\\)\\$" + // non-escaped $ + // sign + "\\{?" + // optional delimiting brace + "([a-zA-Z][a-zA-Z0-9_]*)" + // group 1: variable name + "(\\[" + // group 2: optional index + "([a-zA-Z0-9_]+)" + // group 3: array index + "\\])?" + // end optional index + "\\}?"; // optional delimiting brace + public static final Pattern VARIABLE_RE = Pattern.compile(VARIABLE_PAT); + /* + * Requires only a variable to be present in the string but permits leading + * and trailing whitespace. + */ + private static final String VARIABLE_ONLY_PAT = "^\\s*" + VARIABLE_PAT + "\\s*$"; + public static final Pattern VARIABLE_ONLY_RE = Pattern.compile(VARIABLE_ONLY_PAT); + + private Object value = null; + + public Map<String, Object> namespace = null; + public TokenStorageType storageType = TokenStorageType.UNKNOWN; + public TokenType type = TokenType.UNKNOWN; + public String name = null; + public String index = null; + + Token(Object input, Map<String, Object> namespace) { + this.namespace = namespace; + if (input instanceof String) { + parseVariable((String) input); + if (this.storageType == TokenStorageType.CONSTANT) { + this.value = input; + this.type = classify(input); + } + } else { + this.storageType = TokenStorageType.CONSTANT; + this.value = input; + this.type = classify(input); + } + } + + @Override + public String toString() { + if (this.storageType == TokenStorageType.CONSTANT) { + return String.format("%s", this.value); + } else if (this.storageType == TokenStorageType.VARIABLE) { + if (this.index == null) { + return String.format("$%s", this.name); + } else { + return String.format("$%s[%s]", this.name, this.index); + } + } else { + return "UNKNOWN"; + } + } + + void parseVariable(String string) { + Matcher matcher = VARIABLE_ONLY_RE.matcher(string); + if (matcher.find()) { + String name = matcher.group(1); + String index = matcher.group(3); + + this.storageType = TokenStorageType.VARIABLE; + this.name = name; + this.index = index; + } else { + this.storageType = TokenStorageType.CONSTANT; + } + } + + public static TokenType classify(Object value) { + TokenType tokenType = TokenType.UNKNOWN; + // ordered by expected occurrence + if (value instanceof String) { + tokenType = TokenType.STRING; + } else if (value instanceof List) { + tokenType = TokenType.ARRAY; + } else if (value instanceof Map) { + tokenType = TokenType.MAP; + } else if (value instanceof Long) { + tokenType = TokenType.INTEGER; + } else if (value instanceof Boolean) { + tokenType = TokenType.BOOLEAN; + } else if (value == null) { + tokenType = TokenType.NULL; + } else if (value instanceof Double) { + tokenType = TokenType.REAL; + } else { + throw new InvalidRuleException(String.format( + "Type must be String, Long, Double, Boolean, List, Map, or null, not %s", + value.getClass().getSimpleName(), value)); + } + return tokenType; + } + + Object get() { + return get(null); + } + + Object get(Object index) { + Object base = null; + + if (this.storageType == TokenStorageType.CONSTANT) { + return this.value; + } + + if (this.namespace.containsKey(this.name)) { + base = this.namespace.get(this.name); + } else { + throw new UndefinedValueException(String.format("variable '%s' not defined", this.name)); + } + + if (index == null) { + index = this.index; + } + + if (index == null) { // scalar types + value = base; + } else { + if (base instanceof List) { + @SuppressWarnings("unchecked") + List<Object> list = (List<Object>) base; + Integer idx = null; + + if (index instanceof Long) { + idx = new Integer(((Long) index).intValue()); + } else if (index instanceof String) { + try { + idx = new Integer((String) index); + } catch (NumberFormatException e) { + throw new InvalidTypeException( + String.format( + "variable '%s' is an array indexed by '%s', however the index cannot be converted to an integer", + this.name, index, e)); + } + } else { + throw new InvalidTypeException( + String.format( + "variable '%s' is an array indexed by '%s', however the index must be an integer or string not %s", + this.name, index, index.getClass().getSimpleName())); + } + + try { + value = list.get(idx); + } catch (IndexOutOfBoundsException e) { + throw new UndefinedValueException( + String.format( + "variable '%s' is an array of size %d indexed by '%s', however the index is out of bounds", + this.name, list.size(), idx, e)); + } + } else if (base instanceof Map) { + @SuppressWarnings("unchecked") + Map<String, Object> map = (Map<String, Object>) base; + String idx = null; + if (index instanceof String) { + idx = (String) index; + } else { + throw new InvalidTypeException( + String.format( + "variable '%s' is a map indexed by '%s', however the index must be a string not %s", + this.name, index, index.getClass().getSimpleName())); + } + if (!map.containsKey(idx)) { + throw new UndefinedValueException( + String.format( + "variable '%s' is a map indexed by '%s', however the index does not exist", + this.name, index)); + } + value = map.get(idx); + } else { + throw new InvalidTypeException( + String.format( + "variable '%s' is indexed by '%s', variable must be an array or map, not %s", + this.name, index, base.getClass().getSimpleName())); + + } + } + this.type = classify(value); + return value; + } + + void set(Object value) { + set(value, null); + } + + void set(Object value, Object index) { + + if (this.storageType == TokenStorageType.CONSTANT) { + throw new InvalidTypeException("cannot assign to a constant"); + } + + if (index == null) { + index = this.index; + } + + if (index == null) { // scalar types + this.namespace.put(this.name, value); + } else { + Object base = null; + + if (this.namespace.containsKey(this.name)) { + base = this.namespace.get(this.name); + } else { + throw new UndefinedValueException(String.format("variable '%s' not defined", + this.name)); + } + + if (base instanceof List) { + @SuppressWarnings("unchecked") + List<Object> list = (List<Object>) base; + Integer idx = null; + + if (index instanceof Long) { + idx = new Integer(((Long) index).intValue()); + } else if (index instanceof String) { + try { + idx = new Integer((String) index); + } catch (NumberFormatException e) { + throw new InvalidTypeException( + String.format( + "variable '%s' is an array indexed by '%s', however the index cannot be converted to an integer", + this.name, index, e)); + } + } else { + throw new InvalidTypeException( + String.format( + "variable '%s' is an array indexed by '%s', however the index must be an integer or string not %s", + this.name, index, index.getClass().getSimpleName())); + } + + try { + value = list.set(idx, value); + } catch (IndexOutOfBoundsException e) { + throw new UndefinedValueException( + String.format( + "variable '%s' is an array of size %d indexed by '%s', however the index is out of bounds", + this.name, list.size(), idx, e)); + } + } else if (base instanceof Map) { + @SuppressWarnings("unchecked") + Map<String, Object> map = (Map<String, Object>) base; + String idx = null; + if (index instanceof String) { + idx = (String) index; + } else { + throw new InvalidTypeException( + String.format( + "variable '%s' is a map indexed by '%s', however the index must be a string not %s", + this.name, index, index.getClass().getSimpleName())); + } + if (!map.containsKey(idx)) { + throw new UndefinedValueException( + String.format( + "variable '%s' is a map indexed by '%s', however the index does not exist", + this.name, index)); + } + value = map.put(idx, value); + } else { + throw new InvalidTypeException( + String.format( + "variable '%s' is indexed by '%s', variable must be an array or map, not %s", + this.name, index, base.getClass().getSimpleName())); + + } + } + } + + public Object load() { + this.value = get(); + return this.value; + } + + public Object load(Object index) { + this.value = get(index); + return this.value; + } + + public String getStringValue() { + if (this.type == TokenType.STRING) { + return (String) this.value; + } else { + throw new InvalidTypeException(String.format("expected %s value but token type is %s", + TokenType.STRING, this.type)); + } + } + + public List<Object> getListValue() { + if (this.type == TokenType.ARRAY) { + @SuppressWarnings("unchecked") + List<Object> list = (List<Object>) this.value; + return list; + } else { + throw new InvalidTypeException(String.format("expected %s value but token type is %s", + TokenType.ARRAY, this.type)); + } + } + + public Map<String, Object> getMapValue() { + if (this.type == TokenType.MAP) { + @SuppressWarnings("unchecked") + Map<String, Object> map = (Map<String, Object>) this.value; + return map; + } else { + throw new InvalidTypeException(String.format("expected %s value but token type is %s", + TokenType.MAP, this.type)); + } + } + + public Long getLongValue() { + if (this.type == TokenType.INTEGER) { + return (Long) this.value; + } else { + throw new InvalidTypeException(String.format("expected %s value but token type is %s", + TokenType.INTEGER, this.type)); + } + } + + public Boolean getBooleanValue() { + if (this.type == TokenType.BOOLEAN) { + return (Boolean) this.value; + } else { + throw new InvalidTypeException(String.format("expected %s value but token type is %s", + TokenType.BOOLEAN, this.type)); + } + } + + public Double getDoubleValue() { + if (this.type == TokenType.REAL) { + return (Double) this.value; + } else { + throw new InvalidTypeException(String.format("expected %s value but token type is %s", + TokenType.REAL, this.type)); + } + } + + public Object getNullValue() { + if (this.type == TokenType.NULL) { + return this.value; + } else { + throw new InvalidTypeException(String.format("expected %s value but token type is %s", + TokenType.NULL, this.type)); + } + } + + public Object getObjectValue() { + return this.value; + } +} diff --git a/odl-aaa-moon/aaa/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/UndefinedValueException.java b/odl-aaa-moon/aaa/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/UndefinedValueException.java new file mode 100644 index 00000000..7200da3d --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/UndefinedValueException.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2014 Red Hat, Inc. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.aaa.idpmapping; + +/** + * Exception thrown when a statement references an undefined value. + * + * @author John Dennis <jdennis@redhat.com> + */ + +public class UndefinedValueException extends RuntimeException { + + private static final long serialVersionUID = -1607453931670834435L; + + public UndefinedValueException() { + } + + public UndefinedValueException(String message) { + super(message); + } + + public UndefinedValueException(Throwable cause) { + super(cause); + } + + public UndefinedValueException(String message, Throwable cause) { + super(message, cause); + } +} |