Java tutorial
/** * 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; } }