summaryrefslogtreecommitdiffstats
path: root/odl-aaa-moon/aaa-idp-mapping
diff options
context:
space:
mode:
Diffstat (limited to 'odl-aaa-moon/aaa-idp-mapping')
-rw-r--r--odl-aaa-moon/aaa-idp-mapping/pom.xml84
-rw-r--r--odl-aaa-moon/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/Activator.java25
-rw-r--r--odl-aaa-moon/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/IdpJson.java248
-rw-r--r--odl-aaa-moon/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/InvalidRuleException.java35
-rw-r--r--odl-aaa-moon/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/InvalidTypeException.java35
-rw-r--r--odl-aaa-moon/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/InvalidValueException.java35
-rw-r--r--odl-aaa-moon/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/RuleProcessor.java1368
-rw-r--r--odl-aaa-moon/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/StatementErrorException.java35
-rw-r--r--odl-aaa-moon/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/Token.java401
-rw-r--r--odl-aaa-moon/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/UndefinedValueException.java34
-rw-r--r--odl-aaa-moon/aaa-idp-mapping/src/test/java/org/opendaylight/aaa/idpmapping/RuleProcessorTest.java130
-rw-r--r--odl-aaa-moon/aaa-idp-mapping/src/test/java/org/opendaylight/aaa/idpmapping/TokenTest.java66
12 files changed, 2496 insertions, 0 deletions
diff --git a/odl-aaa-moon/aaa-idp-mapping/pom.xml b/odl-aaa-moon/aaa-idp-mapping/pom.xml
new file mode 100644
index 00000000..99c2322d
--- /dev/null
+++ b/odl-aaa-moon/aaa-idp-mapping/pom.xml
@@ -0,0 +1,84 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>org.opendaylight.aaa</groupId>
+ <artifactId>aaa-parent</artifactId>
+ <version>0.3.1-Beryllium-SR1</version>
+ <relativePath>../parent</relativePath>
+ </parent>
+
+ <artifactId>aaa-authn-idpmapping</artifactId>
+ <version>0.3.1-Beryllium-SR1</version>
+ <packaging>bundle</packaging>
+
+ <properties>
+ <powermock.version>1.5.2</powermock.version>
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.glassfish</groupId>
+ <artifactId>javax.json</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.osgi</groupId>
+ <artifactId>org.osgi.core</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>org.apache.felix.dependencymanager</artifactId>
+ <scope>provided</scope>
+ </dependency>
+
+ <!-- Test dependencies -->
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-all</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-simple</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.powermock</groupId>
+ <artifactId>powermock-api-mockito</artifactId>
+ <version>${powermock.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.powermock</groupId>
+ <artifactId>powermock-module-junit4</artifactId>
+ <version>${powermock.version}</version>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>maven-bundle-plugin</artifactId>
+ <extensions>true</extensions>
+ <configuration>
+ <instructions>
+ <Bundle-Activator>org.opendaylight.aaa.idpmapping.Activator</Bundle-Activator>
+ </instructions>
+ <manifestLocation>${project.basedir}/META-INF</manifestLocation>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git a/odl-aaa-moon/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/Activator.java b/odl-aaa-moon/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-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-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/IdpJson.java b/odl-aaa-moon/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-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 &lt;jdennis@redhat.com&gt;
+ */
+
+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-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/InvalidRuleException.java b/odl-aaa-moon/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-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 &lt;jdennis@redhat.com&gt;
+ */
+
+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-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/InvalidTypeException.java b/odl-aaa-moon/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-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 &lt;jdennis@redhat.com&gt;
+ */
+
+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-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/InvalidValueException.java b/odl-aaa-moon/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-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 &lt;jdennis@redhat.com&gt;
+ */
+
+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-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/RuleProcessor.java b/odl-aaa-moon/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-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 &lt;jdennis@redhat.com&gt;
+ */
+
+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-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/StatementErrorException.java b/odl-aaa-moon/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-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 &lt;jdennis@redhat.com&gt;
+ */
+
+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-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/Token.java b/odl-aaa-moon/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-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-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/UndefinedValueException.java b/odl-aaa-moon/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-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 &lt;jdennis@redhat.com&gt;
+ */
+
+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);
+ }
+}
diff --git a/odl-aaa-moon/aaa-idp-mapping/src/test/java/org/opendaylight/aaa/idpmapping/RuleProcessorTest.java b/odl-aaa-moon/aaa-idp-mapping/src/test/java/org/opendaylight/aaa/idpmapping/RuleProcessorTest.java
new file mode 100644
index 00000000..84d403f9
--- /dev/null
+++ b/odl-aaa-moon/aaa-idp-mapping/src/test/java/org/opendaylight/aaa/idpmapping/RuleProcessorTest.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (c) 2016 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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.powermock.api.mockito.PowerMockito;
+import org.powermock.api.support.membermodification.MemberMatcher;
+import org.powermock.api.support.membermodification.MemberModifier;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+import org.powermock.reflect.Whitebox;
+
+@PrepareForTest(RuleProcessor.class)
+@RunWith(PowerMockRunner.class)
+public class RuleProcessorTest {
+
+ @Mock
+ private RuleProcessor ruleProcess;
+
+ @Before
+ public void setUp() {
+ ruleProcess = PowerMockito.mock(RuleProcessor.class, Mockito.CALLS_REAL_METHODS);
+ }
+
+ @Test
+ public void testJoin() {
+ List<Object> list = new ArrayList<Object>();
+ list.add("str1");
+ list.add("str2");
+ list.add("str3");
+ assertEquals("str1/str2/str3", RuleProcessor.join(list, "/"));
+ }
+
+ @Test
+ public void testSubstituteVariables() {
+ Map<String, Object> namespace = new HashMap<String, Object>() {
+ {
+ put("foo1", new HashMap<String, String>() {
+ {
+ put("0", "1");
+ }
+ });
+ }
+ };
+ String str = "foo1[0]";
+ String subVariable = ruleProcess.substituteVariables(str, namespace);
+ assertNotNull(subVariable);
+ assertEquals(subVariable, str);
+ }
+
+ @Test
+ public void testGetMapping() {
+ Map<String, Object> namespace = new HashMap<String, Object>() {
+ {
+ put("foo1", new HashMap<String, String>() {
+ {
+ put("0", "1");
+ }
+ });
+ }
+ };
+ final Map<String, Object> item = new HashMap<String, Object>() {
+ {
+ put("str", "val");
+ }
+ };
+ Map<String, Object> rules = new HashMap<String, Object>() {
+ {
+ put("mapping", item);
+ put("mapping_name", "mapping");
+ }
+ };
+ Map<String, Object> mapping = ruleProcess.getMapping(namespace, rules);
+ assertNotNull(mapping);
+ assertTrue(mapping.containsKey("str"));
+ assertEquals("val", mapping.get("str"));
+ }
+
+ @Test
+ public void testProcess() throws Exception {
+ String json = " {\"rules\":[" + "{\"Name\":\"user\", \"Id\":1},"
+ + "{\"Name\":\"Admin\", \"Id\":2}]} ";
+ Map<String, Object> mapping = new HashMap<String, Object>() {
+ {
+ put("Name", "Admin");
+ }
+ };
+ List<Map<String, Object>> internalRules = new ArrayList<Map<String, Object>>();
+ Map<String, Object> internalRule = new HashMap<String, Object>() {
+ {
+ put("Name", "Admin");
+ put("statement_blocks", "user");
+ }
+ };
+ internalRules.add(internalRule);
+ MemberModifier.field(RuleProcessor.class, "rules").set(ruleProcess, internalRules);
+ PowerMockito.suppress(MemberMatcher.method(RuleProcessor.class, "processRule", Map.class,
+ Map.class));
+ PowerMockito.when(ruleProcess, "processRule", any(Map.class), any(Map.class)).thenReturn(
+ ProcessResult.RULE_SUCCESS);
+ PowerMockito.suppress(MemberMatcher.method(RuleProcessor.class, "getMapping", Map.class,
+ Map.class));
+ when(ruleProcess.getMapping(any(Map.class), any(Map.class))).thenReturn(mapping);
+ Whitebox.invokeMethod(ruleProcess, "process", json);
+ verify(ruleProcess, times(3)).getMapping(any(Map.class), any(Map.class));
+ }
+
+}
diff --git a/odl-aaa-moon/aaa-idp-mapping/src/test/java/org/opendaylight/aaa/idpmapping/TokenTest.java b/odl-aaa-moon/aaa-idp-mapping/src/test/java/org/opendaylight/aaa/idpmapping/TokenTest.java
new file mode 100644
index 00000000..d6181051
--- /dev/null
+++ b/odl-aaa-moon/aaa-idp-mapping/src/test/java/org/opendaylight/aaa/idpmapping/TokenTest.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2016 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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.Test;
+
+public class TokenTest {
+
+ private final Map<String, Object> namespace = new HashMap<String, Object>() {
+ {
+ put("foo1", new HashMap<String, String>() {
+ {
+ put("0", "1");
+ }
+ });
+ }
+ };
+ private Object input = "$foo1[0]";
+ private Token token = new Token(input, namespace);
+ private Token mapToken = new Token(namespace, namespace);
+
+ @Test
+ public void testToken() {
+ assertEquals(token.toString(), input);
+ assertTrue(token.storageType == TokenStorageType.VARIABLE);
+ assertEquals(mapToken.toString(), "{foo1={0=1}}");
+ assertTrue(mapToken.storageType == TokenStorageType.CONSTANT);
+ }
+
+ @Test
+ public void testClassify() {
+ assertEquals(Token.classify(new ArrayList<>()), TokenType.ARRAY);
+ assertEquals(Token.classify(true), TokenType.BOOLEAN);
+ assertEquals(Token.classify(new Long(365)), TokenType.INTEGER);
+ assertEquals(Token.classify(new HashMap<String, Object>()), TokenType.MAP);
+ assertEquals(Token.classify(null), TokenType.NULL);
+ assertEquals(Token.classify(365.00), TokenType.REAL);
+ assertEquals(Token.classify("foo_str"), TokenType.STRING);
+ }
+
+ @Test
+ public void testGet() {
+ assertNotNull(token.get());
+ assertTrue(token.get("0") == "1");
+ assertNotNull(mapToken.get());
+ assertTrue(mapToken.get(0) == namespace);
+ }
+
+ @Test
+ public void testGetMapValue() {
+ assertTrue(mapToken.getMapValue() == namespace);
+ }
+}