/**
*
*
* Author: Chris Betts
* Date: 28/11/2002 / 17:02:19
*/
package com.idega.core.ldap.client.jndi;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.BasicAttribute;
import javax.naming.directory.BasicAttributes;
import javax.naming.directory.DirContext;
/**
* <p>The Schema Ops provides a number of convenience methods for accessing schema information.</p>
*
* <p>In addition, it allows the schema to be accessed even if a particular jndi provider
* does not provide a getSchemaOps() call, by attempting to access a subschema subentry call directly.
* (e.g. for a DSML directory).</p>
*/
public class SchemaOps
{
protected DirContext ctx = null; // the root jndi context used for all directory queries.
Attributes rawSchemaAttributes = null; // what is read from the directory subschema subentry
private String schemaRoot = null; // the entry in the directory that holds the schema attribute set - usually cn=schema
HashMap oids = new HashMap(1000); // a fast lookup list of oids to descriptive names.
final static String subschemaAttributeName = "subschemaSubentry";
private final static Logger log = Logger.getLogger("com.idega.core.ldap.client.jndi.SchemaOps");
public static final String SCHEMA_FAKE_OBJECT_CLASS_NAME = "synthetic_JXplorer_schema_object";
private static final BasicAttribute schemaObjectClassAttribute = new BasicAttribute("objectClass");
private ArrayList fullObjectClassArray = null; // cache the complete list of object classes; it gets used a bit.
private ArrayList fullAttributeNameArray = null; // cache the complete list of attribute names; it gets used a bit.
static
{
schemaObjectClassAttribute.add("top");
schemaObjectClassAttribute.add(SCHEMA_FAKE_OBJECT_CLASS_NAME);
}
/**
* <p>Initialise the SchemaOps object, and read the
* full schema from the directory. Note that this is a very
* expensive operation, that can involve downloading 10-100k from
* the directory!</p>
*/
public SchemaOps(DirContext context)
throws NamingException
{
this.ctx = context;
if (this.ctx == null) {
return;
}
log.log(Level.FINER,"Reading Schema info from directory context");
setSchemaRoot(getSchemaRoot());
this.rawSchemaAttributes = getRawSchema();
//printRawSchema();
//System.out.println("\n\n\n\n");
loadOIDs();
}
/**
* <p>Initialise the SchemaOps object from an Attributes list.
* This constructor is intended for testing.
* </p>
*/
// package visibility for testing
SchemaOps(Attributes rawSchemaAtts)
// throws NamingException
{
this.ctx = null;
this.rawSchemaAttributes = rawSchemaAtts;
setSchemaRoot("cn=schema");
loadOIDs();
log.log(Level.FINER,"SCHEMA ROOTX:" + getSchemaRoot());
}
/**
* This attempts to translate an OID into a descriptive name. If it cannot
* find a translation, it will return the oid unchanged.
* @param oid the 'dot form' OID, e.g. "2.5.6.2" or whatever
* @return the human readable string form of the OID (e.g. "country")
*/
public String translateOID(String oid)
{
if (this.oids.containsKey(oid)) {
return (String)this.oids.get(oid);
}
else {
return oid;
}
}
/**
* setup the global list of oids vs readable strings, by loading the schema and using the rfc defaults
*/
protected void loadOIDs()
{
loadOIDsFromSchema();
loadStaticOIDs();
}
/**
* Iterates through the schema finding OIDs and their corresponding descriptions.
*/
protected void loadOIDsFromSchema()
{
if (this.rawSchemaAttributes == null) {
return;
}
try
{
NamingEnumeration rawSchemaAtts = this.rawSchemaAttributes.getAll();
while (rawSchemaAtts.hasMoreElements())
{
Attribute rawSchemaAtt = (Attribute)rawSchemaAtts.nextElement();
NamingEnumeration values = rawSchemaAtt.getAll();
while (values.hasMoreElements())
{
String value = (String)values.nextElement();
if (value.indexOf('(') == -1) {
log.log(Level.FINER,"skipping non schema attribute: " + rawSchemaAtt.getID() + ":" + value);
}
else {
this.oids.put (getOID(value), getFirstName(value));
}
}
}
}
catch (NamingException e)
{
log.log(Level.WARNING,"Unable to read schema oids: ", e);
}
}
protected void loadStaticOIDs()
{
// a quick pick of common syntaxes for Active Directory support
// (and other servers that don't publish syntax descriptions)
// taken from rfc 2252
this.oids.put("1.3.6.1.4.1.1466.115.121.1.1","ACI Item");
this.oids.put("1.3.6.1.4.1.1466.115.121.1.2","Access Point");
this.oids.put("1.3.6.1.4.1.1466.115.121.1.3","Attribute Type Description");
this.oids.put("1.3.6.1.4.1.1466.115.121.1.4","Audio");
this.oids.put("1.3.6.1.4.1.1466.115.121.1.5","Binary");
this.oids.put("1.3.6.1.4.1.1466.115.121.1.6","Bit String");
this.oids.put("1.3.6.1.4.1.1466.115.121.1.7","Boolean");
this.oids.put("1.3.6.1.4.1.1466.115.121.1.8","Certificate");
this.oids.put("1.3.6.1.4.1.1466.115.121.1.9","Certificate List");
this.oids.put("1.3.6.1.4.1.1466.115.121.1.10","Certificate Pair");
this.oids.put("1.3.6.1.4.1.1466.115.121.1.11","Country String");
this.oids.put("1.3.6.1.4.1.1466.115.121.1.12","DN");
this.oids.put("1.3.6.1.4.1.1466.115.121.1.13","Data Quality Syntax");
this.oids.put("1.3.6.1.4.1.1466.115.121.1.14","Delivery Method");
this.oids.put("1.3.6.1.4.1.1466.115.121.1.15","Directory String");
this.oids.put("1.3.6.1.4.1.1466.115.121.1.16","DIT Content Rule Description");
this.oids.put("1.3.6.1.4.1.1466.115.121.1.17","DIT Structure Rule Description");
this.oids.put("1.3.6.1.4.1.1466.115.121.1.18","DL Submit Permission");
this.oids.put("1.3.6.1.4.1.1466.115.121.1.19","DSA Quality Syntax");
this.oids.put("1.3.6.1.4.1.1466.115.121.1.20","DSE Type");
this.oids.put("1.3.6.1.4.1.1466.115.121.1.21","Enhanced Guide");
this.oids.put("1.3.6.1.4.1.1466.115.121.1.22","Facsimile Telephone Number");
this.oids.put("1.3.6.1.4.1.1466.115.121.1.23","Fax");
this.oids.put("1.3.6.1.4.1.1466.115.121.1.24","Generalized Time");
this.oids.put("1.3.6.1.4.1.1466.115.121.1.25","Guide");
this.oids.put("1.3.6.1.4.1.1466.115.121.1.26","IA5 String");
this.oids.put("1.3.6.1.4.1.1466.115.121.1.27","INTEGER");
this.oids.put("1.3.6.1.4.1.1466.115.121.1.28","JPEG");
this.oids.put("1.3.6.1.4.1.1466.115.121.1.54","LDAP Syntax Description");
this.oids.put("1.3.6.1.4.1.1466.115.121.1.56","LDAP Schema Definition");
this.oids.put("1.3.6.1.4.1.1466.115.121.1.57","LDAP Schema Description");
this.oids.put("1.3.6.1.4.1.1466.115.121.1.29","Master And Shadow Access Points");
this.oids.put("1.3.6.1.4.1.1466.115.121.1.30","Matching Rule Description");
this.oids.put("1.3.6.1.4.1.1466.115.121.1.31","Matching Rule Use Description");
this.oids.put("1.3.6.1.4.1.1466.115.121.1.32","Mail Preference");
this.oids.put("1.3.6.1.4.1.1466.115.121.1.33","MHS OR Address");
this.oids.put("1.3.6.1.4.1.1466.115.121.1.55","Modify Rights");
this.oids.put("1.3.6.1.4.1.1466.115.121.1.34","Name And Optional UID");
this.oids.put("1.3.6.1.4.1.1466.115.121.1.35","Name Form Description");
this.oids.put("1.3.6.1.4.1.1466.115.121.1.36","Numeric String");
this.oids.put("1.3.6.1.4.1.1466.115.121.1.37","Object Class Description");
this.oids.put("1.3.6.1.4.1.1466.115.121.1.40","Octet String");
this.oids.put("1.3.6.1.4.1.1466.115.121.1.38","OID");
this.oids.put("1.3.6.1.4.1.1466.115.121.1.39","Other Mailbox");
this.oids.put("1.3.6.1.4.1.1466.115.121.1.41","Postal Address");
this.oids.put("1.3.6.1.4.1.1466.115.121.1.42","Protocol Information");
this.oids.put("1.3.6.1.4.1.1466.115.121.1.43","Presentation Address");
this.oids.put("1.3.6.1.4.1.1466.115.121.1.44","Printable String");
this.oids.put("1.3.6.1.4.1.1466.115.121.1.58","Substring Assertion");
this.oids.put("1.3.6.1.4.1.1466.115.121.1.45","Subtree Specification");
this.oids.put("1.3.6.1.4.1.1466.115.121.1.46","Supplier Information");
this.oids.put("1.3.6.1.4.1.1466.115.121.1.47","Supplier Or Consumer");
this.oids.put("1.3.6.1.4.1.1466.115.121.1.48","Supplier And Consumer");
this.oids.put("1.3.6.1.4.1.1466.115.121.1.49","Supported Algorithm");
this.oids.put("1.3.6.1.4.1.1466.115.121.1.50","Telephone Number");
this.oids.put("1.3.6.1.4.1.1466.115.121.1.51","Teletex Terminal Identifier");
this.oids.put("1.3.6.1.4.1.1466.115.121.1.52","Telex Number");
this.oids.put("1.3.6.1.4.1.1466.115.121.1.53","UTC Time");
}
/**
* Utility method to print a syntax subtree...
* @param syntaxRoot the root of the syntax tree to print out, e.g. "", "objectClasses"
* @throws NamingException
*/
void debugPrint(String syntaxRoot)
throws NamingException
{
System.out.println("---DEBUG PRINT---");
System.out.println("schema root: "+ getSchemaRoot());
if (syntaxRoot.length()>0 && syntaxRoot.startsWith("schema=")==false) {
syntaxRoot = "schema=" + syntaxRoot;
}
tabbedDebugPrint(syntaxRoot, "");
System.out.println("-----------------");
}
void tabbedDebugPrint(String syntaxElement, String indent)
throws NamingException
{
System.out.println(indent + syntaxElement);
Attributes entry = getAttributes(syntaxElement);
System.out.println(indent + "--==< " + syntaxElement + ">==--");
if (entry == null) {
System.out.println(indent + " ** NULL ENTRY **");
}
else
{
NamingEnumeration atts = entry.getAll();
while (atts.hasMoreElements())
{
Attribute att = (Attribute)atts.nextElement();
System.out.println(indent + "att " + att.getID());
NamingEnumeration values = att.getAll();
while (values.hasMoreElements()) {
System.out.println(indent + " " + values.nextElement().toString());
}
}
}
System.out.println(indent + "-");
ArrayList list = listEntryNames(syntaxElement);
if (list == null)
{
return;
}
for (int i = 0; i < list.size(); i++)
{
String nextLevel = syntaxElement;
if (nextLevel.length()>0) {
nextLevel = "," + nextLevel;
}
nextLevel = "schema=" + list.get(i) + nextLevel;
tabbedDebugPrint(nextLevel, "\t"+indent);
}
}
public void printRawSchema()
{
if (this.rawSchemaAttributes == null)
{
System.out.println("NO SCHEMA READ!");
return;
}
try
{
System.out.println("---RAW SCHEMA---");
Enumeration attEnum = this.rawSchemaAttributes.getAll();
while (attEnum.hasMoreElements())
{
Attribute att = (Attribute)attEnum.nextElement();
String ID = att.getID();
Enumeration vals = att.getAll();
while (vals.hasMoreElements()) {
System.out.println(ID + " : " + vals.nextElement());
}
}
}
catch (NamingException e)
{
System.out.println("error printing raw schema:" + e);
}
}
/**
* <p>This returns the raw schema straight from the
* directory. Only use this if you <i>really</i>
* know what you're doing - see getAttributes() for
* an explanation.</p>
*/
// ...Your schema caching code here...
public Attributes getRawSchema()
throws NamingException
{
String rawSchemaRoot = getSchemaRoot();
log.log(Level.FINER,"reading raw schema from " + rawSchemaRoot);
//Attributes rawSchema = ctx.getAttributes(rawSchemaRoot);
// need to explicitly list operational attributes... (although eTrust Directory doesn't need this, others do)
// Attributes rawSchema = ctx.getAttributes(rawSchemaRoot, new String[] {"attributeTypes", "objectClasses", "matchingRules", "matchingRuleUse", "ditStructureRules", "ldapSyntaxes", "nameForms" });
Attributes rawSchema = this.ctx.getAttributes(rawSchemaRoot, new String[] {"attributeTypes", "objectClasses", "matchingRules", "ldapSyntaxes", "*" });
if (rawSchema == null)
{
log.log(Level.WARNING,"null schema read - returning empty schema list.");
rawSchema = new BasicAttributes(); // return empty atts rather than null, to cut down on 'check for null' code...
}
else
{
if (rawSchema.size()==0) // may be a type of directory that requires explicit listing of schema objects...
{
log.log(Level.WARNING,"Unable to read schema details from directory.");
rawSchema = new BasicAttributes(); // give up - set to empty :-(
return rawSchema;
}
log.log(Level.FINER,"some schema read...");
rawSchema.remove("objectClass"); // Paranoid. Yes.
rawSchema.remove("oc"); // But very thorough.
rawSchema.remove("objectclass");
String nameAttribute = rawSchemaRoot.substring(0, rawSchemaRoot.indexOf('='));
rawSchema.remove(nameAttribute);
}
return rawSchema;
}
private void setSchemaRoot(String schema)
{
this.schemaRoot = schema;
}
/**
* returns the root DN of the schema subentry as a string.
* @return the schema subentry (i.e. something like 'cn=schema')
*/
public String getSchemaRoot()
{
if (this.schemaRoot != null) {
return this.schemaRoot;
}
try
{
log.log(Level.FINER,"start get schema root call");
Attributes SSSE;
SSSE = this.ctx.getAttributes("", new String[]{subschemaAttributeName});
if (SSSE != null && SSSE.get(subschemaAttributeName) != null) {
this.schemaRoot = (String) SSSE.get(subschemaAttributeName).get();
}
log.log(Level.FINER,"schema root read as being: '" + String.valueOf(this.schemaRoot) + "'");
}
catch (NamingException e)
{
// revert to using good old 'cn=schema' ...
}
if (this.schemaRoot == null )
{
log.log(Level.FINER,"forcing value of schema root to 'cn=schema', since can't read subschema attribute name");
this.schemaRoot = "cn=schema"; // default: this is what it usually is anyway... :-)
}
return this.schemaRoot;
}
/**
* <p>HERE BE DRAGONS</p>
*
* <p>Similarly to jndi, we impose a structure on the raw schema entry. In the
* directory, there is a single schema entry, with a number of multi-valued attributes, e.g.
* one attribute for 'objectClasses', one for 'ldapSyntaxes'. Each attribute value has
* a wretched format, e.g. an objectClasses value might be:
* " ( 0.9.2342.19200300.100.4.4 NAME 'newPilotPerson' SUP ( person )
* STRUCTURAL MAY ( uid $ mail $ drink $ roomNumber $ userClass $ homePhone $ homePostalAddress
* $ secretary $ personalTitle $ preferredDeliveryMethod $ businessCategory $ janetMailbox
* $ otherMailbox $ mobile $ pager $ organizationalStatus $ mailPreferenceOption $ personalSignature ) ) "</p>
*
* <p>We break this up by adding an extra layer of virtual attributes, turning the above attribute
* value into an attributes object (with a 'MAY' attribute, a 'NAME' attribute etc. ...
* (If you need the real deal, use getRawSchema()... but be sure
* you know what you're doing :-). </p>
*
* @param entryName the name of an entry, e.g. schema=cn,schema=attributeTypes
*/
public Attributes getAttributes(String entryName)
throws NamingException
{
entryName = mangleEntryName(entryName);
BasicAttributes schemaAttributes = new BasicAttributes(); // add fake object class to keep some DXattributes routines happy...
schemaAttributes.put(schemaObjectClassAttribute);
if (entryName == null || entryName.length() == 0) // return synthetic entry for syntax root
{
schemaAttributes.put(subschemaAttributeName, this.schemaRoot);
}
else if (entryName.indexOf(',') == -1 && entryName.indexOf('/') == -1) // return synthetic entry for syntax type headings
{
String schemaType = entryName.substring(entryName.indexOf('=')+1);
schemaAttributes.put("schemaType", schemaType);
}
else
{
schemaAttributes = getAttributesFromSchemaName(entryName);
}
return schemaAttributes;
}
/**
* <p>This method does three things; firstly, it trims the schema root from
* the name if present (e.g. trims ",cn=schema" in most cases).
* Secondly, it trims any ';binary' from the end of the string.
* Finally, it translates the pseudo-schema names jndi imposes on top of the
* standard ldap/X500 syntax names from rfc 2252/2256:</p>
*<ul>
* <li>AttributeDefinition => attributeTypes
* <li>ClassDefinition => objectClasses
* <li>SyntaxDefinition => ldapSyntaxes
* </ul>
*
* @param entryName : schema=ClassDefinition,cn=schema or schema=cn,schema=attributeTypes
* @return
*/
protected String mangleEntryName(String entryName)
{
if (entryName.indexOf("ClassDefinition")>-1) {
entryName = entryName.replaceAll("(ClassDefinition)", "objectClasses");
}
if (entryName.indexOf("SyntaxDefinition")>-1) {
entryName = entryName.replaceAll("(SyntaxDefinition)", "ldapSyntaxes");
}
if (entryName.indexOf("AttributeDefinition")>-1) {
entryName = entryName.replaceAll("(AttributeDefinition)", "attributeTypes");
}
// if it is an ldap name, restructure it to the schema=..., schema=... used in JX.
if (entryName.indexOf('/') > 0)
{
// trim ;binary for prettiness...
// TODO: Is this such a good idea?; it may mess up some directories such as slapd...
int pos = entryName.indexOf(";binary");
if (pos > -1) {
entryName = entryName.substring(0, pos);
}
int slashpos = entryName.indexOf('/');
String topLevelName = entryName.substring(0, slashpos);
String specificName = entryName.substring(++slashpos);
return "schema=" + specificName + ",schema="+topLevelName;
}
// otherwise it is already a JX style name, so we clean it up a bit to get it in a standard form
// trim the schema root off the end, since we're only interested in the next level (objectclasses etc.)
int pos = entryName.indexOf(this.schemaRoot);
if (pos > 0) {
entryName = entryName.substring(0, pos-1);
}
// a little naughtily, we often use 'cn=schema' as shorthand for the schema root... get rid of that instead if it is different from the schema root (usually it isn't)
pos = entryName.indexOf("cn=schema");
if (pos > 0) {
entryName = entryName.substring(0, pos-1);
}
return entryName;
}
/**
* returns the specific schema entry name - eg 'cn' in 'schema=cn,schema=attributeTypes'
* @param entryName the schema entry DN in JX format - 'schema=cn,schema=attributeTypes'
* @return the specific schema name - e.g. 'cn'
* @throws NamingException
*/
protected String getSpecificName(String entryName)
throws NamingException
{
int equalpos = entryName.indexOf('=')+1;
int commapos = entryName.indexOf(',');
if (equalpos <= 0 || commapos == -1 || equalpos>commapos) {
throw new NamingException("error parsing schema dn '" + entryName + "' ");
}
return entryName.substring(equalpos, commapos);
}
/**
* returns the specific schema entry name - eg 'cn' in 'schema=cn,schema=attributeTypes'
* @param entryName the schema entry DN in JX format - 'schema=cn,schema=attributeTypes'
* @return the specific schema name - e.g. 'cn'
* @throws NamingException
*/
protected String getTypeName(String entryName)
throws NamingException
{
if (entryName.endsWith(",cn=schema")) {
entryName = entryName.substring(0, entryName.length()-10);
}
int equalpos = entryName.lastIndexOf('=')+1;
return entryName.substring(equalpos);
}
/**
* This looks up the raw 'Attribute' corresponding to the full entryName. It then takes the value
* of that attribute, and creates a new Attributes object by parsing that value.
* @param entryName
* @throws NamingException
*/
// package visibility for testing
BasicAttributes getAttributesFromSchemaName(String entryName) throws NamingException
{
if (this.rawSchemaAttributes == null) {
return null;
}
entryName = mangleEntryName(entryName);
// do all this the slow way for now...
String schemaTypeName = getTypeName(entryName);
String specificName = getSpecificName(entryName);
Attribute schemaGroup = this.rawSchemaAttributes.get(schemaTypeName);
if (schemaGroup == null)
{
// some wierdo directories manage to get their cases muddled. This is a last-gasp attempt
// to read them by using an all-lower case version of the name.
schemaGroup = this.rawSchemaAttributes.get(schemaTypeName.toLowerCase());
throw new NamingException("Unable to find schema entry for schema type '" + schemaTypeName + "'");
}
NamingEnumeration schemaValues = schemaGroup.getAll();
String schemaValue;
while (schemaValues.hasMore())
{
schemaValue = (String) schemaValues.next();
String[] names = getNames(schemaValue);
for (int i=0; i<names.length; i++)
{
// use case-insensitive match to cope with weirdo directories that muddle case
if (specificName.equalsIgnoreCase(names[i]))
{
return getAttributesFromSchemaValue(schemaValue);
}
}
}
return null;
}
/**
* <p>This Parses the schema values from the syntaxValue. The parser is very simple,
* and this might cause trouble in future years. The assumptions are:</p>
* <ul>
* <li> syntax keywords are all in upper case (e.g. NAME, DESC etc.).
* <li> keywords are all followed by a list of values in mixed case.
* <li> all values are more than one character long.
* <li>
* </ul>
*/
// package visibility for testing
/**
* A quicky parse routine to spin through a list ( 'fred' 0.2.3.4 'nigel' 'horse heads' ) fnord fnord fnord ( 'more fnords' )
* adding elements to the passed attribute until it hits the first ')'
* @param schemaAttribute attribute to add the bracketed strings to.
* @param st a string tokeniser to read values from.
*/
private void addBracketedValues(Attribute schemaAttribute, StringTokenizer st)
{
while (st.hasMoreTokens())
{
String token = st.nextToken();
if (token.endsWith(")")) // that's all for this list...
{
if (token.length()>1) {
schemaAttribute.add(getQuotedTokens(token.substring(0, token.length()-1), st));
}
return;
}
schemaAttribute.add(getQuotedTokens(token, st));
}
}
/**
* If the token has an opening ' character, this will read either it or a sequence until
* it gets to the closing ' - otherwise it just returns the token.
* @param token
* @return
*/
private String getQuotedTokens(String token, StringTokenizer st)
{
if (token.charAt(0) != '\'') {
return token;
}
if (token.length() < 2) {
return token;
}
if (token.charAt(0) == '\'' && token.charAt(token.length()-1) == '\'') {
return token.substring(1, token.length()-1);
}
// string of quoted text... this would be so much easier in perl. sigh.
StringBuffer returnText = new StringBuffer(token.substring(1));
while (st.hasMoreTokens())
{
token = st.nextToken();
if (token.endsWith("'")) {
return (returnText.append(" ").append(token.substring(0, token.length()-1)).toString());
}
else {
returnText.append(" ").append(token);
}
}
return returnText.toString(); // someone forgot the closing quote I guess...
}
/**
* This parses an attribute schema string such as: "( 2.5.6.2 NAME 'country' SUP ( top ) STRUCTURAL MUST ( c ) MAY ( description $ searchGuide ) )"
* and breaks it up into a pseudo attribute with elements such as 'NAME'->'country' and 'MAY' -> 'description', 'searchGuide'
* @param syntaxValue the string of syntax details as per rfc 2252
* @return the pseudo attribute, suitable for display.
*/
BasicAttributes getAttributesFromSchemaValue(String syntaxValue)
{
BasicAttributes schemaValues = new BasicAttributes(); // add fake object class to keep some DXattributes routines happy...
schemaValues.put(schemaObjectClassAttribute);
StringTokenizer st = new StringTokenizer(syntaxValue, " \t\n\r\f$");
// Special Handling for first OID case
if (st.hasMoreTokens())
{
String oid = st.nextToken();
if (oid.startsWith("(")) // can be a stray opening '('.
{
if (oid.length()==1) {
oid = st.nextToken();
}
else {
oid = oid.substring(1); // handle case where there is no space between ( and Oid.
}
}
schemaValues.put(new BasicAttribute("OID", oid));
}
while (st.hasMoreTokens())
{
String attributeID = st.nextToken();
if (attributeID.endsWith(")") == false) // stray closing ')' is possible (see above)
{
addAttribute(schemaValues, attributeID, st);
}
else
{
if (attributeID.length()>1) {
addAttribute(schemaValues, attributeID.substring(1), st);
}
}
}
return schemaValues;
}
/**
* Parses the current attribute from the schema string. NB - this method will recurse if two
* attribute keyword tokens are discovered next to each other.
* @param schemaValues - the Attributes object to add the schema Attribut<b>e</b> objects to.
* @param attributeName - the name of the new Attribut<b>e</b> object to construct
* @param st the token list to read the attribute data from
*/
private void addAttribute(Attributes schemaValues, String attributeName, StringTokenizer st)
{
BasicAttribute schemaAttribute = new BasicAttribute(attributeName);
schemaValues.put(schemaAttribute);
if (st.hasMoreTokens())
{
String token = st.nextToken();
if (token.startsWith("("))
{
if (token.length()>1)
{
if (token.endsWith(")") == true) // pathalogical case: "(VALUE)"
{
token = token.substring(0, token.length()-1);
schemaAttribute.add(token.substring(1));
}
else
{
schemaAttribute.add(token.substring(1));
addBracketedValues(schemaAttribute, st);
}
}
else {
addBracketedValues(schemaAttribute, st);
}
}
else if (token.endsWith(")"))
{ // do nothing - this should be the very end of the string tokenizer list, and this the left over bit at the end.
// (note this is *not* the match to the "{" case above...!)
}
else if (isSyntaxKeyword(token) == true)
{
addAttribute(schemaValues, token, st);
}
else
{
token = getQuotedTokens(token, st);
schemaAttribute.add(token);
}
}
}
/**
* Read the next block of text in a single quoted block, e.g. DESC 'some stuff here',
* (assuming that the first quoted string has already been read before the string
* tokenizer has been passed in, e.g. this should get "stuff here'", and return
* "stuff here".
* @param st a string tokenizer with a series of tokens one of which ends in a single quote
* @return the concatenated string of all tokens up to and including the one suffixed with a single quote.
*/
private static String readQuoteBlock(StringTokenizer st)
{
StringBuffer returnBuffer = new StringBuffer();
while (st.hasMoreTokens())
{
String token = st.nextToken();
returnBuffer.append(" ");
returnBuffer.append(token);
if (token.endsWith("'"))
{
returnBuffer.deleteCharAt(returnBuffer.length()-1); // remove final quote
return returnBuffer.toString();
}
}
log.log(Level.FINER,"unexpected end of schema text in single quoted block");
return returnBuffer.toString();
}
/**
* This <i>should</i> probably have a list of keywords extracted from rfc 2252... but I can't be
* bothered, so I'll simply test that the token is entirely uppercase, alphabetic characters...
* @param token the token to test for being an rfc 2252 syntax keyword.
* @return
*/
private static boolean isSyntaxKeyword(String token)
{
String [] reservedKeywords = {
"ABSTRACT",
"APPLIES",
"AUXILIARY",
"COLLECTIVE",
"DESC",
"EQUALITY",
"MAY",
"MUST",
"NAME",
"NO-USER-MODIFICATION",
"OBSOLETE",
"ORDERING",
"SINGLE-VALUE",
"STRUCTURAL",
"SUBSTR",
"SUP",
"SYNTAX",
"USAGE" };
int size = reservedKeywords.length;
for (int i=0; i<size; i++) {
if (reservedKeywords[i].equals(token)) {
return true;
}
}
if (token.startsWith("X-")) {
return true;
}
return false; // probably isn't - but they might change the standard I suppose. Oh well...
}
/* simply returning true for capitalised is too simplistic...
int i, len = token.length();
char c;
for (i=0; i<len; i++)
{
c = token.charAt(i);
if (c<'A' || c>'Z')
return false;
}
return true;
*/
/**
* <p>Returns the next level in our virtual view of the schema.
* The virtual view may either be in the jndi form (e.g. "objectClass/person")
* of a 'dn like form' (e.g. "schema=objectClass, schema=person"). If it is
* the latter, the 'fake attribute type' is ignored, e.g. "fnordle=objectClass,
* snork=person" would resolve the same as "schema=objectClass, schema=person").</p>
* @param entryName the full schema name to get the next level of: e.g. "schema=objectClass"
* @return the undecorated names of the next level (e.g. {'person', 'top', 'organisation'}
*/
public ArrayList listEntryNames(String entryName)
throws NamingException
{
if (this.rawSchemaAttributes == null) {
return new ArrayList();
}
entryName = mangleEntryName(entryName);
ArrayList schemaNames;
if (entryName == null || entryName.length() == 0 || entryName.equals("cn=schema") || entryName.equals(this.schemaRoot)) // The 'root node', i.e. the top of the schema tree - returns things like
{ // 'objectClasses', 'ldapSyntaxes', 'attributeTypes'
schemaNames = new ArrayList(10);
Enumeration schemaTopLevelNames = this.rawSchemaAttributes.getIDs();
while (schemaTopLevelNames.hasMoreElements())
{
String name = (String)schemaTopLevelNames.nextElement();
if(!schemaNames.contains(name)) {
schemaNames.add(name);
}
}
}
else if (entryName.indexOf(',') == -1 && entryName.indexOf('/') == -1) // the first layer - returns things like
{ // 'person', 'orgunit', 'newPilotPerson' etc...
schemaNames = new ArrayList(1000);
if (entryName.indexOf('=') > 0) {
entryName = entryName.substring(entryName.indexOf('=')+1);
}
Attribute rawSyntaxAttribute = this.rawSchemaAttributes.get(entryName); // entryName might be 'attributeTypes'
if (rawSyntaxAttribute == null) {
throw new NamingException("unable to list syntaxes of type '" + entryName + "'");
}
Enumeration values = rawSyntaxAttribute.getAll();
String[] names;
while (values.hasMoreElements())
{
names = getNames((String) values.nextElement());
for (int i = 0; i < names.length; i++)
{
if(!schemaNames.contains(names[i])) {
schemaNames.add(names[i]);
}
}
}
}
else // double element, e.g. objectClass/person -> never has children.
{
schemaNames = new ArrayList(0);
}
return schemaNames;
}
// note kind-a-sad attempt to make this method fastish.
private int name_pos, bracket_pos, quote_pos, last_pos, pos; // pointers for string parsing.
/**
* This strips the OID from a schema attribute description string. The OID
* is assumed to be the first element after an optional '(' character.
* @param ldapSchemaDescription
* @return the OID string ('1.2.3.4' etc.) - or '0' if ldapSchemaDescription is null or unknown
*/
// package visibility for testing
final String getOID(String ldapSchemaDescription)
{
if (ldapSchemaDescription == null) {
return "0"; // error.
}
int start = 0;
if (ldapSchemaDescription.charAt(0) == '(') {
start++;
}
while (ldapSchemaDescription.charAt(start)==' ') {
start++;
}
try
{
int endpos = ldapSchemaDescription.indexOf(' ', start);
if (endpos == -1) {
endpos = ldapSchemaDescription.indexOf(')', start);
}
if (endpos == -1) {
endpos = ldapSchemaDescription.length();
}
String ret = ldapSchemaDescription.substring(start, endpos);
return ret;
}
catch(Exception e)
{
log.log(Level.WARNING,"can't parse '" + ldapSchemaDescription + "'");
e.printStackTrace();
return "0";
}
}
/**
* parse strings that may be of the form either:
* ????????????????/ NAME 'myname' ???????????
* ????????????????/ NAME ('firstname', 'secondname', 'thirdname') ???????????
* @return the single name, or the first of the array of names, as a string.
*/
// IMP note - for speed, this is implemented separately from getNames()
final String getFirstName(String ldapSchemaDescription)
{
this.name_pos = ldapSchemaDescription.indexOf("NAME");
if (this.name_pos == -1) {
this.name_pos = ldapSchemaDescription.indexOf("DESC"); // for ldapSyntaxes entries
}
if (this.name_pos == -1) // fall back - should never happen; try to return OID
{
if (ldapSchemaDescription.startsWith("{")) {
ldapSchemaDescription = ldapSchemaDescription.substring(1).trim();
}
this.pos = ldapSchemaDescription.indexOf(' ');
if(this.pos == -1)
{
log.log(Level.WARNING,"unable to get name from " + ldapSchemaDescription);
return "syntax_error";
}
return ldapSchemaDescription.substring(0, this.pos).trim();
}
this.quote_pos = ldapSchemaDescription.indexOf('\'', this.name_pos);
this.quote_pos++;
this.last_pos = ldapSchemaDescription.indexOf('\'', this.quote_pos);
if (this.quote_pos != 0 && this.last_pos != -1) {
return ldapSchemaDescription.substring(this.quote_pos, this.last_pos);
}
else
{
log.log(Level.WARNING,"unable to parse " + ldapSchemaDescription);
return "syntax_error";
}
}
/**
* parse strings that may be of the form either:
* ????????????????/ NAME 'myname' ???????????
* ????????????????/ NAME ('firstname', 'secondname', 'thirdname') ???????????
* @return the Name or array of names, as an array of strings 1 or more elements long.
*/
// package visibility for testing
final String[] getNames(String ldapSyntaxDescription)
{
try
{
this.name_pos = ldapSyntaxDescription.indexOf("NAME");
if (this.name_pos == -1) {
this.name_pos = ldapSyntaxDescription.indexOf("DESC"); // for ldapSyntaxes entries
}
if (this.name_pos == -1) // fall back - should never happen; try to return OID
{
if (ldapSyntaxDescription.startsWith("{")) {
ldapSyntaxDescription = ldapSyntaxDescription.substring(1).trim();
}
return new String[] {ldapSyntaxDescription.substring(0, ldapSyntaxDescription.indexOf(' ')).trim()};
}
this.bracket_pos = ldapSyntaxDescription.indexOf('(', this.name_pos);
this.quote_pos = ldapSyntaxDescription.indexOf('\'', this.name_pos);
if (this.bracket_pos != -1 && this.bracket_pos < this.quote_pos) // multiple names...
{
this.bracket_pos = ldapSyntaxDescription.indexOf(')', this.bracket_pos); // get end bracket pos
ArrayList newList = new ArrayList(5);
while (this.quote_pos < this.bracket_pos && this.quote_pos != -1) // iterate through grabbing 'quoted' substrings until we get to the end of the bracketed expression
{
int start = ++this.quote_pos;
this.quote_pos = ldapSyntaxDescription.indexOf('\'', this.quote_pos);
String temp = ldapSyntaxDescription.substring(start, this.quote_pos);
newList.add(temp);
this.quote_pos++;
this.quote_pos = ldapSyntaxDescription.indexOf('\'', this.quote_pos);
}
return (String[]) newList.toArray(new String[]{});
}
else // return the single name
{
this.quote_pos++;
int next_quote = ldapSyntaxDescription.indexOf('\'', this.quote_pos);
String temp = ldapSyntaxDescription.substring(this.quote_pos, next_quote);
return new String[]{temp};
}
}
catch (StringIndexOutOfBoundsException e)
{
log.log(Level.WARNING,"unable to parse line: " + ldapSyntaxDescription, e);
return new String[]{"syntax_error"};
}
}
/**
* Takes a DXAttributes set representing attribute schema defs,
* and translates the oids into human friendly strings...
*/
/*
protected Attributes addAttributeInfo(Attributes attdefs)
{
try
{
Attribute syntax = attdefs.get("SYNTAX"); // get syntax attribute
String oid = syntax.get().toString(); // convert oid value to string
if (oid.indexOf('{') > -1)
oid = oid.substring(0, oid.indexOf('{'));
String syntaxdesc = (String) oids.get(oid); // look up description for oid
attdefs.put("(SYNTAX-DESC)", syntaxdesc); // stick in synthetic attribute
return attdefs;
}
catch (NamingException e) { return attdefs; }
catch (NullPointerException e) { return attdefs; }
}
*/
/**
* This returns a sorted list of all known object classes, as read from the schema
* @return an array list of strings
* @throws NamingException
*/
public ArrayList objectClasses()
throws NamingException
{
if (this.fullObjectClassArray == null)
{
ArrayList temp = listEntryNames("schema=objectClasses,cn=schema");
if (temp==null) {
throw new NamingException("unable to read list of object classes from schema");
}
String[] OCs = (String[]) temp.toArray(new String[temp.size()]);
Arrays.sort(OCs, new Comparator() {
public int compare(Object a, Object b) { return ((String)a).compareToIgnoreCase((String)b); }
public boolean equals(Object a, Object b) { return ((String)a).equalsIgnoreCase((String)b); }
});
int size = OCs.length;
this.fullObjectClassArray = new ArrayList(size);
for (int i=0;i<size; i++) {
this.fullObjectClassArray.add(i, OCs[i]);
}
}
return this.fullObjectClassArray;
}
/**
* This returns a sorted list of all known attribute names, as read from the schema
* @return an array list of strings
* @throws NamingException
*/
public ArrayList attributeNames()
throws NamingException
{
if (this.fullAttributeNameArray == null)
{
ArrayList temp = listEntryNames("schema=attributeTypes,cn=schema");
if (temp==null) {
throw new NamingException("unable to read list of attribute types from schema");
}
String[] ATs = (String[]) temp.toArray(new String[temp.size()]);
Arrays.sort(ATs, new Comparator() {
public int compare(Object a, Object b) { return ((String)a).compareToIgnoreCase((String)b); }
public boolean equals(Object a, Object b) { return ((String)a).equalsIgnoreCase((String)b); }
});
int size = ATs.length;
this.fullAttributeNameArray = new ArrayList(size);
for (int i=0;i<size; i++) {
this.fullAttributeNameArray.add(i, ATs[i]);
}
}
return this.fullAttributeNameArray;
}
/**
* Gets a list of the object classes most likely
* to be used for the next Level of the DN...
* @param dn the dn of the parent to determine likely
* child object classes for
* @return list of recommended object classes...
*/
public ArrayList getRecommendedObjectClasses(String dn)
{
try
{
if ((dn!=null))
{
Attributes atts = this.ctx.getAttributes(dn);
if (atts == null)
{
log.log(Level.WARNING,"error reading object classes for " + dn );
}
else
{
Attribute objectClasses = atts.get("objectclass");
if (objectClasses == null) {
objectClasses = atts.get("objectClass");
}
if (objectClasses == null) {
objectClasses = atts.get("oc");
}
if (objectClasses == null) // aargh! Give up.
{
log.log(Level.WARNING,"unable to recognize object classes for " + dn);
}
else
{
NamingEnumeration names = objectClasses.getAll();
if (names == null) {
log.log(Level.WARNING,"object class has no attributes!");
}
ArrayList returnArray = new ArrayList(10);
while (names.hasMore()) {
returnArray.add(names.next());
}
return returnArray;
}
}
}
}
catch (NamingException e)
{
log.log(Level.WARNING,"error reading object classes for " + dn +"\n internal error III: ", e);
}
return null;
}
/**
* Gets a list of all attribute definitions that have a 'binary' syntax
* (currently defined as: SYNTAX = 1.3.6.1.4.1.1466.115.121.1.5 or 1.3.6.1.4.1.1466.115.121.1.40)
* @return list of space separated attribute names as a string array - used to set ctx.addToEnvironment("java.naming.ldap.attributes.binary", -String- );
*/
// Performance note - this might be sped up a tad if required...
public String getNewBinaryAttributes()
{
if (this.rawSchemaAttributes == null) {
return "";
}
try
{
StringBuffer binaryAttributeList = new StringBuffer(1000);
Attribute rawSyntaxAttribute = this.rawSchemaAttributes.get("attributeTypes");
if (rawSyntaxAttribute == null) {
return "";
}
NamingEnumeration values = rawSyntaxAttribute.getAll();
while (values.hasMore())
{
String attributeDescription = (String)values.next(); // something like "( 1.3.6.1.4.1.453.7.6.2.1 NAME 'mhsX400Domain' SYNTAX 1.3.6.1.4.1.1466.115.121.1.5 )"
if ((attributeDescription.indexOf("1.3.6.1.4.1.1466.115.121.1.5 ")>0) ||
(attributeDescription.indexOf("1.3.6.1.4.1.1466.115.121.1.40")>0))
{
String [] names = getNames(attributeDescription);
for (int i=0; i<names.length; i++)
{
binaryAttributeList.append(names[i]);
binaryAttributeList.append(' ');
}
}
}
return binaryAttributeList.toString();
}
catch (NamingException e)
{
log.log(Level.WARNING,"unable to get binary attributes from schema", e);
return "";
}
/*
try
{
NamingEnumeration results = schemaOps.search("", "(|(SYNTAX = 1.3.6.1.4.1.1466.115.121.1.5)(SYNTAX = 1.3.6.1.4.1.1466.115.121.1.40))", new String [] {"name"}, constraints);
while (results.hasMore())
{
// yuck. Basically keep processing the search result until it will give us the name result.
ret.append(" ");
ret.append(((SearchResult)results.next()).getAttributes().get("name").get().toString());
}
}
catch (NamingException e) { CBUtility.log("unable to enumerate schema search results" + e, 1); }
catch (Exception e) { CBUtility.log("error in syntax search " + e, 1); }
return ret.toString();
*/
}
/**
* Finds the syntax of the corresponding attribute
* @param attID the undecorated attribute name - e.g. 'commonName'
* @return returns the attribute syntax, or null if not found.
*/
public String getAttributeSyntax(String attID)
{
if (attID.indexOf(';') > 0) {
attID = attID.substring(0, attID.indexOf(';')); //TE: for example: userCertificate;binary.
}
return schemaLookup("schema="+attID+",schema=attributeTypes", "SYNTAX");
}
/**
* Looks up a particular value in a particular schema attribute
* @param entryName the entry to lookup; e.g. 'schema=person, schema=objectClass'
* @param schemaAttribute the actual field to look up, e.g. "DESC"
* @return the looked up value (or the first one found, if multiple)
*/
public String schemaLookup(String entryName, String schemaAttribute)
{
entryName = mangleEntryName(entryName);
try
{
Attributes schemaAtts = getAttributes(entryName);
Attribute schemaAtt = schemaAtts.get(schemaAttribute);
String att = (String)schemaAtt.get();
return att;
}
catch (NamingException e)
{
log.log(Level.WARNING,"unable to get value for " + entryName + " value: " + schemaAttribute, e);
}
catch (NullPointerException e2)
{
if ("DESC".equals(schemaAttribute) == false) {
log.log(Level.WARNING,"unable to read any schema entry for " + entryName + "and attribute: " + schemaAttribute, e2);
}
}
return null;
}
public String getNameOfObjectClassAttribute()
{
return schemaLookup("schema=objectClass,schema=attributeTypes", "NAME");
}
/**
* Tries to determine if the attribute is a SINGLE-VALUE attribute.
* The rawSchemaAttributes Attributes object represents each attribute
* similar to
* <br><br>
* ( 1.3.6.1.4.1.3327.77.4.1.2 NAME 'uNSPSCTitle' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )
* <br><br>
* This method gets each value (see above string) in the attribute, then each of their names in turn
* until the name of the attribute we are seeking is found. Then checks if 'SINGLE-VALUE'
* is present in that value.
* @param name the name of the attribute, for example uNSPCSTitle.
* @return true if the attribute is a SINGLE-VALUE attribute, false otherwise.
*/
public boolean isAttributeSingleValued(String name)
{/* TE */
if (this.rawSchemaAttributes == null) {
return false;
}
try
{
Attribute attributeTypes = this.rawSchemaAttributes.get("attributeTypes");
NamingEnumeration enumer = attributeTypes.getAll();
while(enumer.hasMore())
{
String attr = (String)enumer.next();
String[] attrName = getNames(attr);
for(int i=0;i<attrName.length;i++) {
if(attrName[i].equals(name) && (attr.indexOf("SINGLE-VALUE") >= 0)) {
return true;
}
}
}
}
catch(NamingException e)
{
log.log(Level.WARNING,"Unable to determine if attribute '" + name + "' is SINGLE-VALUE." + e);
}
return false;
}
}