/* * Copyright (c) 2004, Stefan Walter * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above * copyright notice, this list of conditions and the * following disclaimer. * * Redistributions in binary form must reproduce the * above copyright notice, this list of conditions and * the following disclaimer in the documentation and/or * other materials provided with the distribution. * * The names of contributors to this software may not be * used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH * DAMAGE. * * * CONTRIBUTORS * Stef Walter * */ package com.memberwebs.ldapxml; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Enumeration; import java.util.Hashtable; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.Vector; import org.w3c.dom.Document; import org.w3c.dom.Element; import com.memberwebs.ldapxml.helpers.LXDefaultConvert; import com.memberwebs.ldapxml.helpers.LXDefaultHook; import com.memberwebs.ldapxml.map.*; import com.memberwebs.ldapxml.map.LXAttribute; import com.memberwebs.ldapxml.map.LXBase; import com.memberwebs.ldapxml.map.LXClass; import com.memberwebs.ldapxml.map.LXEntry; import com.novell.ldap.LDAPAttribute; import com.novell.ldap.LDAPAttributeSchema; import com.novell.ldap.LDAPCompareAttrNames; import com.novell.ldap.LDAPConnection; import com.novell.ldap.LDAPEntry; import com.novell.ldap.LDAPException; import com.novell.ldap.LDAPObjectClassSchema; import com.novell.ldap.LDAPSchema; import com.novell.ldap.LDAPSearchConstraints; import com.novell.ldap.LDAPSearchResults; /** * Uses an LX map to read data from an LDAP directory, returning * XML in a DOM format. * * @author stef@memberwebs.com * @version 0.5 */ public class LXReader { private static final String CLASS = "objectClass"; /** * Creates a new LXReader object. */ public LXReader() { m_map = null; m_connection = null; m_hooks = new Hashtable(); m_convert = new LXDefaultConvert(); } /** * Get the map that will be used to transform data. * * @return The map. */ public final LXMap getMap() { return m_map; } public final void setConvert(LXConvert convert) { m_convert = convert; } public final LXConvert getConvert() { return m_convert; } /** * Set the LX map that will be used to transform data * retrieved from the LDAP directory. * * @param map The map. */ public final void setMap(LXMap map) throws LXException { if(map == null) throw new LXException("Must supply a valid loaded map"); m_map = map; } /** * Get the LDAP connection used to retrieve data. * * @return The connection. */ public final LDAPConnection getConnection() { return m_connection; } /** * Set the LDAP connection to retrieve data from. * * @param conn The connection. */ public final void setConnection(LDAPConnection conn) throws LXException { if(!conn.isConnected()) throw new LXException("Must supply a valid open connection"); // Force a refresh of the schema as connection // has changed. m_schema = null; m_connection = conn; } /** * Search for and retrieve data matching a filter. * * @param doc The document from which to create elements. * @param base Point in the LDAP tree to root search. * @param filter The search filter. * @return An array of retrieved elements, one for each LDAP * entry found. */ public LXResults retrieveSearch(Document doc, String base, String filter) throws LXException, LDAPException { return retrieveSearch(doc, base, filter, new LXSpecs(), false); } public LXResults retrieveSearch(Document doc, String base, String filter, LXSpecs specs) throws LXException, LDAPException { return retrieveSearch(doc, base, filter, specs, false); } /** * Search for and retrive data matching filter with additional * retrieval specifications. * * @param doc The document from which to create elements. * @param base The point in the LDAP tree to root search. * @param filter The search filter. * @param batch Whether the retrieval is called in a batch * and should update start and limit in specs * @return An array of retrieved DOM elements, one for each LDAP * entry found. */ public LXResults retrieveSearch(Document doc, String base, String filter, LXSpecs specs, boolean batch) throws LXException, LDAPException { checkInternals(); String[] attrs = getAttributes(specs); String[] sort = specs.getMappedSort(m_map); int start = specs.getStart(); int last = specs.getLimit() + start; LDAPSearchConstraints cons = new LDAPSearchConstraints(); cons.setMaxResults(0); // Search tree for entries LDAPSearchResults results = m_connection.search(base, LDAPConnection.SCOPE_SUB, specs.getFilter(filter), attrs, false, cons); List res = new ArrayList(); if(results.hasMore()) { while(results.hasMore()) res.add(results.next()); if(sort != null && sort.length > 0) Collections.sort(res, new LDAPCompareAttrNames(sort, specs.getSortDirection())); } // Now retrieve each Vector els = new Vector(); int number = 0; int skipped = 0; int retrieved = 0; Iterator it = res.iterator(); // Idle through entries till we find the one we're supposed to start at while(number < start && it.hasNext()) { it.next(); number++; skipped++; } // Now read each element in while(it.hasNext() && number <= last) { LDAPEntry entry = (LDAPEntry)it.next(); Element el = retrieveEntry(doc, entry, specs); // And retrieve sub elements if neccessary if(el != null) { retrieveSubTree(doc, entry.getDN(), specs, el, specs.getDepth(), attrs); els.add(el); } number++; retrieved++; } if(batch) { start -= skipped; specs.setStart(start < 0 ? 0 : start); int limit = specs.getLimit(); limit -= retrieved; specs.setLimit(retrieved < 0 ? 0 : limit); } // Create the root element etc... Element root = createElement(doc, m_map); return new LXResults(doc, els, root); } /** * Retrieve a blank result set, with a properly * created document, root node etc... * * @param doc The document from which to create elements. * @return The blank result set. */ public LXResults retrieveBlank(Document doc) throws LXException { if(m_map == null) throw new LXException("Must supply a valid loaded map"); Element root = createElement(doc, m_map); return new LXResults(doc, new Vector(), root); } /** * Retrieves a single entry from an LDAP tree. * * @param doc The document from which to create elements. * @param dn The LDAP DN of the entry to retrieve. * @return The DOM element or null if not found. */ public Element retrieveEntry(Document doc, String dn) throws LXException, LDAPException { return retrieveEntry(doc, dn, new LXSpecs()); } /** * Retrieves a single entry from an LDAP tree with additional * specifications. * * @param doc The document from which to create elements. * @param dn The LDAP DN of the entry to retrieve. * @param specs The additional retrieval specifications. * @return The DOM element or null if not found. */ public Element retrieveEntry(Document doc, String dn, LXSpecs specs) throws LXException, LDAPException { return retrieveEntry(doc, dn, null, specs); } /** * Retrieves a single entry from an LDAP tree with additional * specifications. * * @param doc The document from which to create elements. * @param dn The LDAP DN of the entry to retrieve. * @param query An ldap query to filter the entry with. * @param specs The additional retrieval specifications. * @return The DOM element or null if not found. */ public Element retrieveEntry(Document doc, String dn, String query, LXSpecs specs) throws LXException, LDAPException { checkInternals(); String[] attrs = getAttributes(specs); // Get the entry LDAPSearchResults results = m_connection.search(dn, LDAPConnection.SCOPE_BASE, specs.getFilter(query), attrs, false); Element el = null; // If we got something then return it. if(results.hasMore()) { LDAPEntry entry = results.next(); el = retrieveEntry(doc, entry, specs); // And get sub elements if neccessary retrieveSubTree(doc, dn, specs, el, specs.getDepth(), attrs); } return el; } /** * Transform an already retrieved LDAP entry. * * @param doc The document from which to create elements. * @param entry The LDAP entry. * @return The DOM element or null if not found in map. */ public Element retrieveEntry(Document doc, LDAPEntry entry) throws LXException, LDAPException { return retrieveEntry(doc, entry, new LXSpecs()); } /** * Transform an already retrieved LDAP entry with additional specifications. * * @param doc The document from which to create elements. * @param entry The LDAP entry. * @return The DOM element retrieved or null if not found in map. */ public Element retrieveEntry(Document doc, LDAPEntry entry, LXSpecs specs) throws LXException, LDAPException { checkInternals(); // Make sure we have a copy of the server schema with us // We retrieve the whole thing for efficiency refreshSchema(); // Get all the classes we use them to determine a number // of things LDAPAttribute objectClasses = entry.getAttribute(CLASS); if(objectClasses == null) return null; String[] classes = objectClasses.getStringValueArray(); // Okay now we need to find out which entry it is. We use the // name attribute of an LXEntry in order to determine this LXEntry lxentry = null; for(int i = 0; i < classes.length; i++) { lxentry = m_map.getEntry(classes[i]); if(lxentry != null) break; } // We don't create elements not found in the map // so return null to signify this if(lxentry == null) return null; LXHook hook = getHookFor(lxentry, specs); // Call the hooks for this element if(!hook.prefix(entry)) return null; objectClasses = entry.getAttribute(CLASS); classes = objectClasses.getStringValueArray(); // Create the entry element Element el = createElement(doc, lxentry); // Okay go through all the objectClass attributes // and "do" those classes for(int i = 0; i < classes.length; i++) { String clsName = classes[i]; // Look up the class. We only do classes we can handle LXClass lxclass = lxentry.getClass(clsName); if(lxclass != null) { // If the class is not inline then create the class element Element clsEl = el; if(!lxclass.isInline()) { clsEl = createElement(doc, lxclass); el.appendChild(clsEl); } // Otherwise add the namespaces of the class to the parent // element as necessary else { addNamespace(lxclass, el); } if(lxclass.isInclusive()) { // Get out the actual LDAP objectClass LDAPObjectClassSchema objectClass = m_schema.getObjectClassSchema(clsName); if(objectClass == null) throw new LXException("invalid objectClass in schema: " + clsName); // Okay now go through the attributes and convert them // into XML retrieveAttributes(objectClass.getRequiredAttributes(), clsEl, entry, lxclass, specs); retrieveAttributes(objectClass.getOptionalAttributes(), clsEl, entry, lxclass, specs); } else { // Just get the attributes that were asked for Vector attrs = new Vector(); Enumeration e = lxclass.getChildNames(); while(e.hasMoreElements()) attrs.add(e.nextElement()); retrieveAttributes(toStringArray(attrs), clsEl, entry, lxclass, specs); } } } // Call the hooks for this element if(!hook.postfix(entry, el)) el = null; return el; } /** * Retrieve given attributes and add them to a DOM element as appropriate * * @param attributes Array of attributes to retrieve. * @param clsEl The parent class element. * @param entry The LDAP entry to retrieve attributes from. * @param lxcls The class these attributes belong to. * @param specs Additional retrieval specifications. */ private void retrieveAttributes(String[] attributes, Element clsEl, LDAPEntry entry, LXClass lxcls, LXSpecs specs) throws LXException { // We need this for later node creation Document doc = clsEl.getOwnerDocument(); // Get language stuff initialized String language = specs.getLanguage(); for(int i = 0; i < attributes.length; i++) { // Retrieve the attribute based on language LDAPAttribute ldapattr = getAttributeForLang(entry, attributes[i], language); // Each one can have multiple values LXAttribute lxattr = lxcls.getAttribute(attributes[i]); if(ldapattr != null && lxattr != null) { // Get the syntax for conversion functions LDAPAttributeSchema schattr = m_schema.getAttributeSchema(ldapattr.getBaseName()); String syntax = ""; if(schattr != null) syntax = schattr.getSyntaxString(); Enumeration values = ldapattr.getStringValues(); // XML child elements handled here. if(lxattr.isElement()) { // Just convert and append one child element // for each value. while(values.hasMoreElements()) { String val = m_convert.parse(ldapattr.getBaseName(), syntax, (String)values.nextElement()); if(val == null) continue; Element attrEl = createElement(doc, lxattr); attrEl.appendChild(doc.createTextNode(val)); clsEl.appendChild(attrEl); } } // XML Attributes handled here. else { // All values are concatenated with spaces // and set as the attribute. String value = ""; while(values.hasMoreElements()) { String val = m_convert.parse(ldapattr.getBaseName(), syntax, (String)values.nextElement()); if(val == null) continue; if(value.length() != 0) value += " "; value += val; } clsEl.setAttributeNS(lxattr.getNamespace(), lxattr.getXmlName(), value); addNamespace(lxattr, clsEl); } } } } /** * Helper function to create an element from an LX object. * Uses namespace information as appropriate. * * @param doc The document from which to create the element. * @param lx The LX object. * @return The DOM element. */ private Element createElement(Document doc, LXBase lx) { Element el = doc.createElementNS(lx.getNamespace(), lx.getXmlName()); addNamespace(lx, el); return el; } /** * Add namespace (xmlns) attribute to element as necessary. * * @param lx The LX object * @param el The element to add the attribute to */ private void addNamespace(LXBase lx, Element el) { // Passing true to getNamespace forces it // only to return namespaces for this LX object String nameSpace = lx.getNamespace(true); if(nameSpace != null) { String attr = "xmlns"; String prefix = lx.getPrefix(); if(prefix != null) attr += ":" + prefix; el.setAttributeNS("http://www.w3.org/2000/xmlns/", attr, nameSpace); } } /** * Refresh the cached LDAP schema if necessary. */ private void refreshSchema() throws LDAPException, LXException { refreshSchema(false); } /** * Refresh the cached LDAP schema. * * @param force Force a refresh even if not necessary. */ private void refreshSchema(boolean force) throws LDAPException, LXException { checkInternals(); if(m_schema == null || force) m_schema = m_connection.fetchSchema(m_connection.getSchemaDN()); } /** * Retrieve sub elements of an LDAP entry to a specified depth. * * @param doc Document to create elements from. * @param dn The parent LDAP entry. * @param specs Additional retrieval specifications. * @param el The parent DOM element. * @param depth The depth to go down in the tree. */ private void retrieveSubTree(Document doc, String dn, LXSpecs specs, Element el, int depth, String[] attrs) throws LXException, LDAPException { // Make sure we need to retrieve this level // If depth has gone down to zero, then no need if(el != null && depth > 0) { LDAPSearchConstraints cons = new LDAPSearchConstraints(); cons.setMaxResults(0); // Get everything at one level down LDAPSearchResults results = m_connection.search(dn, LDAPConnection.SCOPE_ONE, specs.getFilter(), attrs, false, cons); List res = new ArrayList(); while(results.hasMore()) res.add(results.next()); String[] sort = specs.getMappedSort(m_map); if(sort != null && sort.length > 0) Collections.sort(res, new LDAPCompareAttrNames(sort, specs.getSortDirection())); // Now retrieve each Iterator it = res.iterator(); while(it.hasNext()) { LDAPEntry entry = (LDAPEntry)it.next(); Element sub = retrieveEntry(doc, entry, specs); // And it's sub tree if(sub != null) { retrieveSubTree(doc, entry.getDN(), specs, sub, depth - 1, attrs); el.appendChild(sub); } } } } /** * Retrieves the hook for a given element. Caches the * hooks. When no hook is present returns a default hook. * * @param base The LX object to retrieve a hook for. */ private LXHook getHookFor(LXBase base, LXSpecs specs) throws LXException { String hook = base.getHook(); if(hook == null) hook = ""; LXHook hk = (LXHook)m_hooks.get(hook); if(hk == null) { try { if(hook.length() == 0) hk = new LXDefaultHook(); else hk = (LXHook)Class.forName(hook).newInstance(); } catch(ClassNotFoundException e) { } catch(IllegalAccessException e) { } catch(InstantiationException e) { } if(hk == null) throw new LXException("couldn't instantiate hook class: " + hook); hk.initialize(specs.getData()); m_hooks.put(hook, hk); } return hk; } /** * Helper to do some sanity checking on required calling * procedures. */ private final void checkInternals() throws LXException { if(m_connection == null || !m_connection.isConnected()) throw new LXException("Must supply a valid open connection"); if(m_map == null) throw new LXException("Must supply a valid loaded map"); } /** * Get the list of attributes required for a map and sort */ private String[] getAttributes(LXSpecs specs) { String[] sort = specs.getMappedSort(m_map); Set names = m_map.getNameSet(); if(names != null && sort != null) names.addAll(Arrays.asList(sort)); if(names != null) names.add(CLASS); if(names == null) { String[] ret = { "*", "+" }; return ret; } else { names.add("+"); return toStringArray(names); } } /** * The language functionality in the Novell LDAP classes doesn't * work as advertised. So this is a work around. * * @param entry The LDAP entry to retrieve the attribute from. * @param attr The attribute name. * @param language The language of the attribute. */ private static LDAPAttribute getAttributeForLang(LDAPEntry entry, String attr, String language) { // Retrieve the attribute based on language if(language == null) return entry.getAttribute(attr); else { LDAPAttribute ldapattr; if((ldapattr = entry.getAttribute(attr + ";" + language)) == null) ldapattr = entry.getAttribute(attr); return ldapattr; } } protected static String[] toStringArray(Collection c) { String[] a = new String[c.size()]; int j = 0; Iterator i = c.iterator(); while(i.hasNext()) a[j++] = i.next().toString(); return a; } // The LDAP connection we're using private LDAPConnection m_connection; // Cache of schema for the connection private LDAPSchema m_schema; // The LX map we're using to transform data private LXMap m_map; // Cache of all the loaded hooks so far private Hashtable m_hooks; // The converter private LXConvert m_convert; };