summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--doc/mapping.rst1609
-rw-r--r--java/src/main/java/com/redhat/IdPMapping/IdpJson.java254
-rw-r--r--java/src/main/java/com/redhat/IdPMapping/InvalidRuleException.java34
-rw-r--r--java/src/main/java/com/redhat/IdPMapping/InvalidTypeException.java35
-rw-r--r--java/src/main/java/com/redhat/IdPMapping/InvalidValueException.java34
-rw-r--r--java/src/main/java/com/redhat/IdPMapping/RuleProcessor.java1382
-rw-r--r--java/src/main/java/com/redhat/IdPMapping/StatementErrorException.java34
-rw-r--r--java/src/main/java/com/redhat/IdPMapping/Token.java406
-rw-r--r--java/src/main/java/com/redhat/IdPMapping/UndefinedValueException.java34
-rw-r--r--java/src/main/java/com/redhat/app/MappingApp.java57
-rwxr-xr-xpython/idp_mapping.py1055
-rwxr-xr-xpython/mapping_app.py55
12 files changed, 4989 insertions, 0 deletions
diff --git a/doc/mapping.rst b/doc/mapping.rst
new file mode 100644
index 0000000..3363550
--- /dev/null
+++ b/doc/mapping.rst
@@ -0,0 +1,1609 @@
+Operation Model
+===============
+
+The assertions from an IdP are stored in an associative array. A
+sequence of rules are applied, the first rule which returns success is
+considered a match. During the execution of each rule values from the
+assertion can be tested and transformed with the results selectively
+stored in variables local to the rule. If the rule succeeds an
+associative array of mapped values is returned. The mapped values are
+taken from the local variables set during the rule execution. The
+definition of the rules and mapped results are expressed in JSON
+notation.
+
+A rule is somewhat akin to a function in a programming language. It
+starts execution with a set of predefined local variables. It executes
+statements which are grouped together in blocks. Execution continues
+until an `exit`_ statement returning a success/fail result is
+executed or until the last statement is reached which implies
+success. The remaining statements in a block may be skipped via a
+`continue`_ statement which tests a condition, this is equivalent to
+an "if" control flow of logic in a programming language.
+
+Rule execution continues until a rule returns success. Each rule has a
+`mapping`_ associative array bound to it which is a template for the
+transformed result. Upon success the `mapping`_ template for the
+rule is loaded and the local variables from the successful rule are
+used to populate the values in the `mapping`_ template yielding the
+final mapped result.
+
+If no rules returns success authentication fails.
+
+
+Pseudo Code Illustrating Operational Model
+------------------------------------------
+
+::
+
+ mapped = null
+ foreach rule in rules {
+ result = null
+ initialize rule.variables with pre-defined values
+
+ foreach block in rule.statement_blocks {
+ for statement in block.statements {
+ if statement.verb is exit {
+ result = exit.status
+ break
+ }
+ elif statement.verb is continue {
+ break
+ }
+ }
+ if result {
+ break
+ }
+ if result == null {
+ result = success
+ }
+ if result == success {
+ mapped = rule.mapping(rule.variables)
+ }
+ return mapped
+
+
+
+Structure Of Rule Definitions
+=============================
+
+Rules are loaded by the rule processor via a JSON document called a
+rule definition. A definition has an *optional* set of mapping
+templates and a list of rules. Each rule has specifies a mapping
+template and has a list of statement blocks. Each statement block has
+a list of statements.
+
+In pseudo-JSON (JSON does not have comments, the ... ellipsis is a
+place holder):
+
+::
+
+ {
+ "mappings": {
+ "template1": "{...}",
+ "template2": "{...}"
+ },
+ "rules": [
+ { # Rule 0. A rule has a mapping or a mapping name
+ # and a list of statement blocks
+
+ "mapping": {...},
+ # -OR-
+ "mapping_name": "template1",
+
+ "statement_blocks": [
+ [ # Block 0
+ [statement 0]
+ [statement 1]
+ ],
+ [ # Block 1
+ [statement 0]
+ [statement 1]
+ ],
+
+ ]
+ },
+ { # Rule 1 ...
+ }
+ ]
+
+ }
+
+Mapping
+-------
+
+A mapping template is used to produce the final associative array of
+name/value pairs. The template is a JSON Object. The value in a
+name/value pair can be a constant or a variable. If the template value
+is a variable the value of the variable is retrieved from the set of
+local variables bound to the rule thereby replacing it in the final
+result.
+
+For example given this mapping template and rule variables in JSON:
+
+template:
+
+::
+
+ {
+ "organization": "BigCorp.com",
+ "user: "$subject",
+ "roles": "$roles"
+ }
+
+local variables:
+
+::
+
+ {
+ "subject": "Sally",
+ "roles": ["user", "admin"]
+ }
+
+The final mapped results would be:
+
+::
+
+ {
+ "organization": "BigCorp.com",
+ "user: "Sally",
+ "roles": ["user", "admin"]
+ }
+
+
+Each rule must bind a mapping template to the rule. The mapping
+template may either be defined directly in the rule via the
+``mapping`` key or referenced by name via the ``mapping_name`` key.
+
+If the ``mapping_name`` is specified the mapping is looked up in a
+table of mapping templates bound to the Rule Processor. Using the name
+of a mapping template is useful when many rules generate the exact
+same template values.
+
+If both ``mapping`` and ``mapping_name`` are defined the locally bound
+``mapping`` takes precedence.
+
+Syntax
+------
+
+The logic for a rule consists of a sequence of statements grouped in
+blocks. A statement is similar to a function call in a programming
+language.
+
+A statement is a list of values the first of which is a verb which
+defines the operation the statement will perform. Think of the
+`verbs`_ as function names or operators. Following the verb are
+parameters which may be constants or variables. If the statement
+assigns a value to a variable left hand side of the assignment (lhs)
+is always the first parameter following the verb in the list of
+statement values.
+
+For example this statement in JSON:
+
+::
+
+ ["split", "$groups", "$assertion[Groups]", ":"]
+
+will assign an array to the variable ``$groups``. It looks up the
+string named ``Groups`` in the assertion which is a colon (:)
+separated list of group names splitting that string on the colon
+character.
+
+Statements **must** be grouped together in blocks. Therefore a rule is
+a sequence of blocks and block is a sequence of statements. The
+purpose of blocks is allow for crude flow of control logic. For
+example this JSON rule has 4 blocks.
+
+::
+
+ [
+ [
+ ["set", $user, ""],
+ ["set", $roles, []]
+ ],
+ [
+ ["in", "UserName", "$assertion"],
+ ["continue", "if_not_success"],
+ ["set", "$user", "$assertion[UserName"],
+ ],
+ [
+ ["in", "subject", "$assertion"],
+ ["continue", "if_not_success"],
+ ["set", "$user", "$assertion[subject]"],
+ ],
+ [
+ ["length", "$temp", "$user"],
+ ["compare", "$temp", ">", 0],
+ ["exit", "rule_fails", "if_not_success"]
+ ["append" "$roles", "unprivileged"]
+ ]
+ ]
+
+The rule will succeed if either ``UserName`` or ``subject`` is defined
+in the assertion and if so the local variable ``$user`` will be set to
+the value found in the assertion and the "unprivileged" role will be
+appended to the roles array.
+
+The first block performs initialization. The second block tests to see
+if the assertion has the key ``UserName`` if not execution continues
+at the next block otherwise the value of UserName in the assertion is
+copied into the variable ``$user``. The third block performs a similar
+operation looking for a ``subject`` in the assertion. The fourth block
+checks to see if the ``$user`` variable is empty, if it is empty the
+rule fails because it didn't find either a ``UserName`` nor a
+``subject`` in the assertion. If ``$user`` is not empty the
+"unprivileged" role is appended and the rule succeeds.
+
+Data Types
+----------
+
+There are 7 supported types which equate to the types available in
+JSON. At the time of this writing there are 2 implementations of this
+Mapping specification, one in Python and one in Java. This table
+illustrates how each data type is represented. The first two columns
+are definitions from an abstract specification. The JSON column
+enumerates the data type JSON supports. The Mapping column lists the
+7 enumeration names used by the Mapping implemenation in each
+language. The following columns list the concrete data type used in
+that language.
+
++-----------+------------+--------------------+---------------------+
+| JSON | Mapping | Python | Java |
++===========+============+====================+=====================+
+| object | MAP | dict | Map<String, Object> |
++-----------+------------+--------------------+---------------------+
+| array | ARRAY | list | List<Object> |
++-----------+------------+--------------------+---------------------+
+| string | STRING | unicode (Python 2) | String |
+| | +--------------------+ |
+| | | str (Python 3) | |
++-----------+------------+--------------------+---------------------+
+| | INTEGER | int | Long |
+| number +------------+--------------------+---------------------+
+| | REAL | float | Double |
++-----------+------------+--------------------+---------------------+
+| true | | | |
++-----------+ BOOLEAN | bool | Boolean |
+| false | | | |
++-----------+------------+--------------------+---------------------+
+| null | NULL | None | null |
++-----------+------------+--------------------+---------------------+
+
+
+Rule Debugging and Documentation
+--------------------------------
+
+If the rule processor reports an error or if you're debugging your
+rules by enabling DEBUG log tracing then you must be able to correlate
+the reported statement to where it appears in your rule JSON source. A
+message will always identify a statement by the rule number, block
+number within that rule and the statement number within that
+block. However once your rules become moderately complex it will
+become increasingly difficult to identify a statement by counting
+rules, blocks and statements.
+
+A better approach is to tag rules and blocks with a name or other
+identifying string. You can set the `Reserved Variables`_
+``rule_name`` and ``block_name`` to a string of your choice. These
+strings will be reported in all messages along with the rule, block
+and statement numbers.
+
+JSON does not permit comments, as such you cannot include explanatory
+comments next to your rules, blocks and statements in the JSON
+source. The ``rule_name`` and ``block_name`` can serve a similar
+purpose. By putting assignments to these variables as the first
+statement in a block you'll both document your rules and be able to
+identify specific statements in log messages.
+
+During rule execution the ``rule_name`` and ``block_name`` are
+initialized to the empty string at the beginning of each rule and
+block respectively.
+
+The above example is augmented to include this information. The rule
+name is set in the first statement in the first block.
+
+::
+
+ [
+ [
+ ["set", "$rule_name", "Must have UserName or subject"],
+ ["set", "block_name", "Initialization"],
+ ["set", $user, ""],
+ ["set", $roles, []]
+ ],
+ [
+ ["set", "block_name", "Test for UserName, set $user"],
+ ["in", "UserName", "$assertion"],
+ ["continue", "if_not_success"],
+ ["set", "$user", "$assertion[UserName"],
+ ],
+ [
+ ["set", "block_name", "Test for subject, set $user"],
+ ["in", "subject", "$assertion"],
+ ["continue", "if_not_success"],
+ ["set", "$user", "$assertion[subject]"],
+ ],
+ [
+ ["set", "block_name", "If not $user fail, else append unprivileged to roles"],
+ ["length", "$temp", "$user"],
+ ["compare", "$temp", ">", 0],
+ ["exit", "rule_fails", "if_not_success"]
+ ["append" "$roles", "unprivileged"]
+ ]
+ ]
+
+
+
+
+Variables
+---------
+
+
+Variables always begin with a dollar sign ($) and are followed by an
+identifier which is any alpha character followed by zero or more
+alphanumeric or underscore characters. The variable may optionally be
+delimited with braces ({}) to separate the variable from surrounding
+text. Three types of variables are supported:
+
+* scalar
+* array (indexed by zero based integer)
+* associative array (indexed by string)
+
+Both arrays and associative arrays use square brackets ([]) to specify
+a member of the array. Examples of variable usage:
+
+::
+
+ $name
+ ${name}
+ $groups[0]
+ ${groups[0]}
+ $properties[key]
+ ${properties[key]}
+
+An array or an associative array may be referenced by it's base name
+(omitting the indexing brackets). For example the associative array
+array named "properties" is referenced using it's base name
+``$properties`` but if you want to access a member of the "properties"
+associative array named "duration" you would do this ``$properties[duration]``
+
+This is not a general purpose language with full expression
+syntax. Only one level of variable lookup is supported. Therefore
+compound references like this
+
+::
+
+ $properties[$groups[2]]
+
+will not work.
+
+
+Escaping
+^^^^^^^^
+
+If you need to include a dollar sign in a string (where it is
+immediately followed by either an identifier or a brace and identifier)
+and do not want to have it be interpreted as representing a variable
+you must escape the dollar sign with a backslash, for example
+"$amount" is interpreted as the variable ``amount`` but "\\$amount"
+is interpreted as the string "$amount" .
+
+
+Reserved Variables
+------------------
+
+A rule has the following reserved variables:
+
+assertion
+ The current assertion values from the federated IdP. It is a
+ dictionary of key/value pairs.
+
+regexp_array
+ The regular expression groups from the last successful regexp match
+ indexed by number. Group 0 is the entire match. Groups 1..n are
+ the corresponding parenthesized group counting from the left. For
+ example regexp_array[1] is the first group.
+
+regexp_map
+ The regular expression groups from the last successful regexp match
+ indexed by group name.
+
+rule_number
+ The zero based index of the currently executing rule.
+
+rule_name
+ The name of the currently executing rule. If the rule name has not
+ been set it will be the empty string.
+
+block_number
+ The zero based index of the currently executing block within the
+ currently executing rule.
+
+block_name
+ The name of the currently executing block. If the block name has not
+ been set it will be the empty string.
+
+
+statement_number
+ The zero based index of the currently executing statement within the
+ currently executing block.
+
+
+Examples
+========
+
+Split a fully qualified username into user and realm components
+---------------------------------------------------------------
+
+It's common for some IdP's to return a fully qualified username
+(e.g. principal or subject). The fully qualified username is the
+concatenation of the user name, separator and realm name. A common
+separator is the @ character. In this example lets say the fully
+qualified username is ``bob@example.com`` and you want to return the
+user and realm as independent values in your mapped result. The
+username appears in the assertion as the value ``Principal``.
+
+Our strategy will be to use a regular expression identify the user and
+realm components and then assign them to local variables which will
+then populate the mapped result.
+
+The mapping in JSON is:
+
+::
+
+ {
+ "user": "$username",
+ "realm": "$domain"
+ }
+
+The assertion in JSON is:
+
+::
+
+ {
+ "Principal": "bob@example.com"
+ }
+
+Our rule is:
+
+::
+
+ [
+ [
+ ["in", "Principal", "assertion"],
+ ["exit", "rule_fails", "if_not_success"],
+ ["regexp", "$assertion[Principal]", (?P<username>\\w+)@(?P<domain>.+)"],
+ ["set", "$username", "$regexp_map[username]"],
+ ["set", "$domain", "$regexp_map[domain]"],
+ ["exit, "rule_succeeds", "always"]
+ ]
+ ]
+
+Rule explanation:
+
+Block 0:
+
+0. Test if the assertion contains a Principal value.
+1. Abort the rule if the assertion does not contain a Principal
+ value.
+2. Apply a regular expression the the Principal value. Use named
+ groupings for the username and domain components for clarity.
+3. Assign the regexp group username to the $username local variable.
+4. Assign the regexp group domain to the $domain local variable.
+5. Exit the rule, apply the mapping, return the mapped values. Note, an
+ explicit `exit`_ is not required if there are no further statements
+ in the rule, as is the case here.
+
+The mapped result in JSON is:
+
+::
+
+ {
+ "user": "bob",
+ "realm": "example.com"
+ }
+
+Build a set of roles based on group membership
+----------------------------------------------
+
+Often one wants to grant roles to a user based on their membership in
+certain groups. In this example let's say the assertion contains a
+``Groups`` value which is a colon separated list of group names. Our
+strategy is to split the ``Groups`` assertion value into an array of
+group names. Then we'll test if a specific group is in the groups
+array, if it is we'll add a role. Finally if no roles have been mapped
+we fail. Users in the group "student" will get the role "unprivileged"
+and users in the group "helpdesk" will get the role "admin".
+
+The mapping in JSON is:
+
+::
+
+ {
+ "roles": "$roles",
+ }
+
+The assertion in JSON is:
+
+::
+
+ {
+ "Groups": "student:helpdesk"
+ }
+
+Our rule is:
+
+::
+
+ [
+ [
+ ["in", "Groups", "assertion"],
+ ["exit", "rule_fails", "if_not_success"],
+ ["set", "$roles", []],
+ ["split", "$groups", "$assertion[Groups]", ":"],
+ ],
+ [
+ ["in", "student", "$groups"],
+ ["continue", "if_not_success"],
+ ["append", "$roles", "unprivileged"]
+ ],
+ [
+ ["in", "helpdesk", "$groups"],
+ ["continue", "if_not_success"],
+ ["append", "$roles", "admin"]
+ ],
+ [
+ ["unique", "$roles", "$roles"],
+ ["length", "$temp", "roles"],
+ ["compare", $temp", ">", 0],
+ ["exit", "rule_fails", "if_not_success"]
+ ]
+
+ ]
+
+Rule explanation:
+
+Block 0
+
+0. Test if the assertion contains a Groups value.
+1. Abort the rule if the assertion does not contain a Groups
+ value.
+2. Initialize the $roles variable to an empty array.
+3. Split the colon separated list of group names into an array of
+ individual group names
+
+Block 1
+
+0. Test if "student" is in the $groups array
+1. Exit the block if it's not.
+2. Append "unprivileged" to the $roles array
+
+Block 2
+
+0. Test if "helpdesk" is in the $groups array
+1. Exit the block if it's not.
+2. Append "admin" to the $roles array
+
+Block 3
+
+0. Strip any duplicate roles that might have been appended to the
+ $roles array to assure each role is unique.
+1. Count how many members are in the $roles array, assign the
+ length to the $temp variable.
+2. Test to see if the $roles array had any members.
+3. Fail if no roles had been assigned.
+
+The mapped result in JSON is:
+
+::
+
+ {
+ "roles": ["unprivileged", "admin"]
+ }
+
+However, suppose whatever is receiving your mapped results is not
+expecting an array of roles. Instead it expects a comma separated list
+in a string. To accomplish this add the following statement as the
+last one in the final block:
+
+::
+
+ ["join", "$roles", "$roles", ","]
+
+Then the mapped result will be:
+
+::
+
+ {
+ "roles": "unprivileged,admin"]
+ }
+
+
+
+
+White list certain users and grant them specific roles
+------------------------------------------------------
+
+Suppose you have certain users you always want to unconditionally
+accept and authorize with specific roles. For example if the user is
+"head_of_IT" then assign her the "user" and "admin" roles. Otherwise
+keep processing. The list of white listed users is hard-coded into the
+rule.
+
+The mapping in JSON is:
+
+::
+
+ {
+ "user": $user,
+ "roles": "$roles",
+ }
+
+The assertion in JSON is:
+
+::
+
+ {
+ "UserName": "head_of_IT"
+ }
+
+Our rule in JSON is:
+
+::
+
+ [
+ [
+ ["in", "UserName", "assertion"],
+ ["exit", "rule_fails", "if_not_success"],
+ ["in", "$assertion[UserName]", ["head_of_IT", "head_of_Engineering"]],
+ ["continue", "if_not_success"],
+ ["set", "$user", "$assertion[UserName"]
+ ["set", "$roles", ["user", "admin"]],
+ ["exit", "rule_succeeds", "always"]
+ ],
+ [
+ ...
+ ]
+ ]
+
+Rule explanation:
+
+Block 0
+
+0. Test if the assertion contains a UserName value.
+1. Abort the rule if the assertion does not contain a UserName
+ value.
+2. Test if the user is in the hardcoded list of white listed users.
+3. If the user isn't in the white listed array then exit the block and
+ continue execution at the next block.
+4. Set the $user local variable to $assertion[UserName]
+5. Set the $roles local variable to the hardcoded array containing
+ "user" and "admin"
+6. We're done, unconditionally exit and return the mapped result.
+
+Block 1
+
+0. Further processing
+
+The mapped result in JSON is:
+
+::
+
+ {
+ "user": "head_of_IT",
+ "roles": ["users", "admin"]
+ }
+
+
+Black list certain users
+------------------------
+
+Suppose you have certain users you always want to unconditionally
+deny access to by placing them in a black list. In this example the
+user "BlackHat" will try to gain access. The black list includes the
+users "BlackHat" and "Spook".
+
+The mapping in JSON is:
+
+::
+
+ {
+ "user": $user,
+ "roles": "$roles",
+ }
+
+The assertion in JSON is:
+
+::
+
+ {
+ "UserName": "BlackHat"
+ }
+
+Our rule in JSON is:
+
+::
+
+ [
+ [
+ ["in", "UserName", "assertion"],
+ ["exit", "rule_fails", "if_not_success"],
+ ["in", "$assertion[UserName]", ["BlackHat", "Spook"]],
+ ["exit", "rule_fails", "if_success"]
+ ],
+ [
+ ...
+ ]
+ ]
+
+Rule explanation:
+
+Block 0
+
+0. Test if the assertion contains a UserName value.
+1. Abort the rule if the assertion does not contain a UserName
+ value.
+2. Test if the user is in the hard-coded list of black listed users.
+3. If the test succeeds then immediately abort and return failure.
+
+Block 1
+
+0. Further processing
+
+The mapped result in JSON is:
+
+::
+
+ Null
+
+Format Strings and/or Concatenate Strings
+-----------------------------------------
+
+You can replace variables in a format string using the `interpolate`_
+verb. String concatenation is trivially placing two variables adjacent
+to one another in a format string. Suppose you want to form an email
+address from the username and domain in an assertion.
+
+The mapping in JSON is:
+
+::
+
+ {
+ "email": $email,
+ }
+
+The assertion in JSON is:
+
+::
+
+ {
+ "UserName": "Bob",
+ "Domain": "example.com"
+ }
+
+Our rule in JSON is:
+
+::
+
+ [
+ [
+ ["interpolate", "$email", "$assertion[UserName]@$assertion[Domain]"],
+ ]
+ ]
+
+Rule explanation:
+
+Block 0
+
+0. Replace the variable $assertion[UserName] with it's value and
+ replace the variable $assertion[Domain] with it's value.
+
+The mapped result in JSON is:
+
+::
+
+ {
+ "email": "Bob@example.com",
+ }
+
+
+Note, sometimes it's necessary to utilize braces to separate variables
+from surrounding text by using the brace notation. This can also make
+the format string more readable. Using braces to delimit variables the
+above would be:
+
+::
+
+ [
+ [
+ ["interpolate", "$email", "${assertion[UserName]}@${assertion[Domain]}"],
+ ]
+ ]
+
+
+
+Make associative array lookups case insensitive
+-----------------------------------------------
+
+Many systems treat field names as case insensitive. By default
+associative array indexing is case sensitive. The solution is to lower
+case all the keys in an associative array and then only use lower case
+indices. Suppose you want the assertion associative array to be case
+insensitive.
+
+The mapping in JSON is:
+
+::
+
+ {
+ "user": $user,
+ }
+
+The assertion in JSON is:
+
+::
+
+ {
+ "UserName": "Bob"
+ }
+
+Our rule in JSON is:
+
+::
+
+ [
+ [
+ ["lower", "$assertion", "$assertion"],
+ ["in", "username", "assertion"],
+ ["exit", "rule_fails", "if_not_success"],
+ ["set", "$user", "$assertion[username"]
+ ]
+ ]
+
+Rule explanation:
+
+Block 0
+
+0. Lower case all the keys in the assertion associative array.
+1. Test if the assertion contains a username value.
+2. Abort the rule if the assertion does not contain a username
+ value.
+3. Assign the username value in the assertion to $user
+
+The mapped result in JSON is:
+
+::
+
+ {
+ "user": "Bob",
+ }
+
+
+Verbs
+=====
+
+The following verbs are supported:
+
+* `set`_
+* `length`_
+* `interpolate`_
+* `append`_
+* `unique`_
+* `regexp`_
+* `regexp_replace`_
+* `split`_
+* `join`_
+* `lower`_
+* `upper`_
+* `compare`_
+* `in`_
+* `not_in`_
+* `exit`_
+* `continue`_
+
+Some verbs have a side effects. A verb may set a boolean success/fail
+result which may then be tested with a subsequent verb. For example
+the ``fail`` verb can be used to indicate the rule fails if a prior
+result is either ``success`` or ``not_success``. The ``regexp`` verb
+which performs a regular expression search on a string stores the
+regular expression sub-matches as a side effect in the variables
+``$regexp_array`` and ``$regexp_map``.
+
+
+Verb Definitions
+================
+
+set
+---
+
+``set $variable value``
+
+$variable
+ The variable being assigned (i.e. lhs)
+
+value
+ The value to assign to the variable (i.e. rhs). The value may be
+ another variable or a constant.
+
+**set** assigns a value to a variable, in other words it's an
+assignment statement.
+
+Examples:
+^^^^^^^^^
+
+Initialize a variable to an empty array.
+
+::
+
+ ["set", "$groups", []]
+
+Initialize a variable to an empty associative array.
+
+::
+
+ ["set", "$groups", {}]
+
+Assign a string.
+
+::
+
+ ["set", "$version", "1.2.3"]
+
+Copy the ``UserName`` value from the assertion to a temporary variable.
+
+::
+
+ ["set", "$temp", "$assertion[UserName]"],
+
+
+Get the 2nd item in an array (array indexing is zero based)
+
+::
+
+ ["set", "$group", "$groups[1]"]
+
+
+Set the associative array entry "IdP" to "kdc.example.com".
+
+::
+
+ ["set", "$metadata[IdP]", "kdc.example.com""]
+
+--------------------------------------------------------------------------------
+
+length
+------
+
+``length $variable value``
+
+$variable
+ The variable which receives the length value
+
+value
+ The value whose length is to be determined. May be one of array,
+ associative array, or string.
+
+**length** computes the number of items in the value. How this is done
+depends upon the type of value:
+
+array
+ The length is the number of items in the array.
+
+associative array
+ The length is the number of key/value pairs in the associative
+ array.
+
+string
+ The length is the number of *characters* (not octets) in the
+ string.
+
+Examples:
+^^^^^^^^^
+
+Count how many items are in the ``$groups`` array and assign that
+value to the ``$groups_length`` variable.
+
+::
+
+ ["length", "$groups_length", "$groups"]
+
+Count how many key/value pairs are in the ``$assertion`` associative
+array and assign that value to the ``$num_assertion_values`` variable.
+
+::
+
+ ["length", "$num_assertion_values", "$assertion"]
+
+Count how many characters are in the assertion's UserName and assign
+the value to ``$username_length``.
+
+::
+
+ ["length", "$user_name_length", "$assertion[UserName]"]
+
+
+--------------------------------------------------------------------------------
+
+interpolate
+-----------
+
+``interpolate $variable string``
+
+$variable
+ This variable is assigned the result of the interpolation.
+
+string
+ A string containing references to variables which will be replaced
+ in the string.
+
+**interpolate** replaces each occurrence of a variable in a string with
+it's value. The result is assigned to $variable.
+
+Examples:
+^^^^^^^^^
+
+Form an email address given the username and domain. If the username
+is "jane" and the domain is "example.com" then $email will be
+"jane@example.com"
+
+::
+
+ ["interpolate", "$email", "${username}@${domain}"]
+
+
+--------------------------------------------------------------------------------
+
+
+append
+------
+
+``append $variable value``
+
+$variable
+ This variable **must** be an array. It is modified in place by
+ appending ``value`` to the end of the array.
+
+value
+ The value to append to the end of the array.
+
+**append** adds a value to end of an array.
+
+Examples:
+^^^^^^^^^
+
+Append the role "qa_test" to the roles list.
+
+::
+
+ ["append", "$roles", "qa_test"]
+
+
+--------------------------------------------------------------------------------
+
+
+unique
+------
+
+``unique $variable value``
+
+$variable
+ This variable is assigned the unique values in the ``value``
+ array.
+
+value
+ An array of values. **must** be an array.
+
+**unique** builds an array of unique values in ``value`` by stripping
+out duplicates and assigns the array of unique values to
+``$variable``. The order of items in the ``value`` array are
+preserved.
+
+Examples:
+^^^^^^^^^
+
+$one_of_a_kind will be assigned ["a", "b"]
+
+::
+
+ ["unique", "$one_of_a_kind", ["a", "b", "a"]]
+
+
+--------------------------------------------------------------------------------
+
+regexp
+------
+
+``regexp string pattern``
+
+string
+ The string the regular expression pattern is applied to.
+
+pattern
+ The regular expression pattern.
+
+**regexp** performs a regular expression match against ``string``. The
+regular expression pattern syntax is defined by the regular expression
+implementation of the language this API is written in.
+
+Pattern groups are a convenient way to select sub-matches. Pattern
+groups may accessed by either group number or group name. After a
+successful regular expression match the groups are stored in the
+special variables ``$regexp_array`` and
+``$regexp_map``.
+
+``$regexp_array`` is used to access the groups by
+numerical index. Groups are numbered by counting the left parenthesis
+group delimiter starting at 1. Group 0 is the entire
+match. ``$regexp_array`` is valid irregardless of whether you used
+named groups or not.
+
+``$regexp_map`` is used to access the groups by
+name. ``$regexp_map`` is only valid if you used named groups in the
+pattern.
+
+Examples:
+^^^^^^^^^
+
+Many user names are of the form "user@domain", to split the username
+from the domain and to be able to work with those values independently
+use a regular expression and then assign the results to a variable. In
+this example there are two regular expression groups, the first group
+is the username and the second group is the domain. In the first
+example we use named groups and then access the match information in
+the special variable ``$regexp_map`` via the name of the group.
+
+::
+
+ ["regexp", "$assertion[UserName]", "(?P<username>\\w+)@(?P<domain>.+)"],
+ ["continue", "if_not_success"],
+ ["set", "$username", "$regexp_map[username]"],
+ ["set", "$domain", "$regexp_map[domain]"],
+
+
+This is exactly equivalent but uses numbered groups instead of named
+groups. In this instance the group matches are stored in the special
+variable ``$regexp_array`` and accessed by numerical index.
+
+::
+
+ ["regexp", "$assertion[UserName]", "(\\w+)@(.+)"],
+ ["continue", "if_not_success"],
+ ["set", "$username", "$regexp_array[1]"],
+ ["set", "$domain", "$regexp_array[2]"],
+
+
+
+--------------------------------------------------------------------------------
+
+regexp_replace
+--------------
+
+``regexp_replace $variable string pattern replacement``
+
+$variable
+ The variable which receives result of the replacement.
+
+string
+ The string to perform the replacement on.
+
+pattern
+ The regular expression pattern.
+
+replacement
+ The replacement specification.
+
+**regexp_replace** replaces each occurrence of ``pattern`` in
+``$string`` with ``replacement``. See `regexp`_ for details of using
+regular expressions.
+
+Examples:
+^^^^^^^^^
+
+Convert hyphens in a name to underscores.
+
+::
+
+ ["regexp_replace", "$name", "$name", "-", "_"]
+
+
+--------------------------------------------------------------------------------
+
+split
+-----
+
+``split $variable string pattern``
+
+$variable
+ This variable is assigned an array containing the split items.
+
+string
+ The string to split into separate items.
+
+pattern
+ The regular expression pattern used to split the string.
+
+**split** splits ``string`` into separate pieces and assigns the
+result to ``$variable`` as an array of pieces. The split occurs
+wherever the regular expression ``pattern`` occurs in ``string``. See
+`regexp`_ for details of using regular expressions.
+
+Examples:
+^^^^^^^^^
+
+Split a list of groups separated by a colon (:) into an array of
+individual group names. If $assertion[Groups] contained the string
+"user:admin" then $group_list will set to ["user", "admin"].
+
+::
+
+ ["split", "$group_list", "$assertion[Groups]", ":"]
+
+
+
+--------------------------------------------------------------------------------
+
+join
+----
+
+``join $variable array join_string``
+
+$variable
+ This variable is assigned the string result of the join operation.
+
+array
+ An array of string items to be joined together with
+ ``$join_string``.
+
+join_string
+ The string inserted between each element in ``array``.
+
+**join** accepts an array of strings and produces a single string
+where each element in the array is separated by ``join_string``.
+
+Examples:
+^^^^^^^^^
+
+Convert a list of group names into a single string where each group
+name is separated by a colon (:). If the array ``$group_list`` is
+["user", "admin"] and the ``join_string`` is ":" then the
+``$group_string`` variable will be set to "user:admin".
+
+::
+
+ ["join", "$group_string", "$groups", ":"]
+
+
+--------------------------------------------------------------------------------
+
+lower
+-----
+
+``lower $variable value``
+
+$variable
+ This variable is assigned the result of the lower operation.
+
+value
+ The value to lower case, may be either a string, array, or
+ associative array.
+
+**lower** lower cases the input value. The input value may be one of
+the following types:
+
+string
+ The string is lower cased.
+
+array
+ Each member of the array must be a string, the result is an array
+ with the items replaced by their lower case value.
+
+associative array
+ Each key in the associative array is lower cased. The values
+ associated with the key are **not** modified.
+
+Examples:
+^^^^^^^^^
+
+Lookup ``UserName`` in the assertion and set the variable
+``$username`` to it's lower case value.
+
+::
+
+ ["lower", "$username", "$assertion[UserName]"],
+
+Set each member of the ``$groups`` array to it's lower case value. If
+``$groups`` was ["User", "Admin"] then ``$groups`` will become
+["user", "admin"].
+
+::
+
+ ["lower", "$groups", "$groups"],
+
+To enable case insensitive lookup's in an associative array lower case
+each key in the associative array. If ``$assertion`` was {"UserName":
+"JoeUser"} then ``$assertion`` will become {"username": "JoeUser"}
+
+::
+
+ ["lower", "$assertion", $assertion"]
+
+--------------------------------------------------------------------------------
+
+upper
+-----
+
+``upper $variable value``
+
+$variable
+ This variable is assigned the result of the upper operation.
+
+value
+ The value to upper case, may be either a string, array, or
+ associative array.
+
+**upper** is exactly analogous to `lower`_ except the values are upper
+cased, see `lower`_ for details.
+
+
+--------------------------------------------------------------------------------
+
+in
+--
+
+``in member collection``
+
+member
+ The value whose membership is being tested.
+
+collection
+ A collection of members. May be string, array or associative array.
+
+**in** tests to see if ``member`` is a member of ``collection``. The
+membership test depends on the type of collection, the following are
+supported:
+
+array
+ If any item in the array is equal to ``member`` then the result is
+ success.
+
+associative array
+ If the associative array contains a key equal to ``member`` then
+ the result is success.
+
+string
+ If the string contains a sub-string equal to ``member`` then the
+ result is success.
+
+Examples:
+^^^^^^^^^
+
+Test to see if the assertion contains a UserName value.
+
+::
+
+ ["in", "UserName", "$assertion"]
+ ["continue", "if_not_success"]
+
+Test to see if a group is one of "user" or "admin".
+
+::
+
+ ["in", "$group", ["user", "admin"]]
+ ["continue", "if_not_success"]
+
+Test to see if the sub-string "BigCorp" is in
+the assertion's ``Provider`` value.
+
+::
+
+ ["in", "BigCorp", "$assertion[Provider]"]
+ ["continue", "if_not_success"]
+
+
+--------------------------------------------------------------------------------
+
+not_in
+------
+
+``in member collection``
+
+member
+ The value whose membership is being tested.
+
+collection
+ A collection of members. May be string, array or associative array.
+
+**not_in** is exactly analogous to `in`_ except the sense of the test
+is reversed. See `in`_ for details.
+
+--------------------------------------------------------------------------------
+
+compare
+-------
+
+``compare left operator right``
+
+left
+ The left hand value of the binary operator.
+
+operator
+ The binary operator used for comparing left to right.
+
+right
+ The right hand value of the binary operator.
+
+
+**compare** compares the left value to the right value according the
+operator and sets success if the comparison evaluates to True. The
+following relational operators are supported.
+
++----------+-----------------------+
+| Operator | Description |
++==========+=======================+
+| == | equal |
++----------+-----------------------+
+| != | not equal |
++----------+-----------------------+
+| < | less than |
++----------+-----------------------+
+| <= | less than or equal |
++----------+-----------------------+
+| > | greater than |
++----------+-----------------------+
+| >= | greater than or equal |
++----------+-----------------------+
+
+
+The left and right hand sides of the comparison operator *must* be
+the same type, no type conversions are performed. Not all combinations
+of operator and type are supported. The table below illustrates the
+supported combinations. Essentially you can test for equality or
+inequality on any type. But only strings and numbers support the
+magnitude relational operators.
+
+
++----------+--------+---------+------+---------+-----+------+------+
+| Operator | STRING | INTEGER | REAL | BOOLEAN | MAP | LIST | NULL |
++==========+========+=========+======+=========+=====+======+======+
+| == | X | X | X | X | X | X | X |
++----------+--------+---------+------+---------+-----+------+------+
+| != | X | X | X | X | X | X | X |
++----------+--------+---------+------+---------+-----+------+------+
+| < | X | X | X | | | | |
++----------+--------+---------+------+---------+-----+------+------+
+| <= | X | X | X | | | | |
++----------+--------+---------+------+---------+-----+------+------+
+| > | X | X | X | | | | |
++----------+--------+---------+------+---------+-----+------+------+
+| >= | X | X | X | | | | |
++----------+--------+---------+------+---------+-----+------+------+
+
+
+Examples:
+^^^^^^^^^
+
+Test to see if the ``$groups`` array has at least 2 members
+
+::
+
+ ["length", "$group_length", "$groups"],
+ ["compare", "$group_length", ">=", 2]
+
+
+--------------------------------------------------------------------------------
+
+exit
+----
+
+``exit status criteria``
+
+status
+ The result for the rule.
+
+criteria
+ The criteria upon which will cause the rule will be immediately
+ exited with a failed status.
+
+**exit** causes the rule being executed to immediately exit and a rule
+result if the specified criteria is met. Statement verbs such as `in`_
+or `compare`_ set the result status which may be tested with the
+``success`` and ``not_success`` criteria.
+
+The exit ``status`` may be one of:
+
+rule_fails
+ The rule has failed and no mapping will occur.
+
+rule_succeeds
+ The rule succeeded and the mapping will be applied.
+
+The ``criteria`` may be one of:
+
+if_success
+ If current result status is success then exit with ``status``.
+
+if_not_success
+ If current result status is not success then exit with ``status``.
+
+always
+ Unconditionally exit with ``status``.
+
+never
+ Effectively a no-op. Useful for debugging.
+
+Examples:
+^^^^^^^^^
+
+The rule requires ``UserName`` to be in the assertion.
+
+::
+
+ ["in", "UserName", "$assertion"]
+ ["exit", "rule_fails", "if_not_success"]
+
+--------------------------------------------------------------------------------
+
+
+continue
+--------
+
+``continue criteria``
+
+criteria
+ The criteria which causes the remainder of the *block* to be
+ skipped.
+
+**continue** is used to control execution for statement blocks. It
+mirrors in a crude way the `if` expression in a procedural
+language. ``continue`` does *not* affect the success or failure of a
+rule, rather it controls whether subsequent statements in a block are
+executed or not. Control continues at the next statement block.
+
+Statement verbs such as `in`_ or `compare`_ set the result status
+which may be tested with the ``success`` and ``not_success`` criteria.
+
+The criteria may be one of:
+
+if_success
+ If current result status is success then exit the statement
+ block and continue execution at the next statement block.
+
+if_not_success
+ If current result status is not success then exit the statement
+ block and continue execution at the next statement block.
+
+always
+ Immediately exit the statement block and continue execution at the
+ next statement block.
+
+never
+ Effectively a no-op. Useful for debugging. Execution continues at
+ the next statement.
+
+Examples:
+^^^^^^^^^
+
+The following pseudo code:
+
+::
+
+ roles = [];
+ if ("Groups" in assertion) {
+ groups = assertion["Groups"].split(":");
+ if ("qa_test" in groups) {
+ roles.append("tester");
+ }
+ }
+
+could be implemented this way:
+
+::
+
+ [
+ ["set", "$roles", []],
+ ["in", "Groups", "$assertion"],
+ ["continue", "if_not_success"],
+ ["split" "$groups", $assertion[Groups]", ":"],
+ ["in", "qa_test", "$groups"],
+ ["continue", "if_not_success"],
+ ["append", "$roles", "tester"]
+ ]
diff --git a/java/src/main/java/com/redhat/IdPMapping/IdpJson.java b/java/src/main/java/com/redhat/IdPMapping/IdpJson.java
new file mode 100644
index 0000000..7b95ea9
--- /dev/null
+++ b/java/src/main/java/com/redhat/IdPMapping/IdpJson.java
@@ -0,0 +1,254 @@
+/*
+ * Copyright (C) 2014 Red Hat
+ * 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.LinkedHashMap;
+import java.util.List;
+import java.util.HashMap;
+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 new Long(parser.getLong());
+ } else {
+ return new Double(parser.getBigDecimal().doubleValue());
+ }
+ }
+ case VALUE_STRING: {
+ return parser.getString();
+ }
+ case VALUE_TRUE: {
+ return new Boolean(true);
+ }
+ case VALUE_FALSE: {
+ return new 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/java/src/main/java/com/redhat/IdPMapping/InvalidRuleException.java b/java/src/main/java/com/redhat/IdPMapping/InvalidRuleException.java
new file mode 100644
index 0000000..61bf0c2
--- /dev/null
+++ b/java/src/main/java/com/redhat/IdPMapping/InvalidRuleException.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2014 Red Hat
+ * 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/java/src/main/java/com/redhat/IdPMapping/InvalidTypeException.java b/java/src/main/java/com/redhat/IdPMapping/InvalidTypeException.java
new file mode 100644
index 0000000..aea0416
--- /dev/null
+++ b/java/src/main/java/com/redhat/IdPMapping/InvalidTypeException.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2014 Red Hat
+ * 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/java/src/main/java/com/redhat/IdPMapping/InvalidValueException.java b/java/src/main/java/com/redhat/IdPMapping/InvalidValueException.java
new file mode 100644
index 0000000..1524b4f
--- /dev/null
+++ b/java/src/main/java/com/redhat/IdPMapping/InvalidValueException.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2014 Red Hat
+ * 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/java/src/main/java/com/redhat/IdPMapping/RuleProcessor.java b/java/src/main/java/com/redhat/IdPMapping/RuleProcessor.java
new file mode 100644
index 0000000..ebf4e0a
--- /dev/null
+++ b/java/src/main/java/com/redhat/IdPMapping/RuleProcessor.java
@@ -0,0 +1,1382 @@
+/*
+ * Copyright (C) 2014 Red Hat
+ * 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.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.HashMap;
+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;
+import org.opendaylight.aaa.idpmapping.IdpJson;
+import org.opendaylight.aaa.idpmapping.Token;
+
+
+
+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 logger = 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)));
+ }
+ 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)));
+ }
+ 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)));
+ }
+ logger.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()));
+ }
+
+ 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()));
+ }
+
+ 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()));
+ }
+
+ 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()));
+ }
+
+ 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);
+ System.out.println(assertionJson);
+ System.out.println(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, new Long(ruleNumber));
+ namespace.put(RULE_NAME, new String(""));
+ 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, new Long(blockNumber));
+ namespace.put(BLOCK_NAME, "");
+
+ result = processBlock(namespace, block);
+ System.out.println();
+ 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, new Long(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 (logger.isDebugEnabled()) {
+ logger.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(new Long(length));
+ this.success = true;
+
+ if (logger.isDebugEnabled()) {
+ logger.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 (logger.isDebugEnabled()) {
+ logger.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 (logger.isDebugEnabled()) {
+ logger.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 (logger.isDebugEnabled()) {
+ logger.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 (logger.isDebugEnabled()) {
+ logger.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 (logger.isDebugEnabled()) {
+ logger.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()));
+ }
+ 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 (logger.isDebugEnabled()) {
+ logger.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()));
+ }
+ 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 (logger.isDebugEnabled()) {
+ logger.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 (logger.isDebugEnabled()) {
+ logger.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 (logger.isDebugEnabled()) {
+ logger.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 (logger.isDebugEnabled()) {
+ logger.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 (logger.isDebugEnabled()) {
+ logger.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 (logger.isDebugEnabled()) {
+ logger.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 (logger.isDebugEnabled()) {
+ logger.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 (logger.isDebugEnabled()) {
+ logger.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/java/src/main/java/com/redhat/IdPMapping/StatementErrorException.java b/java/src/main/java/com/redhat/IdPMapping/StatementErrorException.java
new file mode 100644
index 0000000..4d17f64
--- /dev/null
+++ b/java/src/main/java/com/redhat/IdPMapping/StatementErrorException.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2014 Red Hat
+ * 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/java/src/main/java/com/redhat/IdPMapping/Token.java b/java/src/main/java/com/redhat/IdPMapping/Token.java
new file mode 100644
index 0000000..9b835cb
--- /dev/null
+++ b/java/src/main/java/com/redhat/IdPMapping/Token.java
@@ -0,0 +1,406 @@
+/*
+ * Copyright (C) 2014 Red Hat
+ * 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));
+ }
+ } 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));
+ }
+ } 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));
+ }
+ } 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));
+ }
+ } 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/java/src/main/java/com/redhat/IdPMapping/UndefinedValueException.java b/java/src/main/java/com/redhat/IdPMapping/UndefinedValueException.java
new file mode 100644
index 0000000..d3e429c
--- /dev/null
+++ b/java/src/main/java/com/redhat/IdPMapping/UndefinedValueException.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2014 Red Hat
+ * 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);
+ }
+}
diff --git a/java/src/main/java/com/redhat/app/MappingApp.java b/java/src/main/java/com/redhat/app/MappingApp.java
new file mode 100644
index 0000000..511a823
--- /dev/null
+++ b/java/src/main/java/com/redhat/app/MappingApp.java
@@ -0,0 +1,57 @@
+package com.redhat.app;
+
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.Map;
+
+import org.apache.log4j.BasicConfigurator;
+import org.apache.log4j.Logger;
+
+import com.redhat.IdpMapping.InvalidTypeException;
+import com.redhat.IdpMapping.IdpJson;
+import com.redhat.IdpMapping.RuleProcessor;
+import com.redhat.IdpMapping.InvalidRuleException;
+import com.redhat.IdpMapping.UndefinedValueException;
+
+class MappingApp {
+ private static final Logger log = Logger.getLogger(MappingApp.class);
+
+
+ private static String loadFile(String path) throws IOException {
+ byte[] encoded = Files.readAllBytes(Paths.get(path));
+ return new String(encoded, "UTF-8");
+ }
+
+ public static void main(String[] args) {
+ BasicConfigurator.configure();
+ log.info("Federated Mapping");
+
+ String rulesFilename = args[0];
+ String assertionFilename = "/home/jdennis/src/misc/federation_mapping/assertion-01.json";
+ String assertionJson;
+ Map<String, Object> mapped;
+ IdpJson json = new IdpJson();
+ RuleProcessor rp = null;
+
+ try {
+ rp = new RuleProcessor(Paths.get(rulesFilename), null); // FIXME, mappings parameter
+ assertionJson = loadFile(assertionFilename);
+
+ } catch (IOException e) {
+ log.error(e);
+ return;
+ }
+ try {
+ mapped = rp.process(assertionJson);
+ System.out.println(String.format("mapped=%s", mapped));
+ if (mapped != null) {
+ System.out.println(json.dumpJson(mapped));
+ }
+ } catch (InvalidRuleException | UndefinedValueException | InvalidTypeException e) {
+ // e.printStackTrace();
+ log.error(e);
+ }
+ }
+}
diff --git a/python/idp_mapping.py b/python/idp_mapping.py
new file mode 100755
index 0000000..14971d5
--- /dev/null
+++ b/python/idp_mapping.py
@@ -0,0 +1,1055 @@
+#!/usr/bin/python
+
+
+import copy
+import json
+import logging
+import re
+import six
+
+def _(string):
+ return string
+
+class InvalidRuleError(ValueError):
+ pass
+
+class UndefinedValueError(ValueError):
+ pass
+
+class StatementError(ValueError):
+ pass
+
+class IllegalStateError(RuntimeError):
+ pass
+
+RULE_FAIL = 0
+RULE_SUCCESS = 1
+BLOCK_CONTINUE = 2
+STATEMENT_CONTINUE = 3
+
+rule_result_names = {
+ RULE_FAIL: 'RULE_FAIL',
+ RULE_SUCCESS: 'RULE_SUCCESS',
+ BLOCK_CONTINUE: 'BLOCK_CONTINUE',
+ STATEMENT_CONTINUE: 'STATEMENT_CONTINUE',
+}
+
+def rule_result_name(result):
+ return rule_result_names.get(result, "unknown")
+
+#
+# Reserved variables
+#
+ASSERTION = 'assertion'
+RULE_NUMBER = 'rule_number'
+RULE_NAME = 'rule_name'
+BLOCK_NUMBER = 'block_number'
+BLOCK_NAME = 'block_name'
+STATEMENT_NUMBER = 'statement_number'
+REGEXP_ARRAY_VARIABLE = 'regexp_array'
+REGEXP_MAP_VARIABLE = 'regexp_map'
+
+class Token(object):
+ # 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
+ VARIABLE_PAT = (r'(?<!\\)\$' # non-escaped $ sign
+ r'{?' # optional delimiting brace
+ r'([a-zA-Z][a-zA-Z0-9_]*)' # group 1: variable name
+ r'(\[' # group 2: optional index
+ r'([a-zA-Z0-9_]+)' # group 3: array index
+ r'\])?' # end optional index
+ r'}?' # optional delimiting brace
+ )
+ VARIABLE_RE = re.compile(VARIABLE_PAT)
+
+ # Requires only a variable to be present in the string
+ # but permits leading and trailing whitespace.
+ VARIABLE_ONLY_PAT = r'^\s*%s\s*$' % (VARIABLE_PAT)
+ VARIABLE_ONLY_RE = re.compile(VARIABLE_ONLY_PAT)
+
+ STORAGE_TYPE_UNKNOWN = 0
+ STORAGE_TYPE_CONSTANT = 1
+ STORAGE_TYPE_VARIABLE = 2
+
+ storage_type_names = {
+ STORAGE_TYPE_UNKNOWN: 'UNKNOWN',
+ STORAGE_TYPE_CONSTANT: 'CONSTANT',
+ STORAGE_TYPE_VARIABLE: 'VARIABLE',
+ }
+
+ # ordered by expected occurrence
+ TYPE_STRING = 1
+ TYPE_ARRAY = 2
+ TYPE_MAP = 3
+ TYPE_INTEGER = 4
+ TYPE_BOOLEAN = 5
+ TYPE_NULL = 6
+ TYPE_REAL = 7
+ TYPE_UNKNOWN = 0
+
+ type_names = {
+ TYPE_STRING: 'STRING',
+ TYPE_ARRAY: 'ARRAY',
+ TYPE_MAP: 'MAP',
+ TYPE_INTEGER: 'INTEGER',
+ TYPE_BOOLEAN: 'BOOLEAN',
+ TYPE_NULL: 'NULL',
+ TYPE_REAL: 'REAL',
+ TYPE_UNKNOWN: 'UNKNOWN',
+ }
+
+ def __init__(self, input, namespace):
+ self.log = logging.getLogger(self.__class__.__name__)
+ self.namespace = namespace
+ self.storage_type = self.STORAGE_TYPE_UNKNOWN
+ self.type = self.TYPE_UNKNOWN
+ self.value = None
+ self.name = None
+ self.index = None
+
+ if isinstance(input, basestring):
+ self.parse_variable(input)
+ if self.storage_type == self.STORAGE_TYPE_CONSTANT:
+ self.value = input
+ self.type = self.classify(input)
+ else:
+ self.storage_type = self.STORAGE_TYPE_CONSTANT
+ self.value = input
+ self.type = self.classify(input)
+
+ @classmethod
+ def get_storage_type_name(cls, storage_type_enum):
+ return cls.storage_type_names.get(storage_type_enum)
+
+ @property
+ def storage_type_name(self):
+ return self.get_storage_type_name(self.storage_type)
+
+ @classmethod
+ def get_type_name(cls, type_enum):
+ return cls.type_names.get(type_enum)
+
+ @property
+ def type_name(self):
+ return self.get_type_name(self.type)
+
+ def __str__(self):
+ if self.storage_type == self.STORAGE_TYPE_CONSTANT:
+ return "%s" % (self.value)
+ elif self.storage_type == self.STORAGE_TYPE_VARIABLE:
+ if self.index is None:
+ return "$%s" % (self.name)
+ else:
+ return "$%s[%s]" % (self.name, self.index)
+ else:
+ return "UNKNOWN"
+
+ def parse_variable(self, string):
+ match = Token.VARIABLE_ONLY_RE.search(string)
+ if match:
+ name = match.group(1)
+ index = match.group(3)
+
+ self.storage_type = self.STORAGE_TYPE_VARIABLE
+ self.name = name
+ self.index = index
+ else:
+ self.storage_type = self.STORAGE_TYPE_CONSTANT
+
+ @classmethod
+ def classify(cls, value):
+ token_type = cls.TYPE_UNKNOWN
+ # ordered by expected occurrence
+ if isinstance(value, basestring):
+ token_type = cls.TYPE_STRING
+ elif isinstance(value, list):
+ token_type = cls.TYPE_ARRAY
+ elif isinstance(value, dict):
+ token_type = cls.TYPE_MAP
+ elif isinstance(value, int):
+ token_type = cls.TYPE_INTEGER
+ elif isinstance(value, bool):
+ token_type = cls.TYPE_BOOLEAN
+ elif value is None:
+ token_type = cls.TYPE_NULL
+ elif isinstance(value, float):
+ token_type = cls.TYPE_REAL
+ else:
+ raise TypeError("Type must be string, integer, real, boolean, null, array or map not %s, value=%s" %
+ (value.__class__.__name__, value))
+ return token_type
+
+ def get(self, index=None):
+ if self.storage_type == self.STORAGE_TYPE_CONSTANT:
+ return self.value
+
+ try:
+ base = self.namespace[self.name]
+ except KeyError:
+ raise UndefinedValueError("variable '%s' not defined" % (self.name))
+
+ if index is None:
+ index = self.index
+
+ if index is None: # scalar types
+ value = base
+ else:
+ if isinstance(base, list):
+ idx = None
+ if isinstance(index, int):
+ idx = index
+ elif isinstance(index, basestring):
+ try:
+ idx = int(index)
+ except:
+ raise TypeError("variable '%s' is an array indexed by '%s', however the index cannot be converted to an integer" %
+ (self.name, index))
+ else:
+ raise TypeError("variable '%s' is an array indexed by '%s', however the index must be an integer or string not %s" %
+ (self.name, index,
+ index.__class__.__name__))
+ try:
+ value = base[idx]
+ except IndexError:
+ raise UndefinedValueError("variable '%s' is an array of size %d indexed by '%s', however the index is out of bounds" %
+ (self.name, len(base), idx))
+
+ elif isinstance(base, dict):
+ idx = None
+ if isinstance(index, basestring):
+ idx = index
+ else:
+ raise TypeError("variable '%s' is a map indexed by '%s', however the index must be a string not %s" %
+ (self.name, index, index.__class__.__name__))
+ try:
+ value = base[idx]
+ except KeyError:
+ raise UndefinedValueError("variable '%s' is a map indexed by '%s', however the index does not exist" %
+ (self.name, idx))
+ else:
+ raise TypeError("variable '%s' is indexed by '%s', variable must be an array or map, not %s"
+ (self.name, index, base.__class__.__name__))
+
+ self.type = self.classify(value)
+ return value
+
+ def set(self, value, index=None):
+ if self.storage_type == self.STORAGE_TYPE_CONSTANT:
+ raise TypeError("cannot assign to a constant")
+
+ if index is None:
+ index = self.index
+
+ if index is None: # scalar types
+ self.namespace[self.name] = value
+ else:
+ try:
+ base = self.namespace[self.name]
+ except KeyError:
+ raise UndefinedValueError("variable '%s' not defined" %
+ (self.name))
+
+ if isinstance(base, list):
+ idx = None
+ if isinstance(index, int):
+ idx = index
+ elif isinstance(index, basestring):
+ try:
+ idx = int(index)
+ except:
+ raise TypeError("variable '%s' is an array indexed by '%s', however the index cannot be converted to an integer" %
+ (self.name, index))
+ else:
+ raise TypeError("variable '%s' is an array indexed by '%s', however the index must be an integer or string not %s" %
+ (self.name, index,
+ index.__class__.__name__))
+ try:
+ base[idx] = value
+ except IndexError:
+ raise UndefinedValueError("variable '%s' is an array of size %d indexed by '%s', however the index is out of bounds" %
+ (self.name, len(base), idx))
+
+ elif isinstance(base, dict):
+ idx = None
+ if isinstance(index, basestring):
+ idx = index
+ else:
+ raise TypeError("variable '%s' is a map indexed by '%s', however the index must be a string not %s" %
+ (self.name, index, index.__class__.__name__))
+ base[idx] = value
+ else:
+ raise TypeError("variable '%s' is indexed by '%s', variable must be an array or map, not %s"
+ (self.name, index, base.__class__.__name__))
+
+ def load(self, index=None):
+ self.value = self.get(index)
+ return self.value
+
+class RuleProcessor(object):
+
+ @classmethod
+ def from_stream(cls, stream, mappings=None):
+ log = logging.getLogger(cls.__name__)
+ log.info("loading rules from stream: %s" % (stream.name))
+
+ rules = json.load(stream)
+ rule_processor = RuleProcessor(rules, mappings);
+ return rule_processor
+
+ @classmethod
+ def from_file(cls, filename, mappings=None):
+ log = logging.getLogger(cls.__name__)
+ log.info("loading rules from file: %s" % (filename))
+
+ with open(filename) as stream:
+ rules = json.load(stream)
+ rule_processor = RuleProcessor(rules, mappings);
+ return rule_processor
+
+ @classmethod
+ def from_string(cls, string, mappings=None):
+ log = logging.getLogger(cls.__name__)
+ log.info("loading rules from string")
+
+ rules = json.loads(string)
+ rule_processor = RuleProcessor(rules, mappings);
+ return rule_processor
+
+ def __init__(self, rules, mappings=None):
+ self.log = logging.getLogger(self.__class__.__name__)
+ self.rule_id_format = '<rule [${rule_number}:"${rule_name}"]>'
+ self.statement_id_format = ('<rule [${rule_number}:"${rule_name}"] '
+ 'block [${block_number}:"${block_name}"] '
+ 'statement ${statement_number}>')
+
+ if isinstance(rules, basestring):
+ rules = json.loads(rules)
+ self.rules = rules
+ if mappings is None:
+ self.mappings = {}
+ else:
+ self.mappings = mappings
+
+
+ def rule_id(self, namespace):
+ return self.substitute_variables(self.rule_id_format, namespace)
+
+ def statement_id(self, namespace):
+ return self.substitute_variables(self.statement_id_format, namespace)
+
+ # FIXME, not used
+ def to_string(self, value):
+ Token.classify(value) # raises TypeError if not supported type
+ return json.dumps(value)
+
+ def substitute_variables(self, string, namespace):
+ def get_replacement(match):
+ token = Token(match.group(0), namespace)
+ token.load()
+ if token.type == Token.TYPE_STRING:
+ replacement = token.value
+ else:
+ replacement = six.text_type(token.value)
+ return replacement
+
+ return Token.VARIABLE_RE.sub(get_replacement, string)
+
+ # FIXME, should we be passing namespace? Just used for rule_id
+ def get_mapping(self, namespace, rule):
+ mapping = rule.get('mapping')
+ if mapping is not None:
+ self.log.debug("using mapping local to rule %s mapping=%s",
+ self.rule_id(namespace), mapping)
+ return mapping
+
+ mapping_name = rule.get('mapping_name')
+ if mapping_name is None:
+ raise InvalidRuleError("%s rule does not define mapping nor mapping_name unable to load mapping" %
+ (self.rule_id(namespace)))
+ mapping = self.mappings.get(mapping_name)
+ if mapping is None:
+ raise InvalidRuleError("%s rule specifies mapping_name '%s' but a mapping by that name does not exist, unable to load mapping" %
+ (self.rule_id(namespace)))
+ self.log.debug("using named mapping '%s' from rule %s mapping=%s",
+ mapping_name, self.rule_id(namespace), mapping)
+ return mapping
+
+ def get_verb(self, statement):
+ if len(statement) < 1:
+ raise InvalidRuleError("statement has no verb")
+ try:
+ verb = Token(statement[0], None)
+ except Exception as exc:
+ raise InvalidRuleError("statement first member (i.e. verb) error %s" %
+ (exc))
+
+ if verb.type != Token.TYPE_STRING:
+ raise InvalidRuleError("statement first member (i.e. verb) must be a string, not %s" %
+ (verb.type_name))
+ return verb.value.lower()
+
+ def get_token(self, verb, statement, index, namespace,
+ storage_type=None, token_types=None):
+ try:
+ item = statement[index]
+ except IndexError:
+ raise InvalidRuleError("verb '%s' requires at least %d items but only %d are available." %
+ (verb, index+1, len(statement)))
+
+ try:
+ token = Token(item, namespace)
+ except Exception as exc:
+ raise StatementError("parameter %d, %s" % (index, exc))
+
+
+ if storage_type is not None:
+ if token.storage_type not in storage_type:
+ raise TypeError("verb '%s' requires parameter #%d to have storage types %s not %s. statement=%s" %
+ (verb, index,
+ [Token.get_storage_type_name(x)
+ for x in storage_type],
+ token.storage_type_name, statement))
+
+ if token_types is not None:
+ try:
+ token.load() # Note, Token.load() sets the Token.type
+ except UndefinedValueError:
+ # OK if not yet defined
+ pass
+
+ if token.type not in token_types:
+ raise TypeError("verb '%s' requires parameter #%d to have types %s, not %s. statement=%s" %
+ (verb, index,
+ [Token.get_type_name(x) for x in sorted(token_types)],
+ token.type_name, statement))
+
+ return token
+
+ def get_parameter(self, verb, statement, index, namespace, token_types=None):
+ try:
+ item = statement[index]
+ except IndexError:
+ raise InvalidRuleError("verb '%s' requires at least %d items but only %d are available." %
+ (verb, index+1, len(statement)))
+
+ try:
+ token = Token(item, namespace)
+ except Exception as exc:
+ raise StatementError("parameter %d, %s" % (index, exc))
+
+
+ if token_types is not None:
+ token.get() # Note, Token.get() sets the Token.type
+
+ if token.type not in token_types:
+ raise TypeError("verb '%s' requires parameter #%d to have types %s, not %s. statement=%s" %
+ (verb, index,
+ [Token.get_type_name(x) for x in sorted(token_types)],
+ token.type_name, statement))
+
+ token.load()
+ return token
+
+ def get_raw_parameter(self, verb, statement, index, token_types=None):
+ try:
+ item = statement[index]
+ except IndexError:
+ raise InvalidRuleError("verb '%s' requires at least %d items but only %d are available." %
+ (verb, index+1, len(statement)))
+
+ if token_types is not None:
+ item_type = Token.classify(item)
+
+ if item_type not in token_types:
+ raise TypeError("verb '%s' requires parameter #%d to have types %s, not %s. statement=%s" %
+ (verb, index,
+ [Token.get_type_name(x) for x in sorted(token_types)],
+ Token.get_type_name(item_type), statement))
+
+ return item
+
+ def get_variable(self, verb, statement, index, namespace):
+
+ try:
+ item = statement[index]
+ except IndexError:
+ raise InvalidRuleError("verb '%s' requires at least %d items but only %d are available." %
+ (verb, index+1, len(statement)))
+
+ try:
+ token = Token(item, namespace)
+ except Exception as exc:
+ raise StatementError("parameter %d, %s" % (index, exc))
+
+ if token.storage_type != Token.STORAGE_TYPE_VARIABLE:
+ raise TypeError("verb '%s' requires parameter #%d to be a variable not %s. statement=%s" %
+ (verb, index, token.storage_type_name, statement))
+
+ return token
+
+
+
+ def process(self, assertion):
+ self.success = True
+ for rule_number, rule in enumerate(self.rules):
+ namespace = {}
+ namespace[RULE_NUMBER] = rule_number
+ namespace[RULE_NAME] = ''
+ namespace[ASSERTION] = copy.deepcopy(assertion)
+ try:
+ result = self.process_rule(namespace, rule)
+ except Exception as exc:
+ self.log.error("%s", exc) # FIXME log.exception?
+ raise
+ if result == RULE_SUCCESS:
+ mapped = {}
+ mapping = self.get_mapping(namespace, rule)
+ for k, v in mapping.iteritems():
+ try:
+ token = Token(v, namespace)
+ new_value = token.get()
+ except Exception as e:
+ raise InvalidRule("%s unable to get value for mapping %s=%s, %s" % (self.rule_id(namespace), k, v, e))
+ mapped[k] = new_value
+ return mapped
+ return None
+
+
+ def process_rule(self, namespace, rule):
+ statement_blocks = rule.get('statement_blocks')
+ if statement_blocks is None:
+ raise InvalidRuleError("rule missing 'statement_blocks'")
+
+ result = BLOCK_CONTINUE
+ for block_number, block in enumerate(statement_blocks):
+ namespace[BLOCK_NUMBER] = block_number
+ namespace[BLOCK_NAME] = ''
+ result = self.process_block(namespace, block)
+ if result in (RULE_SUCCESS, RULE_FAIL):
+ break
+ elif result == BLOCK_CONTINUE:
+ continue
+ else:
+ raise ValueError("%s unexpected block result: %s" %
+ (self.statement_id(namespace), result))
+ if result in (RULE_SUCCESS, BLOCK_CONTINUE):
+ return RULE_SUCCESS
+ else:
+ return RULE_FAIL
+
+ def process_block(self, namespace, statements):
+ result = STATEMENT_CONTINUE
+
+ for statement_number, statement in enumerate(statements):
+ namespace[STATEMENT_NUMBER] = statement_number
+ try:
+ result = self.process_statement(namespace, statement)
+ except Exception as exc:
+ raise StatementError("%s statement=%s %s" % (self.statement_id(namespace), statement, exc))
+ if result in (BLOCK_CONTINUE, RULE_SUCCESS, RULE_FAIL):
+ break
+ elif result == STATEMENT_CONTINUE:
+ continue
+ else:
+ raise ValueError("%s unexpected statement result: %s" %
+ (self.statement_id(namespace), result))
+
+ if result == STATEMENT_CONTINUE:
+ result = BLOCK_CONTINUE
+
+ return result
+
+ def process_statement(self, namespace, statement):
+ result = STATEMENT_CONTINUE
+
+ verb = self.get_verb(statement)
+
+ if verb == 'set':
+ result = self.verb_set(verb, namespace, statement)
+ elif verb == 'length':
+ result = self.verb_length(verb, namespace, statement)
+ elif verb == 'interpolate':
+ result = self.verb_interpolate(verb, namespace, statement)
+ elif verb == 'append':
+ result = self.verb_append(verb, namespace, statement)
+ elif verb == 'unique':
+ result = self.verb_unique(verb, namespace, statement)
+ elif verb == 'split':
+ result = self.verb_split(verb, namespace, statement)
+ elif verb == 'join':
+ result = self.verb_join(verb, namespace, statement)
+ elif verb == 'lower':
+ result = self.verb_lower(verb, namespace, statement)
+ elif verb == 'upper':
+ result = self.verb_upper(verb, namespace, statement)
+ elif verb == 'in':
+ result = self.verb_in(verb, namespace, statement)
+ elif verb == 'not_in':
+ result = self.verb_not_in(verb, namespace, statement)
+ elif verb == 'compare':
+ result = self.verb_compare(verb, namespace, statement)
+ elif verb == 'regexp':
+ result = self.verb_regexp(verb, namespace, statement)
+ elif verb == 'regexp_replace':
+ result = self.verb_regexp_replace(verb, namespace, statement)
+ elif verb == 'exit':
+ result = self.verb_exit(verb, namespace, statement)
+ elif verb == 'continue':
+ result = self.verb_continue(verb, namespace, statement)
+ else:
+ raise InvalidRuleError("unknown verb '%s'" % (verb))
+
+ return result
+
+ def verb_set(self, verb, namespace, statement):
+ variable = self.get_variable(verb, statement, 1, namespace)
+ parameter = self.get_parameter(verb, statement, 2, namespace)
+
+ variable.set(parameter.value)
+ self.success = True
+
+ if self.log.isEnabledFor(logging.DEBUG):
+ self.log.debug("%s verb='%s' success=%s variable: %s=%s",
+ self.statement_id(namespace), verb, self.success,
+ variable, variable.get())
+
+ return STATEMENT_CONTINUE
+
+ def verb_length(self, verb, namespace, statement):
+ variable = self.get_variable(verb, statement, 1, namespace)
+ parameter = self.get_parameter(verb, statement, 2, namespace,
+ set([Token.TYPE_ARRAY,
+ Token.TYPE_MAP,
+ Token.TYPE_STRING]))
+
+ try:
+ length = len(parameter.value)
+ except Exception as exc:
+ raise ValueError("verb '%s' failed, variable='%s' parameter='%s': %s" %
+ (verb, variable, parameter.value, exc))
+ variable.set(length)
+ self.success = True
+
+ if self.log.isEnabledFor(logging.DEBUG):
+ self.log.debug("%s verb='%s' success=%s variable: %s=%s parameter=%s",
+ self.statement_id(namespace), verb, self.success,
+ variable, variable.get(), parameter.value)
+
+ return STATEMENT_CONTINUE
+
+ def verb_interpolate(self, verb, namespace, statement):
+ variable = self.get_variable(verb, statement, 1, namespace)
+ string = self.get_raw_parameter(verb, statement, 2,
+ set([Token.TYPE_STRING]))
+
+ try:
+ new_value = self.substitute_variables(string, namespace)
+ except Exception as exc:
+ raise ValueError("verb '%s' failed, variable='%s' string='%s': %s" %
+ (verb, variable, string, exc))
+ variable.set(new_value)
+ self.success = True
+
+ if self.log.isEnabledFor(logging.DEBUG):
+ self.log.debug("%s verb='%s' success=%s variable: %s=%s string='%s'",
+ self.statement_id(namespace), verb, self.success,
+ variable, variable.get(), string)
+
+ return STATEMENT_CONTINUE
+
+ def verb_append(self, verb, namespace, statement):
+ variable = self.get_token(verb, statement, 1, namespace,
+ set([Token.STORAGE_TYPE_VARIABLE]),
+ set([Token.TYPE_ARRAY]))
+ item = self.get_parameter(verb, statement, 2, namespace)
+
+ try:
+ variable.get().append(item.value)
+ except Exception as exc:
+ raise ValueError("verb '%s' failed, variable='%s' item='%s': %s" %
+ (verb, variable, item.value, exc))
+ self.success = True
+
+ if self.log.isEnabledFor(logging.DEBUG):
+ self.log.debug("%s verb='%s' success=%s variable: %s=%s item=%s",
+ self.statement_id(namespace), verb, self.success,
+ variable, variable.get(), item.value)
+
+ return STATEMENT_CONTINUE
+
+ def verb_unique(self, verb, namespace, statement):
+ variable = self.get_variable(verb, statement, 1, namespace)
+ array = self.get_parameter(verb, statement, 2, namespace,
+ set([Token.TYPE_ARRAY]))
+
+ seen = set()
+ new_value = []
+
+ for member in array.value:
+ if member in seen:
+ continue
+ new_value.append(member)
+ seen.add(member)
+
+
+ variable.set(new_value)
+ self.success = True
+
+ if self.log.isEnabledFor(logging.DEBUG):
+ self.log.debug("%s verb='%s' success=%s variable: %s=%s array=%s",
+ self.statement_id(namespace), verb, self.success,
+ variable, variable.get(), array.value)
+
+ return STATEMENT_CONTINUE
+
+ def verb_split(self, verb, namespace, statement):
+ variable = self.get_variable(verb, statement, 1, namespace)
+ string = self.get_parameter(verb, statement, 2, namespace,
+ set([Token.TYPE_STRING]))
+ pattern = self.get_parameter(verb, statement, 3, namespace,
+ set([Token.TYPE_STRING]))
+
+ try:
+ new_value = re.split(pattern.value, string.value)
+ except Exception as exc:
+ raise ValueError("verb '%s' failed, pattern='%s' string='%s': %s" %
+ (verb, pattern.value, string.value, exc))
+
+ variable.set(new_value)
+ self.success = True
+
+ if self.log.isEnabledFor(logging.DEBUG):
+ self.log.debug("%s verb='%s' success=%s variable: %s=%s string='%s' pattern='%s'",
+ self.statement_id(namespace), verb, self.success,
+ variable, variable.get(), string.value, pattern.value)
+
+ return STATEMENT_CONTINUE
+
+ def verb_join(self, verb, namespace, statement):
+ variable = self.get_variable(verb, statement, 1, namespace)
+ array = self.get_parameter(verb, statement, 2, namespace,
+ set([Token.TYPE_ARRAY]))
+ conjunction = self.get_parameter(verb, statement, 3, namespace,
+ set([Token.TYPE_STRING]))
+
+ try:
+ new_value = conjunction.value.join(array.value)
+ except Exception as exc:
+ raise ValueError("verb '%s' failed, array=%s conjunction='%s'': %s" %
+ (verb, array.value, conjunction.value, exc))
+
+
+ variable.set(new_value)
+ self.success = True
+
+ if self.log.isEnabledFor(logging.DEBUG):
+ self.log.debug("%s verb='%s' success=%s variable: %s=%s array=%s conjunction='%s'",
+ self.statement_id(namespace), verb, self.success,
+ variable, variable.get(), array.value, conjunction.value)
+
+ return STATEMENT_CONTINUE
+
+ def verb_lower(self, verb, namespace, statement):
+ variable = self.get_variable(verb, statement, 1, namespace)
+ parameter = self.get_parameter(verb, statement, 2, namespace,
+ token_types=set([Token.TYPE_STRING,
+ Token.TYPE_ARRAY,
+ Token.TYPE_MAP]))
+
+ try:
+ if parameter.type == Token.TYPE_STRING:
+ new_value = parameter.value.lower()
+ elif parameter.type == Token.TYPE_ARRAY:
+ new_value = [x.lower() for x in parameter.value]
+ elif parameter.type == Token.TYPE_MAP:
+ new_value = dict((k.lower(), v)
+ for k, v in parameter.value.iteritems())
+ else:
+ raise IllegalStateError("unexpected token type: %s" %
+ (parameter.type_name))
+ except Exception as exc:
+ raise ValueError("verb '%s' failed, variable='%s' parameter='%s': %s" %
+ (verb, variable, parameter.value, exc))
+ variable.set(new_value)
+ self.success = True
+
+ if self.log.isEnabledFor(logging.DEBUG):
+ self.log.debug("%s verb='%s' success=%s variable: %s=%s parameter=%s",
+ self.statement_id(namespace), verb, self.success,
+ variable, variable.get(), parameter)
+
+ return STATEMENT_CONTINUE
+
+ def verb_upper(self, verb, namespace, statement):
+ variable = self.get_variable(verb, statement, 1, namespace)
+ parameter = self.get_parameter(verb, statement, 2, namespace,
+ token_types=set([Token.TYPE_STRING,
+ Token.TYPE_ARRAY,
+ Token.TYPE_MAP]))
+
+ try:
+ if parameter.type == Token.TYPE_STRING:
+ new_value = parameter.value.upper()
+ elif parameter.type == Token.TYPE_ARRAY:
+ new_value = [x.upper() for x in parameter.value]
+ elif parameter.type == Token.TYPE_MAP:
+ new_value = dict((k.upper(), v)
+ for k, v in parameter.value.iteritems())
+ else:
+ raise IllegalStateError("unexpected token type: %s" %
+ (parameter.type_name))
+ except Exception as exc:
+ raise ValueError("verb '%s' failed, variable='%s' parameter='%s': %s" %
+ (verb, variable, parameter.value, exc))
+ variable.set(new_value)
+ self.success = True
+
+ if self.log.isEnabledFor(logging.DEBUG):
+ self.log.debug("%s verb='%s' success=%s variable: %s=%s parameter=%s",
+ self.statement_id(namespace), verb, self.success,
+ variable, variable.get(), parameter)
+
+ return STATEMENT_CONTINUE
+
+ def verb_in(self, verb, namespace, statement):
+ member = self.get_parameter(verb, statement, 1, namespace)
+ collection = self.get_parameter(verb, statement, 2, namespace,
+ token_types=set([Token.TYPE_ARRAY,
+ Token.TYPE_MAP,
+ Token.TYPE_STRING]))
+
+ try:
+ self.success = member.value in collection.value
+ except Exception as exc:
+ raise ValueError("verb '%s' failed, member='%s' collection='%s': %s" %
+ (verb, member.value, collection.value, exc))
+
+ if self.log.isEnabledFor(logging.DEBUG):
+ self.log.debug("%s verb='%s' success=%s member=%s collection=%s",
+ self.statement_id(namespace), verb, self.success,
+ member.value, collection.value)
+
+ return STATEMENT_CONTINUE
+
+ def verb_not_in(self, verb, namespace, statement):
+ member = self.get_parameter(verb, statement, 1, namespace)
+ collection = self.get_parameter(verb, statement, 2, namespace,
+ token_types=set([Token.TYPE_ARRAY,
+ Token.TYPE_MAP,
+ Token.TYPE_STRING]))
+
+ try:
+ self.success = member.value not in collection.value
+ except Exception as exc:
+ raise ValueError("verb '%s' failed, member='%s' collection='%s': %s" %
+ (verb, member.value, collection.value, exc))
+
+ if self.log.isEnabledFor(logging.DEBUG):
+ self.log.debug("%s verb='%s' success=%s member=%s collection=%s",
+ self.statement_id(namespace), verb, self.success,
+ member.value, collection.value)
+
+ return STATEMENT_CONTINUE
+
+ def verb_compare(self, verb, namespace, statement):
+ left = self.get_parameter(verb, statement, 1, namespace)
+ op = self.get_parameter(verb, statement, 2, namespace,
+ set([Token.TYPE_STRING]))
+ right = self.get_parameter(verb, statement, 3, namespace)
+
+ if left.type != right.type:
+ raise TypeError("verb '%s' both items must have the same type left is %s and right is %s" %
+ (verb,
+ left.type_name, right.type_name))
+
+ try:
+ if op.value == '==':
+ self.success = left.value == right.value
+ elif op.value == '!=':
+ self.success = left.value != right.value
+ elif op.value == '<':
+ self.success = left.value < right.value
+ elif op.value == '<=':
+ self.success = left.value <= right.value
+ elif op.value == '>':
+ self.success = left.value > right.value
+ elif op.value == '>=':
+ self.success = left.value >= right.value
+ else:
+ raise InvalidRuleError("verb '%s' has unknown comparison operator '%s'" %
+ (verb, op.value))
+ except Exception as exc:
+ self.success = False
+ raise ValueError("verb '%s' failed, left=%s op='%s' right=%s, %s" %
+ (verb, left.value, op.value, right.value, exc))
+
+ if self.log.isEnabledFor(logging.DEBUG):
+ self.log.debug("%s verb='%s' success=%s left=%s op='%s' right=%s",
+ self.statement_id(namespace), verb, self.success,
+ left.value, op.value, right.value)
+
+ return STATEMENT_CONTINUE
+
+ def verb_regexp(self, verb, namespace, statement):
+ string = self.get_parameter(verb, statement, 1, namespace,
+ set([Token.TYPE_STRING]))
+ pattern = self.get_parameter(verb, statement, 2, namespace,
+ set([Token.TYPE_STRING]))
+
+ try:
+ match = re.search(pattern.value, string.value)
+ except Exception as exc:
+ self.success = False
+ raise ValueError("verb '%s' failed, string='%s' pattern='%s', %s" %
+ (verb, string.value, pattern.value, exc))
+ if match:
+ self.success = True
+
+ # Note, match.groups() returns a tuple
+ # containing all the subgroups of the match,
+ # from 1 up to however many groups are in the
+ # pattern. But we want to allow zero-based
+ # indexing as well as access to group 0,
+ # therefore we insert group 0 at the head of
+ # the list.
+
+ result = list(match.groups())
+ result.insert(0, match.group(0))
+ namespace[REGEXP_ARRAY_VARIABLE] = result
+ namespace[REGEXP_MAP_VARIABLE] = match.groupdict()
+ else:
+ self.success = False
+ namespace[REGEXP_ARRAY_VARIABLE] = []
+ namespace[REGEXP_MAP_VARIABLE] = {}
+
+ if self.log.isEnabledFor(logging.DEBUG):
+ self.log.debug("%s verb='%s' success=%s string='%s' pattern='%s' %s=%s %s=%s",
+ self.statement_id(namespace), verb, self.success,
+ string.value, pattern.value,
+ REGEXP_ARRAY_VARIABLE,
+ namespace[REGEXP_ARRAY_VARIABLE],
+ REGEXP_MAP_VARIABLE,
+ namespace[REGEXP_MAP_VARIABLE])
+
+ return STATEMENT_CONTINUE
+
+ def verb_regexp_replace(self, verb, namespace, statement):
+ variable = self.get_variable(verb, statement, 1, namespace,)
+ string = self.get_parameter(verb, statement, 2, namespace,
+ set([Token.TYPE_STRING]))
+ pattern = self.get_parameter(verb, statement, 3, namespace,
+ set([Token.TYPE_STRING]))
+ replacement = self.get_parameter(verb, statement, 4, namespace,
+ set([Token.TYPE_STRING]))
+
+ try:
+ new_value = re.sub(pattern.value, replacement.value, string.value)
+ except Exception as exc:
+ self.success = False
+ raise ValueError("verb '%s' verb failed, pattern='%s' replacement='%s', %s" %
+ (verb, pattern.value, replacement.value, exc))
+ else:
+ variable.set(new_value)
+ self.success = True
+
+ if self.log.isEnabledFor(logging.DEBUG):
+ self.log.debug("%s verb='%s' success=%s variable: %s=%s string='%s' pattern='%s' replacement='%s'",
+ self.statement_id(namespace), verb, self.success,
+ variable, variable.get(),
+ string.value, pattern.value, replacement.value)
+
+ return STATEMENT_CONTINUE
+
+ def verb_exit(self, verb, namespace, statement):
+ statement_result = STATEMENT_CONTINUE
+
+ exit_status_param = self.get_parameter(verb, statement, 1, namespace,
+ set([Token.TYPE_STRING]))
+ criteria_param = self.get_parameter(verb, statement, 2, namespace,
+ set([Token.TYPE_STRING]))
+
+ exit_status = exit_status_param.value.lower()
+ criteria = criteria_param.value.lower()
+
+ if exit_status == 'rule_succeeds':
+ result = RULE_SUCCESS
+ elif exit_status == 'rule_fails':
+ result = RULE_FAIL
+ else:
+ raise InvalidRuleError("verb='%s' unknown exit status '%s'" %
+ (verb, exit_status))
+
+
+ if criteria == 'if_success':
+ if self.success:
+ do_exit = True
+ else:
+ do_exit = False
+ elif criteria == 'if_not_success':
+ if not self.success:
+ do_exit = True
+ else:
+ do_exit = False
+ elif criteria == 'always':
+ do_exit = True
+ elif criteria == 'never':
+ do_exit = False
+ else:
+ raise InvalidRuleError("verb='%s' unknown exit criteria '%s'" %
+ (verb, criteria))
+
+ if do_exit:
+ statement_result = result
+
+ if self.log.isEnabledFor(logging.DEBUG):
+ self.log.debug("%s verb='%s' success=%s status=%s criteria=%s exiting=%s result=%s",
+ self.statement_id(namespace), verb, self.success,
+ exit_status, criteria, do_exit,
+ rule_result_name(statement_result))
+
+ return statement_result
+
+
+ def verb_continue(self, verb, namespace, statement):
+ statement_result = STATEMENT_CONTINUE
+
+ criteria_param = self.get_parameter(verb, statement, 1, namespace,
+ set([Token.TYPE_STRING]))
+ criteria = criteria_param.value.lower()
+
+ if criteria == 'if_success':
+ if self.success:
+ do_continue = True
+ else:
+ do_continue = False
+ elif criteria == 'if_not_success':
+ if not self.success:
+ do_continue = True
+ else:
+ do_continue = False
+ elif criteria == 'always':
+ do_continue = True
+ elif criteria == 'never':
+ do_continue = False
+ else:
+ raise InvalidRuleError("verb='%s' unknown continue criteria '%s'" %
+ (verb, criteria))
+
+ if do_continue:
+ statement_result = BLOCK_CONTINUE
+
+ if self.log.isEnabledFor(logging.DEBUG):
+ self.log.debug("%s verb='%s' success=%s criteria=%s continuing=%s result=%s",
+ self.statement_id(namespace), verb, self.success,
+ criteria, do_continue,
+ rule_result_name(statement_result))
+
+ return statement_result
+
diff --git a/python/mapping_app.py b/python/mapping_app.py
new file mode 100755
index 0000000..c590061
--- /dev/null
+++ b/python/mapping_app.py
@@ -0,0 +1,55 @@
+#!/usr/bin/python
+
+import json
+import logging
+from idp_mapping import RuleProcessor
+import sys
+import traceback
+
+LOG = logging.getLogger()
+logging.basicConfig(level=logging.INFO, format='%(message)s')
+
+rule_filename = '/home/jdennis/src/misc/federation_mapping/rules-python-01.json'
+assertion_filename = '/home/jdennis/src/misc/federation_mapping/assertion-01.json'
+
+def assertion_from_file(filename):
+ with open(filename) as stream:
+ assertion = json.load(stream)
+ return assertion
+
+def assertion_from_string(string):
+ assertion = json.loads(string)
+ return assertion
+
+def main():
+ if True:
+ rule_processor = RuleProcessor.from_file(rule_filename)
+
+ if False:
+ with open(rule_filename) as stream:
+ rule_processor = RuleProcessor.from_stream(stream)
+
+ if False:
+ with open(rule_filename) as stream:
+ string = stream.read()
+ rule_processor = RuleProcessor.from_string(string)
+
+ assertion = assertion_from_file(assertion_filename)
+
+ try:
+ mapped = rule_processor.process(assertion)
+ if mapped is None:
+ print "no rules matched"
+ else:
+ for k, v in mapped.iteritems():
+ print "%s: %s" % (k, v)
+ mapped_json = json.dumps(mapped, indent=4)
+ print "\nmapped JSON"
+ print mapped_json
+ except Exception as exc:
+ print "FAIL: %s" % exc
+ traceback.print_exc()
+ sys.exit(1)
+ sys.exit(0)
+
+main()