/* See LICENSE for licensing and NOTICE for copyright. */
package org.ldaptive.asn1;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.ldaptive.LdapUtils;
/**
* Describes paths to individual elements of an encoded DER object that may be addressed during parsing to associate a
* parsed element with a handler to handle that element. Consider the following production rule for a complex type that
* may be DER encoded:
*
* <pre>
BankAccountSet ::= SET OF {
account BankAccount
}
BankAccount ::= SEQUENCE OF {
accountNumber OCTET STRING,
accountName OCTET STRING,
accountType AccountType,
balance REAL
}
AccountType ::= ENUM {
checking (0),
savings (1)
}
* </pre>
*
* <p>Given an instance of BankAccountSet with two elements, the path to the balance of each bank account in the set is
* given by the following expression:</p>
*
* <pre>/SET/SEQ/REAL</pre>
*
* <p>Individual child elements can be accessed by explicitly mentioning the index of the item relative to its parent.
* For example, the second bank account in the set can be accessed as follows:</p>
*
* <pre>/SET/SEQ[1]</pre>
*
* <p>Node names in DER paths are constrained to the following:</p>
*
* <ul>
* <li>{@link UniversalDERTag} tag names</li>
* <li>{@link ApplicationDERTag#TAG_NAME}</li>
* <li>{@link ContextDERTag#TAG_NAME}</li>
* </ul>
*
* @author Middleware Services
* @see DERParser
*/
public class DERPath
{
/** Separates nodes in a path specification. */
public static final String PATH_SEPARATOR = "/";
/** General pattern for DER path nodes. */
private static final Pattern NODE_PATTERN;
/** hash code seed. */
private static final int HASH_CODE_SEED = 601;
/**
* Class initializer.
*/
static {
final StringBuilder validNames = new StringBuilder();
validNames.append(ApplicationDERTag.TAG_NAME).append("\\(\\d+\\)|");
validNames.append(ContextDERTag.TAG_NAME).append("\\(\\d+\\)|");
for (UniversalDERTag tag : UniversalDERTag.values()) {
validNames.append('|').append(tag.name());
}
NODE_PATTERN = Pattern.compile(String.format("(%s)(\\[(\\d+)\\])?", validNames.toString()));
}
/** Describes the path as a FIFO set of nodes. */
private final Deque<Node> nodeStack = new ArrayDeque<>();
/** Creates an empty path specification. */
public DERPath()
{
this(PATH_SEPARATOR);
}
/**
* Copy constructor.
*
* @param path to read nodes from
*/
public DERPath(final DERPath path)
{
nodeStack.addAll(path.nodeStack);
}
/**
* Creates a path specification from its string representation.
*
* @param pathSpec string representation of a path, e.g. /SEQ[1]/CHOICE.
*/
public DERPath(final String pathSpec)
{
final String[] nodes = pathSpec.split(PATH_SEPARATOR);
for (String node : nodes) {
if ("".equals(node)) {
continue;
}
// Normalize node names to upper case
nodeStack.add(toNode(node.toUpperCase()));
}
}
/**
* Appends a node to the path.
*
* @param name of the path element to add
*
* @return This instance with new node appended.
*/
public DERPath pushNode(final String name)
{
nodeStack.addLast(new Node(name));
return this;
}
/**
* Appends a node to the path with the given child index.
*
* @param name of the path element to add
* @param index child index
*
* @return This instance with new node appended.
*/
public DERPath pushNode(final String name, final int index)
{
nodeStack.addLast(new Node(name, index));
return this;
}
/**
* Examines the last node in the path without removing it.
*
* @return last node in the path or null if no nodes remain
*/
public String peekNode()
{
return nodeStack.peek().toString();
}
/**
* Removes the last node in the path.
*
* @return last node in the path or null if no more nodes remain.
*/
public String popNode()
{
if (nodeStack.isEmpty()) {
return null;
}
return nodeStack.removeLast().toString();
}
/**
* Gets the number of nodes in the path.
*
* @return node count.
*/
public int getSize()
{
return nodeStack.size();
}
/**
* Determines whether the path contains any nodes.
*
* @return True if path contains 0 nodes, false otherwise.
*/
public boolean isEmpty()
{
return nodeStack.isEmpty();
}
@Override
public boolean equals(final Object o)
{
if (o == this) {
return true;
}
if (o instanceof DERPath) {
final DERPath v = (DERPath) o;
return LdapUtils.areEqual(nodeStack.toArray(), v.nodeStack.toArray());
}
return false;
}
@Override
public int hashCode()
{
return LdapUtils.computeHashCode(HASH_CODE_SEED, nodeStack);
}
@Override
public String toString()
{
final StringBuilder sb = new StringBuilder(nodeStack.size() * 10);
for (Node node : nodeStack) {
sb.append(PATH_SEPARATOR);
node.toString(sb);
}
return sb.toString();
}
/**
* Converts a string representation of a node into a {@link Node} object.
*
* @param node String representation of node.
*
* @return Node corresponding to given string representation.
*
* @throws IllegalArgumentException for an invalid node name.
*/
static Node toNode(final String node)
{
final Matcher matcher = NODE_PATTERN.matcher(node);
if (!matcher.matches()) {
throw new IllegalArgumentException("Invalid node: " + node);
}
final String name = matcher.group(1);
final String index = matcher.group(3);
if (index != null) {
return new Node(name, Integer.parseInt(index));
}
return new Node(name);
}
/**
* DER path node encapsulates the path name and its location among other children that share a common parent.
*
* @author Middleware Services
*/
static class Node
{
/** hash code seed. */
private static final int HASH_CODE_SEED = 607;
/** Name of this node. */
private final String name;
/** Index of this node. */
private final int childIndex;
/**
* Creates a new node with an indeterminate index.
*
* @param n name of this node
*/
Node(final String n)
{
name = n;
childIndex = -1;
}
/**
* Creates a new node with the given index.
*
* @param n name of this node
* @param i child index location of this node in the path
*/
Node(final String n, final int i)
{
if (i < 0) {
throw new IllegalArgumentException("Child index cannot be negative.");
}
name = n;
childIndex = i;
}
/**
* Returns the name.
*
* @return name
*/
public String getName()
{
return name;
}
/**
* Returns the child index.
*
* @return child index
*/
public int getChildIndex()
{
return childIndex;
}
@Override
public boolean equals(final Object o)
{
if (o == this) {
return true;
}
if (o instanceof Node) {
final Node v = (Node) o;
return LdapUtils.areEqual(name, v.name) &&
LdapUtils.areEqual(childIndex, v.childIndex);
}
return false;
}
@Override
public int hashCode()
{
return LdapUtils.computeHashCode(HASH_CODE_SEED, name, childIndex);
}
@Override
public String toString()
{
// CheckStyle:MagicNumber OFF
final StringBuilder sb = new StringBuilder(name.length() + 4);
// CheckStyle:MagicNumber ON
toString(sb);
return sb.toString();
}
/**
* Appends the string representation of this instance to the given string builder.
*
* @param builder Builder to hold string representation of this instance.
*/
public void toString(final StringBuilder builder)
{
builder.append(name);
if (childIndex < 0) {
return;
}
builder.append('[').append(childIndex).append(']');
}
}
}