ldap.SearchUtility.java Source code

Java tutorial

Introduction

Here is the source code for ldap.SearchUtility.java

Source

/**
* Copyright (c) 2001-2012 "Redbasin Networks, INC" [http://redbasin.org]
*
* This file is part of Redbasin OpenDocShare community project.
*
* Redbasin OpenDocShare is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package ldap;

/* import sun.security.x509.RDN; */
import util.LdapConstants;

import javax.naming.directory.*;
import javax.naming.*;
import javax.naming.ldap.LdapName;
import java.util.List;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Hashtable;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
import java.io.UnsupportedEncodingException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * (c) Chris Betts: Groupmind Project
 */
public class SearchUtility {

    /** Logger for this class and subclasses */
    protected static final Log logger = LogFactory.getLog("ldap.SearchUtility");

    private List<String> structureTypes; // defines the meaning of the different levels of the directory tree; division, section etc.

    public static boolean debug = false; // whether to print debug info for this utility class
    public static boolean ldapTracing = false; // whether to print trace info on the directory connection

    // DirContext context;  // this is the link to the LDAP directory

    /**
     * This creates a SearchUtility object, intialised with the ordered types that
     * give names to the semantic structure of the directory that is being searched
     * (e.g. define the structure as country, company, division, section etc.
     *
     * @param types       definitions of the 'meaning' of each level of the directory tree - NOTE these
     *                    are in LEAST SIGNIFICANT ORDER FIRST! (Opposite of normal directory numbering)
     *                    e.g.:  types[0] = name;
     *                    types[1] = section
     *                    types[2] = division;
     *                    types[3] = group;
     *                    types[4] = area
     * @param debug       whether to print debug messages from this class
     */
    public SearchUtility(List<String> types, boolean debug) throws NamingException {
        setStructure(types);
        openDirectory(debug);
    }

    /**
     * This sets the ordered types that
     * give names to the semantic structure of the directory that is being searched
     * (e.g. define the structure as country, company, division, section etc.
     *
     * @param types definitions of the 'meaning' of each level of the directory tree - NOTE these
     *              are in LEAST SIGNIFICANT ORDER FIRST! (Opposite of normal directory numbering)
     *              e.g.:  types[0] = name;
     *              types[1] = section
     *              types[2] = division;
     *              types[3] = group;
     *              types[4] = area
     */
    public void setStructure(List<String> types) {
        structureTypes = new ArrayList<String>(types);
        // set the order of structureTypes to match normal DN order (makes parsing LdapNames easier)
        Collections.reverse(structureTypes);
    }

    /**
     * This opens the directory using the defaults set in the Config class.
     *
     * @param debug   whether to print debug messages from this class
     * @throws NamingException
     */
    private void openDirectory(boolean debug) throws NamingException {
        this.debug = debug;
        SearchUtility.ldapTracing = debug;

        /*
                try
                {
        context = setupJNDIConnection(Config.DIRECTORY_URL, Config.DIRECTORY_ADMIN, Config.DIRECTORY_PWD, ldapTracing);
                }
                catch (AuthenticationException e)
                {
        System.out.println("There was an error establishing the 'admin' connection to the directory");
        System.out.println("The directory rejected the administration credentials:");
        System.out.println("   user: " + Config.DIRECTORY_ADMIN);
        System.out.println("   pwd:  " + Config.DIRECTORY_PWD + "\n");
        e.printStackTrace();
        System.exit(-1);
                }
                catch (Exception e)
                {
        System.out.println("There was an error establishing the 'admin' connection to the directory");
        System.out.println("  Examine the stack trace below for details, including the LDAP error message");
        e.printStackTrace();
        System.exit(-1);
                }
        */
    }

    /**
     * open the directory connection.
     *
     * @param url
     * @param tracing
     * @return
     * @throws javax.naming.NamingException
     */
    private DirContext setupJNDIConnection(String url, String userDN, String password, boolean tracing)
            throws NamingException {
        /*
        * First, set up a large number of environment variables to sensible default valuse
        */

        Hashtable env = new Hashtable();
        // sanity check
        if (url == null)
            throw new NamingException("URL not specified in openContext()!");

        // set the tracing level now, since it can't be set once the connection is open.
        if (tracing)
            env.put("com.sun.jndi.ldap.trace.ber", System.err); // echo trace to standard error output

        env.put("java.naming.ldap.version", "3"); // always use ldap v3 - v2 too limited
        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); // use default jndi provider
        env.put("java.naming.ldap.deleteRDN", "false"); // usually what we want
        env.put(Context.REFERRAL, "ignore"); //could be: follow, ignore, throw
        env.put("java.naming.ldap.derefAliases", "finding"); // could be: finding, searching, etc.
        env.put(Context.SECURITY_AUTHENTICATION, "simple"); // 'simple' = username + password
        env.put(Context.SECURITY_PRINCIPAL, userDN); // add the full user dn
        env.put(Context.SECURITY_CREDENTIALS, password); // stupid jndi requires us to cast this to a string-
        env.put(Context.PROVIDER_URL, url); // the ldap url to connect to; e.g. "ldap://ca.com:389"

        /*
        *  Open the actual LDAP session using the above environment variables
        */

        DirContext newContext = new InitialDirContext(env);

        if (newContext == null)
            throw new NamingException(
                    "Internal Error with jndi connection: No Context was returned, however no exception was reported by jndi.");

        return newContext;

    }

    /**
     * This does a tree walk to find all the elements at a given level.
     * WARNING: for low level elements this may involve a VERY LARGE NUMBER OF DIRECTORY
     * READS!  For example, if a tree has a ten fold fan out; top/area/group/division/section
     * returning all sections would return 10,000 sections and require 1,000 separate directory
     * accesses!
     *
     * This returns a List of the LdapNames of the elements at a particular level (e.g. a list of the
     * LdapNames of all divisions).  Use this method if you are going to do further directory operations
     * with the return values, such as looking up staff.
     *
     * @param type
     * @return a list of distringuished names of appropriate elements
     */
    public List<LdapName> getStructureElementNames(String type, DirContext context) throws NamingException {
        int depth = getStructureLevel(type);
        //return getElementNames(new LdapName(Config.SEARCH_BASE_DN), depth);
        return getElementNames(new LdapName(LdapConstants.ldapSearchBaseDn), depth, context);
    }

    /**
     * This returns a List of the String names of the elements at a particular level (e.g. a list of the
     * String titles of all divisions)
     * @param type
     * @return
     * @throws NamingException
     */
    public List<String> getStructureElements(String type, DirContext context) throws NamingException {
        return convertLdapNamesToStrings(getStructureElementNames(type, context));
    }

    /**
     * This is a utility method for turning a list of LdapNames into a list
     * of Strings, being the final RDN value.  E.g.
     * ou=Computer Networks Division,ou=COMPUTERS AND INFORMATION GROUP,ou=DIRECTORS OFFICE AREA,ou=isac,dc=isro,dc=dos,c=au
     * would become simply "Computer Networks Division".
     * @param elementNames
     * @return a list of the same order and size as that passed in, but giving only the final String RDN value.
     */
    public List<String> convertLdapNamesToStrings(List<LdapName> elementNames) {
        List<String> elements = new ArrayList<String>(elementNames.size());
        for (LdapName name : elementNames)
            elements.add(name.getRdn(name.size() - 1).getValue().toString());
        return elements;
    }

    /**
     * This is a utility method for turning a list of LdapNames into a list
     * of Strings, being the final RDN value.  E.g.
     * ou=Computer Networks Division,ou=COMPUTERS AND INFORMATION GROUP,ou=DIRECTORS OFFICE AREA,ou=isac,dc=isro,dc=dos,c=au
     * would become simply "Computer Networks Division".
     * @param userEntries
     * @return a list of the same order and size as that passed in, but giving only the final String RDN value.
     */
    public List<String> convertUserEntriesToStrings(List<Entry> userEntries, String attrType)
            throws NamingException {
        List<String> elements = new ArrayList<String>(userEntries.size());
        for (Entry user : userEntries)
            elements.add(user.getValue(attrType));
        return elements;
    }

    public List<String> convertUserEntriesToStrings(List<Entry> userEntries) throws NamingException {
        //return convertUserEntriesToStrings(userEntries, Config.USER_NAMING_ATT);
        return convertUserEntriesToStrings(userEntries, LdapConstants.ldapAttrUid);
    }

    /**
     * recursively walks the tree to depth 'depth', and returns
     * a list of all names found at that depth.
     * @param treeNode
     * @param depth
     * @return
     * @throws NamingException
     */

    private List<LdapName> getElementNames(LdapName treeNode, int depth, DirContext context)
            throws NamingException {
        depth--;
        NamingEnumeration<NameClassPair> children = context.list(treeNode);
        List<LdapName> elementNames = new ArrayList<LdapName>();

        // cycle through all the children we've found.
        while (children.hasMore()) {
            NameClassPair child = children.next();
            LdapName childName = new LdapName(child.getNameInNamespace());
            if (depth == 0) // return value - these are what we're looking for!
                elementNames.add(childName);
            else
                elementNames.addAll(getElementNames(childName, depth, context)); // keep going down!
        }

        return elementNames;
    }

    /**
     * Gets the list of all users in the sub tree under searchBase
     * @param searchBase
     * @return a sorted list of users
     * @throws NamingException
     */
    public List<Entry> getUsers(LdapName searchBase, DirContext context) throws NamingException {
        return getUsers(searchBase, null, 0, 0, null, context);
    }

    /**
     * This returns a list of all users that match the particular attribute value.
     * Often this will be a single user, in which case the list will only contain one value.  If
     * you know this is the case, use the 'getUser()' form of this method instead.
     * @param attrType
     * @param attrValue
     * @return
     * @throws NamingException
     */
    public List<Entry> getUsers(String attrType, String attrValue, DirContext context) throws NamingException {
        logger.info("getUsers(attrType,attrValue,context)");
        List<Entry> users = new ArrayList<Entry>();
        Attributes atts = new BasicAttributes();
        atts.put(attrType, attrValue);
        //NamingEnumeration<SearchResult> userResults = context.search(new LdapName(Config.SEARCH_BASE_DN), attrType + "={0}", new String[] {attrValue}, getSearchControls());
        NamingEnumeration<SearchResult> userResults = context.search(new LdapName(LdapConstants.ldapSearchBaseDn),
                attrType + "={0}", new String[] { attrValue }, getSearchControls());
        while (userResults.hasMore()) {
            SearchResult userResult = userResults.next();
            users.add(new Entry(userResult));
        }
        return users;
    }

    public boolean userHasAttribute(String DN, String attrType, String attrValue, DirContext context)
            throws NamingException {
        Attributes atts = new BasicAttributes();
        atts.put(attrType, attrValue);
        NamingEnumeration<SearchResult> userResults = context.search(new LdapName(DN), "(" + attrType + "={0})",
                new String[] { attrValue }, getSearchControls());
        return (userResults.hasMore());
    }

    public boolean checkPassword(String DN, String pwdAtt, String value, DirContext context)
            throws NamingException, UnsupportedEncodingException {
        SearchControls ctls = new SearchControls();
        ctls.setReturningAttributes(new String[0]); // Return no attrs
        ctls.setSearchScope(SearchControls.OBJECT_SCOPE); // Search object only
        //byte[] pwdBytes = value.getBytes("UTF-8");
        byte[] pwdBytes = value.getBytes(LdapConstants.UTF8);

        // Invoke search method that will use the LDAP "compare" operation
        NamingEnumeration answer = context.search(DN, "(" + pwdAtt + "={0})", new Object[] { pwdBytes }, ctls);
        return answer.hasMore();
    }

    /**
        * This returns a  users that match the particular attribute value.
        * If there might be more than one user, use getUsers() instead.
        * If there is more than one matching user, the first user found will be
        * returned (note the ordering will be server dependent)
        * @param attrType
        * @param attrValue
        * @return a single user matching the attribute type and value, or null if no such user exists.
        * @throws NamingException
        */
    public Entry getUser(String attrType, String attrValue, DirContext context) throws NamingException {
        List<Entry> users = getUsers(attrType, attrValue, context);
        return users.size() > 0 ? users.get(0) : null;
    }

    /**
     * Gets a page of users in the sub tree under search base
     * @param searchBase
     * @param pageSize
     * @param pageNumber
     * @return a page from the sorted list of users
     * @throws NamingException
     */
    public List<Entry> getUsers(LdapName searchBase, int pageSize, int pageNumber, DirContext context)
            throws NamingException {
        return getUsers(searchBase, null, pageSize, pageNumber, null, context);
    }

    /**
     *
     * @param searchBase
     * @param startChar
     * @param endChar
     * @param context
     * @return a list of matching, sorted users
     * @throws NamingException
     */
    public List<Entry> getUsers(LdapName searchBase, char startChar, char endChar, DirContext context)
            throws NamingException {
        String regexp = "^[" + startChar + "-" + endChar + "].*";
        return getUsers(searchBase, regexp, 0, 0, null, context);
    }

    /**
     *
     * @param searchBase
     * @param startChar
     * @param endChar
     * @param pageSize - the number of users to return (0 for unlimited)
     * @param pageNumber - the number of the page
     * @return a list of matching, sorted users
     * @throws NamingException
     */
    public List<Entry> getUsers(LdapName searchBase, char startChar, char endChar, int pageSize, int pageNumber,
            DirContext context) throws NamingException {
        String regexp = "^[" + startChar + "-" + endChar + "].*";
        return getUsers(searchBase, regexp, pageSize, pageNumber, null, context);
    }

    /**
     *
     * @param searchBase
     * @param regexp
     * @param pageSize
     * @param pageNumber
     * @return  a list of matching users.
     * @throws NamingException
     */
    public List<Entry> getUsers(LdapName searchBase, String regexp, int pageSize, int pageNumber,
            ArrayList<String> attributes, DirContext context) throws NamingException {
        Pattern pattern = null;
        if (regexp != null)
            pattern = Pattern.compile(regexp);
        /*
         *   Figure out an ldap search filter.  Note that unless an ORDERING matching rule is defined on the server
         *   for the attribute we are searching (and they usually aren't, since it requires extra indexing on the
         *   server), we cannot use ldap greater than / less than search filters to find
         *   a range of users, and have to do this search in code using a regular expression.
         */
        //String filter = "(objectClass=" + Config.USER_OBJECTCLASS + ")";
        String filter = "";
        if (LdapConstants.ldapObjectClassEmployeeEnable) {
            filter = "(objectClass=" + LdapConstants.ldapObjectClassEmployee + ")";
        }
        SearchControls controls = getSearchControls();
        String[] attributesToReturn;
        if (attributes == null) {
            attributesToReturn = null; // a JNDI special value that means 'return everything'
        } else {
            //attributes.add(Config.USER_NAMING_ATT);
            attributes.add(LdapConstants.ldapAttrUid);
            attributesToReturn = attributes.toArray(new String[] {});
        }

        if (controls != null) {
            controls.setReturningAttributes(attributesToReturn);
        } else {
            logger.info("controls is null");
        }

        // do the directory search
        NamingEnumeration<SearchResult> userResults = context.search(searchBase, filter, controls);

        if (userResults == null) {
            logger.info("userResults is Null in getUsers()");
            return null;
        } else {

            // parse the results, looking for entries that match our regexp
            ArrayList<Entry> users = new ArrayList<Entry>();
            while (userResults.hasMore()) {
                SearchResult userResult = userResults.next();
                Entry userEntry = new Entry(userResult);

                //String text =  userEntry.getValue(Config.USER_NAMING_ATT).toUpperCase();
                String text = userEntry.getValue(LdapConstants.ldapAttrUid).toUpperCase();

                if (pattern == null) {
                    users.add(userEntry);
                } else {
                    Matcher matcher = pattern.matcher(text);
                    if (matcher.find()) {
                        users.add(userEntry);
                    }
                }
            }

            // sort them alphabeticaly by user naming attribute
            Collections.sort(users);

            // trim the results to the page requested (if any)
            if (pageSize > 0) {
                ArrayList<Entry> userPage = new ArrayList<Entry>(pageSize);
                int startPos = pageSize * pageNumber;
                int size = users.size();
                for (int i = startPos; i < (startPos + pageSize); i++) {
                    if (i < size) {
                        userPage.add(users.get(i));
                    }
                }
                users = userPage;
            }

            // add 'synthetic' attributes for
            for (Entry user : users) {
                fillInSyntheticAttributes(user);
            }
            // return the final user list
            return users;
        } // else
    }

    /**
     * This parses the name, and adds 'fake' attributes corresponding to the structure types list.
     * @param entry
     */
    private void fillInSyntheticAttributes(Entry entry) {
        LdapName name = entry.getName();

        int size = name.size();
        int maxTypeSize = structureTypes.size();
        //for (int i=Config.SEARCH_BASE_DN_SIZE-1; i<size; i++)
        for (int i = LdapConstants.ldapSearchBaseDnSize - 1; i < size; i++) {
            String value = name.getRdn(i).getValue().toString();
            //int typeIndex = i - Config.SEARCH_BASE_DN_SIZE + 1;
            int typeIndex = i - LdapConstants.ldapSearchBaseDnSize + 1;
            if (typeIndex < maxTypeSize) {
                String typeName = structureTypes.get(typeIndex);
                if (entry.get(typeName) == null)
                    entry.put(typeName, value);
                else
                    logger.info("WARNING: Type Name '" + typeName + "' clashes with existing attribute - skipping");
            } else // if we're here, we've run out of defined elements.
            {
                logger.info("WARNING: Type elements out of range in name " + name);
            }

        }
    }

    /**
     * A utility method to get a conrols object.  May be redundant; the default new Controls() would probably
     * suffice.
     * @return a new SearchControls object that can be modified and passed to a context.search method.
     */
    private SearchControls getSearchControls() {
        SearchControls constraints = new SearchControls();
        constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
        constraints.setCountLimit(0);
        constraints.setTimeLimit(0);
        return constraints;
    }

    /**
     * This searches through the list of pre-defined structure elements until it finds one that matches
     * the passed type parameter, and then returns the 'depth' of that element.  We will search this level
     * of the tree (under the Config.SEARCH_BASE dn).
     * @param type
     * @return
     * @throws NamingException
     */
    public int getStructureLevel(String type) throws NamingException {
        int level;
        for (level = 0; level < structureTypes.size(); level++)
            if (structureTypes.get(level).equals(type))
                break;

        if (level == structureTypes.size())
            throw new NamingException("Unknown type: '" + type
                    + "' - has it been initialised in the constructor structure definition?");

        return level;
    }

}