package org.safehaus.penrose.ldap;
import org.apache.log4j.Logger;
import java.util.*;
/**
* @author Endi S. Dewata
*/
public class DNBuilder {
Logger log = Logger.getLogger(getClass());
public List<RDN> rdns = new ArrayList<RDN>();
public void set(String dn) throws Exception {
rdns.clear();
append(dn);
}
public void set(DN dn) throws Exception {
rdns.clear();
append(dn);
}
public void set(RDN rdn) {
rdns.clear();
rdns.add(rdn);
}
public void append(String dn) throws Exception {
if (dn == null) return;
Collection<RDN> list = parse(dn);
for (RDN rdn : list) {
rdns.add(rdn);
}
}
public void append(RDN rdn) {
if (rdn == null) return;
rdns.add(rdn);
}
public void append(DN dn) throws Exception {
if (dn == null) return;
Collection<RDN> list = dn.getRdns();
for (RDN rdn : list) {
rdns.add(rdn);
}
}
public void prepend(String dn) throws Exception {
if (dn == null) return;
Collection<RDN> list = parse(dn);
for (RDN rdn : list) {
rdns.add(0, rdn);
}
}
public void prepend(RDN rdn) {
if (rdn == null) return;
rdns.add(0, rdn);
}
public void prepend(DN dn) throws Exception {
if (dn == null) return;
Collection<RDN> list = dn.getRdns();
for (RDN rdn : list) {
rdns.add(0, rdn);
}
}
public boolean isEmpty() {
return rdns.isEmpty();
}
public int getSize() {
return rdns.size();
}
public void clear() {
rdns.clear();
}
public static RDN parseRdn(String rdn) throws Exception {
Collection list = parse(rdn);
if (list.isEmpty()) return null;
return (RDN)list.iterator().next();
}
/**
* Take a "string" representation of a distinguished name and return an
* ordered list or relative distingished names.
* The string is formatted as
* <attributeName>=<attributeValue>[+<attributeName>=<attributeValue>],...
* attributeName is case insensitive.
* attributeValue is case sensitive.
* Contents may be escaped with backslash to
* avoid the special meaning they would have otherwise.
* Or, the value may be wrapped by double quotes in which case
* the special characters do not need to be escaped, but the quotes are not
* part of the value.
* A multi-valued RDN will have name=value pairs separated by the '+'
* character.
* @param dn DN
* @throws java.lang.Exception Invalid DN format
* @return a Collection of RDN objects
*/
public static Collection<RDN> parse(String dn) throws Exception {
Collection<RDN> list = new ArrayList<RDN>();
if (dn == null || "".equals(dn)) return list;
char[] chars = dn.toCharArray();
int start = 0;
int end = chars.length;
if (chars[start] == '"' && chars[end-1] == '"') {
start++; end--;
}
Map<String,Object> map = new TreeMap<String,Object>();
boolean inName = true;
boolean sawDQ = false;
boolean inDQ = false;
StringBuilder sb = new StringBuilder(100);
String name = null;
List<Integer> bytes = new ArrayList<Integer>();
Object val;
for (int i = start; i < end; i++) {
if (inName) {
switch (chars[i]) {
case '=':
name = sb.toString().trim();
sb.setLength(0);
inName = false;
break;
default:
sb.append(chars[i]);
}
} else {
switch (chars[i]) {
case '"':
sawDQ = true;
inDQ = !inDQ;
break;
case '+':
if (!inDQ) {
// an rdn w/multiple attributes
if (!bytes.isEmpty()) {
val = toByteArray(bytes);
bytes.clear();
} else {
val = sawDQ ? sb.toString() : sb.toString().trim();
sb.setLength(0);
}
map.put(name, val);
inName = true;
sawDQ = false;
break;
}
case ',':
case ';':
if (!inDQ) {
if (!bytes.isEmpty()) {
val = toByteArray(bytes);
bytes.clear();
} else {
val = sawDQ ? sb.toString() : sb.toString().trim();
sb.setLength(0);
}
map.put(name, val);
list.add(new RDN(map));
map = new TreeMap<String,Object>();
inName = true;
break;
}
case '#':
if (!inDQ) {
// this is an escape - pick up next character
while (i < end && chars[i+1] != '+' && chars[i+1] != ',' && chars[i+1] != ';') {
if (isHexDigit(chars[++i]))
{
// assume hexpair
String x = new String(new char[] { chars[i], chars[++i] });
bytes.add(Integer.parseInt(x, 16));
} else {
// invalid hexchar
}
}
break;
}
case '\\':
if (!inDQ) {
// this is an escape - pick up next character
if (isHexDigit(chars[i+1]) && isHexDigit(chars[i+2])) {
// Interpret all immediately follwing "\xx" escaped characters
// using UTF-8 decoding, as individual decoding of each character
// would break for >1 byte UTF-8 sequences.
// determine number of escaped characters
int counter = 1;
int s = i; // points to slash
i += 3;
while(i < end-2
&& chars[i] == '\\'
&& isHexDigit(chars[i+1])
&& isHexDigit(chars[i+2])) {
counter++;
i += 3;
}
// store un-escaped characters in byte buffer for decoding
byte utfBytes[] = new byte[counter];
i = s;
for(int j = 0; j < counter; j++) {
String x = new String(new char[] { chars[i+1], chars[i+2] });
utfBytes[j] = (byte)Integer.parseInt(x, 16);
i += 3;
}
String str;
try {
str = new String(utfBytes, "UTF-8");
} catch (Exception e) {
throw new RuntimeException(e.getMessage(), e);
}
// decode sequence
sb.append(str);
i--;
} else {
// normal escaped special character
i++;
sb.append(chars[i]);
}
break;
}
// else no escapes - fall through and keep the backslash literal
default:
sb.append(chars[i]);
}
}
}
if (inName) throw new Exception("Invalid DN format: "+dn);
// a well formed DN will have left us 1 more RDN at the end
if (!bytes.isEmpty()) {
val = toByteArray(bytes);
} else {
val = sawDQ ? sb.toString() : sb.toString().trim();
}
map.put(name, val);
list.add(new RDN(map));
return list;
}
private static boolean isHexDigit(char c) {
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
}
private static byte[] toByteArray(List<Integer> bytes) {
byte[] ba = new byte[bytes.size()];
for (int i = 0; i < ba.length; i++ ) {
ba[i] = bytes.get(i).byteValue();
}
return ba;
}
public DN toDn() {
DN dn = new DN();
dn.rdns.addAll(rdns);
return dn;
}
public String toString() {
return toDn().toString();
}
}