Java tutorial
/* * Copyright 2008-2011 UnboundID Corp. All Rights Reserved. */ /* * Copyright (C) 2008-2011 UnboundID Corp. This program is free * software; you can redistribute it and/or modify it under the terms of * the GNU General Public License (GPLv2 only) or the terms of the GNU * Lesser General Public License (LGPLv2.1 only) as published by the * Free Software Foundation. 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 samplecode.search; import com.unboundid.ldap.sdk.*; import com.unboundid.util.LDAPCommandLineTool; import com.unboundid.util.MinimalLogFormatter; import com.unboundid.util.NotMutable; import com.unboundid.util.Validator; import com.unboundid.util.args.Argument; import com.unboundid.util.args.ArgumentException; import com.unboundid.util.args.ArgumentParser; import com.unboundid.util.args.StringArgument; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import samplecode.annotation.Author; import samplecode.annotation.CodeVersion; import samplecode.annotation.Since; import samplecode.cli.CommandLineOptions; import samplecode.listener.*; import samplecode.util.SampleCodeCollectionUtils; import samplecode.util.StaticData; import java.io.PrintStream; import java.util.List; import java.util.Vector; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.logging.Level; import java.util.logging.LogRecord; /** * Provides a way to invoke the {@code searchEntryReturned} and * {@code searchReferenceReturned} methods of a * {@code SearchResultListener} for each entry returned from a search * based on the standard command line options. The * {@code SearchResultListener} is supplied as a command line argument. * <p/> * <b>usage example</b> * <p/> * The following invocation for each thread invokes the * searchEntryReturned and searchReferenceReturned methods in the class * {@code "samplecode.PrintEntrySearchResultListener"} for each entry * returned from the search request constructed by the base object * {@code "dc=example,dc=com"}, {@code subtree} scope, filter * {@code "(objectClass=*)"}, attributes {@code mail} and {@code uid}. A * size limit and time limit are specified, as is an instruction to * abandon operations that timed out and automatically reconnect to * directory server when the connection is lost. The connection to the * directory server will be authorized as * {@code "uid=user.0,ou=people,dc=example,dc=com"}, and the * {@code StartTLS} extended request will attempt to encrypt the * connection between client and server.<blockquote> * <p/> * <pre> * java -cp your-classpath samplecode.EveryEntry \ * --abandonOnTimeout --attribute mail --attribute uid \ * --autoReconnect --baseObject "dc=example,dc=com" \ * --bindDn "uid=user.0,ou=people,dc=example,dc=com" \ * --bindPasswordFile /Users/terrygardner/.pwdFile \ * --filter "(objectClass=*)" --hostname localhost \ * --port 1389 --scope SUB --sizeLimit 1000 --timeLimit 1 \ * --searchResultListener "samplecode.PrintEntrySearchResultListener" \ * --useStartTls --trustAll --numThreads 4 * </pre> * <p/> * </blockquote> * * @see SearchResultListener * @see LDAPCommandLineTool */ @Author("terry.gardner@unboundid.com") @Since("Dec 18, 2011") @CodeVersion("1.4") public final class EveryEntry extends LDAPCommandLineTool { /** * <blockquote> * <p/> * <pre> * Provides a way to invoke the searchEntryReturned and * searchReferenceReturned * methods of a SearchResultListener for each entry returned from a search * based * on the standard command line options. * * Usage: EveryEntry {options} * * Available options include: * -h, --hostname {host} * The IP address or resolvable name to use to connect to the directory * server. If this is not provided, then a default value of 'localhost' * will * be used. * -p, --port {port} * The port to use to connect to the directory server. If this is not * provided, then a default value of 389 will be used. * -D, --bindDN {dn} * The DN to use to bind to the directory server when performing simple * authentication. * -w, --bindPassword {password} * The password to use to bind to the directory server when performing * simple * authentication or a password-based SASL mechanism. * -j, --bindPasswordFile {path} * The path to the file containing the password to use to bind to the * directory server when performing simple authentication or a * password-based * SASL mechanism. * -Z, --useSSL * Use SSL when communicating with the directory server. * -q, --useStartTLS * Use StartTLS when communicating with the directory server. * -X, --trustAll * Trust any certificate presented by the directory server. * -K, --keyStorePath {path} * The path to the file to use as the key store for obtaining client * certificates when communicating securely with the directory server. * -W, --keyStorePassword {password} * The password to use to access the key store contents. * -u, --keyStorePasswordFile {path} * The path to the file containing the password to use to access the key * store * contents. * --keyStoreFormat {format} * The format (e.g., jks, jceks, pkcs12, etc.) for the key store file. * -P, --trustStorePath {path} * The path to the file to use as trust store when determining whether to * trust a certificate presented by the directory server. * -T, --trustStorePassword {password} * The password to use to access the trust store contents. * -U, --trustStorePasswordFile {path} * The path to the file containing the password to use to access the * trust * store contents. * --trustStoreFormat {format} * The format (e.g., jks, jceks, pkcs12, etc.) for the trust store file. * -N, --certNickname {nickname} * The nickname (alias) of the client certificate in the key store to * present * to the directory server for SSL client authentication. * -o, --saslOption {name=value} * A name-value pair providing information to use when performing SASL * authentication. * --numThreads {number-of-threads} * Specifies the number of threads to use when running the application. * --useSchema * Whether the LDAP SDK should attempt to use server schema information, * for * example, for matching rules. * --verbose * Whether the tool should be verbose. * --abandonOnTimeout * Whether the LDAP SDK should abandon an operation that has timed out. * --autoReconnect * Whether the LDAP SDK should automatically reconnect when a connection * is * lost. * --connectTimeoutMillis {connect-timeout-millis-integer} * Specifies the maximum length of time in milliseconds that a connection * attempt should be allowed to continue before giving up. A value of * zero * indicates that there should be no connect timeout. * -b, --baseObject {distinguishedName} * The base object used in the search request. * --maxResponseTimeMillis {max-response-time-in-milliseconds} * The maximum length of time in milliseconds that an operation should be * allowed to block, with 0 or less meaning no timeout is enforced. This * command line argument is optional and has a default value of zero. * --reportInterval {positive-integer} * The report interval in milliseconds. * --reportCount {positive-integer} * Specifies the maximum number of reports. This command line argument is * applicable to tools that display repeated reports. The time between * repeated reports is specified by the --reportInterval command line * argument. * -a, --attribute {attribute name or type} * The attribute used in the search request or other request. This * command * line argument is not required, and can be specified multiple times. If * this * command line argument is not specified, the value '*' is used. * -f, --filter {filter} * The search filter used in the search request. * -i, --initialConnections {positiveInteger} * The number of initial connections to establish to directory server * when * creating the connection pool. * -m, --maxConnections {positiveInteger} * The maximum number of connections to establish to directory server * when * creating the connection pool. * -s, --scope {searchScope} * The scope of the search request; allowed values are BASE, ONE, and SUB * --sizeLimit {positiveInteger} * The client-request maximum number of results which are returned to the * client. If the number of entries which match the search parameter is * greater than the client-requested size limit or the server-imposed * size * limit a SIZE_LIMIT_EXCEEDED code is returned in the result code in the * search response. * --timeLimit {positiveInteger} * The client-request maximum time to search used by the server. If the * time * of the search is greater than the client-requested time limit or the * server-imposed time limit a TIME_LIMIT_EXCEEDED code is returned in * the * result code in the search response. * --pageSize {positiveInteger} * The search page size * --searchResultListener {class-name} * The name of class which extends the AbstractSearchResultListener * class. * The * searchEntryReturned method of this class is invoked when an entry is * returned from a search. * -H, -?, --help * Display usage information for this program. * </pre> * <p/> * </blockquote> * * @param args * JVM command line arguments. */ public static void main(final String... args) { final EveryEntry everyEntry = new EveryEntry(); final ResultCode resultCode = everyEntry.runTool(args); if (resultCode != null) { final String message = String.format("%s has completed processing. The result code was: %s", everyEntry.getToolName(), resultCode); final LogRecord logRecord = new LogRecord(Level.INFO, message); final String msg = new MinimalLogFormatter().format(logRecord); everyEntry.out(msg); } } /** * The description of this tool; used for help, diagnostic, and other * purposes. */ private static final String TOOL_DESCRIPTION = "Provides a way to invoke the searchEntryReturned and " + "searchReferenceReturned methods of a SearchResultListener " + "for each " + "entry returned from a search based on the " + "standard command line " + "options."; /** * The name of this tool; used for help, diagnostic, and other * purposes. */ private static final String TOOL_NAME = "EveryEntry"; /** * Constructs a new {@code EveryEntry} object that will use the * System.out and System.err output streams. */ public EveryEntry() { super(System.out, System.err); formatter = new MinimalLogFormatter(); } /** * {@inheritDoc} */ @Override public String toString() { return "EveryEntry [" + (commandLineOptions != null ? "commandLineOptions=" + commandLineOptions + ", " : "") + (formatter != null ? "formatter=" + formatter : "") + "]"; } /** * {@inheritDoc} */ @Override public void addNonLDAPArguments(final ArgumentParser argumentParser) throws ArgumentException { Validator.ensureNotNull(argumentParser); commandLineOptions = EveryEntryCommandLineOptions.newEveryEntryCommandLineOptions(argumentParser); } /** * {@inheritDoc} */ @Override public String getToolName() { return EveryEntry.TOOL_NAME; } /** * {@inheritDoc} */ @Override public String getToolDescription() { return EveryEntry.TOOL_DESCRIPTION; } /** * {@inheritDoc} */ @Override public ResultCode doToolProcessing() { if (commandLineOptions.isVerbose()) { out(commandLineOptions); } /* * Set up an executor service with a fixed thread pool. */ final int numThreads = commandLineOptions.getNumThreads(); final ExecutorService executorService = Executors.newFixedThreadPool(numThreads); /* * Start searches, one per thread. */ ResultCode resultCode = ResultCode.SUCCESS; resultCode = startSearches(executorService, numThreads); executorService.shutdown(); return resultCode; } /** * Starts all threads, one thread per task. * * @param executorService * the service providing a thread pool in which to execute * tasks. * @param numThreads * the number of threads (and tasks since there is one task * per thread). * * @return a single result code. */ private ResultCode startSearches(final ExecutorService executorService, final int numThreads) { Validator.ensureNotNull(executorService); ResultCode resultCode = ResultCode.SUCCESS; for (int t = 0; t < numThreads; ++t) { final String searchListenerClassname = commandLineOptions.getSearchResultListenerClassname(); EveryEntryImpl impl; try { /* * Get a connection to the server and create an error listener * for later assignment to a task. Create the task and submit to * the executor service. */ final LDAPConnection ldapConnection = getConnection(); final List<ErrorListener<ResultCode>> errorListeners = SampleCodeCollectionUtils.newArrayList(); final ErrorListener<ResultCode> l = new ResultCodeErrorListener(); errorListeners.add(l); impl = new EveryEntryImpl(searchListenerClassname, commandLineOptions, ldapConnection, getErr(), errorListeners); final Log logger = LogFactory.getLog(getClass()); final LdapExceptionListener ldapExceptionListener = new DefaultLdapExceptionListener(logger); impl.addLdapExceptionListener(ldapExceptionListener); executorService.submit(impl); } catch (final LDAPException ldapException) { resultCode = ldapException.getResultCode(); } catch (final InstantiationException instantiationException) { err(formatter.format(new LogRecord(Level.SEVERE, "Cannot instantiate " + instantiationException.getLocalizedMessage()))); resultCode = ResultCode.PARAM_ERROR; } catch (final IllegalAccessException e) { resultCode = ResultCode.OPERATIONS_ERROR; } catch (final ClassNotFoundException classNotFoundException) { err(formatter .format(new LogRecord(Level.SEVERE, String.format( "The class '%s' " + "specified as the search " + "result listener could not be found.", searchListenerClassname)))); resultCode = ResultCode.PARAM_ERROR; } } return resultCode; } /** * The command line arguments processor. */ private EveryEntryCommandLineOptions commandLineOptions; /** * Provides services for clients that require messages to be formatted * in a standardized way. */ private final MinimalLogFormatter formatter; } /** * Provides command line argument services local to {@code EveryEntry} * including any command line arguments that used by {@code EveryEntry}. */ final class EveryEntryCommandLineOptions extends CommandLineOptions { /** * Get a new instance of {@code EveryEntryCommandLineOptions}. The * {@code argumentParser} (which cannot be {@code null}) is used to * add an additional command line argument. * * @param argumentParser * handles the task of parsing command line arguments. * * @return a new instance of {@code EveryEntryCommandLineOptions}. * * @throws ArgumentException * if a problem transpired creating and adding an * {@code Argument}. */ public static EveryEntryCommandLineOptions newEveryEntryCommandLineOptions(final ArgumentParser argumentParser) throws ArgumentException { Validator.ensureNotNull(argumentParser); return new EveryEntryCommandLineOptions(argumentParser); } /** * The description of the search result listener command line * argument. */ private static final String DESCRIPTION_SEARCH_RESULT_LISTENER = "The name of class which extends the AbstractSearchResultListener class. " + "The " + "searchEntryReturned method of this class is invoked when " + "an entry is " + "returned from a search."; /** * The isRequired parameter of the command line argument whose * parameter is the name of a class that extends * {@code SearchResultListener}. */ private static final boolean IS_REQUIRED_SEARCH_RESULT_LISTENER = true; /** * The long identifier of the command line argument whose parameter is * the name of a class that extends {@code SearchResultListener}. */ private static final String LONG_ID_SEARCH_RESULT_LISTENER = "searchResultListener"; /** * The short identifier of the command line argument whose parameter * is now many times the search result listener may occur on the * command line. */ private static final int MAX_OCCURRENCES_SEARCH_RESULT_LISTENER = 0; /** * The short identifier of the command line argument whose parameter * indicates whether the search result listener command line argument * is required. */ private static final Character SHORT_ID_SEARCH_RESULT_LISTENER = null; /** * The value place-holder of the command line argument whose parameter * is the value place-holder of the search result listener command * line argument. */ private static final String VALUE_PLACEHOLDER_SEARCH_RESULT_LISTENER = "{class-name}"; private EveryEntryCommandLineOptions(final ArgumentParser argumentParser) throws ArgumentException { super(CommandLineOptions.createDefaultArguments(StaticData.getResourceBundle()), argumentParser); final Argument searchResultListenerArgument = newSearchResultListenerArgument(); addArguments(searchResultListenerArgument); } /** * Retrieves the parameter of the command line argument that specifies * the name of the class used as the search result listener. * * @return The search result listener classname. */ public String getSearchResultListenerClassname() { final StringArgument searchResultListenerArg = (StringArgument) getArgumentParser() .getNamedArgument(EveryEntryCommandLineOptions.LONG_ID_SEARCH_RESULT_LISTENER); return searchResultListenerArg.getValue(); } /** * Create the argument used for transmitting the desired search result * listener classname. * * @return a command line {@code Argument}. * * @throws ArgumentException * if a problem transpires creating the argument. */ private Argument newSearchResultListenerArgument() throws ArgumentException { final Character shortIdentifier = EveryEntryCommandLineOptions.SHORT_ID_SEARCH_RESULT_LISTENER; final String longIdentifier = EveryEntryCommandLineOptions.LONG_ID_SEARCH_RESULT_LISTENER; final boolean isRequired = EveryEntryCommandLineOptions.IS_REQUIRED_SEARCH_RESULT_LISTENER; final int maxOccurrences = EveryEntryCommandLineOptions.MAX_OCCURRENCES_SEARCH_RESULT_LISTENER; final String valuePlaceholder = EveryEntryCommandLineOptions.VALUE_PLACEHOLDER_SEARCH_RESULT_LISTENER; final String description = EveryEntryCommandLineOptions.DESCRIPTION_SEARCH_RESULT_LISTENER; return new StringArgument(shortIdentifier, longIdentifier, isRequired, maxOccurrences, valuePlaceholder, description); } } /** * Invokes the methods of {@code SearchResultListener} for entry entry * returned from a search request. Supply error listeners that will be * notified when an error or exception occurs. */ final class EveryEntryImpl implements Runnable, ObservedByLdapExceptionListener { /** * Get a new instance of {@code EveryEntryImpl}. None of the * parameters are permitted to be {@code null}. * * @param searchListenerClassname * the name of the class to be used as the search result * listener. * @param commandLineOptions * user-provided command line options. * @param ldapConnection * a connection to an LDAP server. * @param errStream * a stream to which error output is transmitted. * @param errorListeners * they are notified when an error or exception transpires. * * @throws LDAPException * if a {@code SearchRequest} cannot be created using * parameters from the command line arguments. * @throws InstantiationException * if the class named by {@code searchListenerClassname} * cannot be instantiated. * @throws IllegalAccessException * if the class named by {@code searchListenerClassname} * cannot be instantiated. * @throws ClassNotFoundException * if the class named by {@code searchListenerClassname} * cannot be found. */ public EveryEntryImpl(final String searchListenerClassname, final EveryEntryCommandLineOptions commandLineOptions, final LDAPConnection ldapConnection, final PrintStream errStream, final List<ErrorListener<ResultCode>> errorListeners) throws LDAPException, InstantiationException, IllegalAccessException, ClassNotFoundException { Validator.ensureNotNull(searchListenerClassname, commandLineOptions, ldapConnection, errStream, errorListeners); this.errStream = errStream; this.errorListeners = errorListeners; this.searchListenerClassname = searchListenerClassname; this.commandLineOptions = commandLineOptions; this.ldapConnection = ldapConnection; setConnectionOptions(); searchRequest = createSearchRequest(newSearchResultListener()); } /** * {@inheritDoc} */ @Override public synchronized void addLdapExceptionListener(final LdapExceptionListener ldapExceptionListener) { if (ldapExceptionListener != null) { ldapExceptionListeners.add(ldapExceptionListener); } } /** * {@inheritDoc} */ @SuppressWarnings("unchecked") @Override public void fireLdapExceptionListener(final LDAPConnection ldapConnection, final LDAPException ldapException) { Validator.ensureNotNull(ldapConnection, ldapException); Vector<LdapExceptionListener> copy; synchronized (this) { copy = (Vector<LdapExceptionListener>) ldapExceptionListeners.clone(); } if (copy.size() == 0) { return; } final LdapExceptionEvent ev = new LdapExceptionEvent(this, ldapConnection, ldapException); for (final LdapExceptionListener l : copy) { l.ldapRequestFailed(ev); } } /** * {@inheritDoc} */ @Override public synchronized void removeLdapExceptionListener(final LdapExceptionListener ldapExceptionListener) { if (ldapExceptionListener != null) { ldapExceptionListeners.remove(ldapExceptionListener); } } /** * {@inheritDoc} * <p/> * Transmits the search request to the server and returns. */ @Override public void run() { ResultCode resultCode; try { final long begin = System.currentTimeMillis(); resultCode = search(); @SuppressWarnings("unused") final long elapsed = System.currentTimeMillis() - begin; if (!resultCode.equals(ResultCode.SUCCESS)) { notifyErrorListeners(resultCode); } } catch (final LDAPSearchException ldapException) { fireLdapExceptionListener(ldapConnection, ldapException); resultCode = ldapException.getResultCode(); notifyErrorListeners(resultCode); } } @Override public String toString() { final int maxLen = 10; return "EveryEntryImpl [" + (commandLineOptions != null ? "commandLineOptions=" + commandLineOptions + ", " : "") + (errorListeners != null ? "errorListeners=" + errorListeners.subList(0, Math.min(errorListeners.size(), maxLen)) + ", " : "") + (errStream != null ? "errStream=" + errStream + ", " : "") + (ldapConnection != null ? "ldapConnection=" + ldapConnection + ", " : "") + (searchListenerClassname != null ? "searchListenerClassname=" + searchListenerClassname + ", " : "") + (searchRequest != null ? "searchRequest=" + searchRequest : "") + "]"; } /** * Create the search request using the user-provided command line * argument parameters. Provide the size limit and time limit from the * command line argument parameters. * * @throws LDAPException * If the search request cannot be created. * @see CommandLineOptions */ private SearchRequest createSearchRequest(final AbstractSearchResultListener searchResultListener) throws LDAPException { Validator.ensureNotNull(searchResultListener); SearchRequest sr; final String baseObject = commandLineOptions.getBaseObject(); final SearchScope scope = commandLineOptions.getSearchScope(); final Filter filter = commandLineOptions.getFilter(); final String[] requestedAttributes = commandLineOptions.getRequestedAttributes().toArray(new String[0]); sr = new SearchRequest(searchResultListener, baseObject, scope, filter, requestedAttributes); final int sizeLimit = commandLineOptions.getSizeLimit(); sr.setSizeLimit(sizeLimit); final int timeLimit = commandLineOptions.getTimeLimit(); sr.setTimeLimitSeconds(timeLimit); return sr; } /** * Create the search result listener specified by the * {@code --searchResultListener} command line arguments. */ private AbstractSearchResultListener newSearchResultListener() throws ClassNotFoundException, InstantiationException, IllegalAccessException { @SuppressWarnings("unchecked") final Class<? extends AbstractSearchResultListener> cl = (Class<? extends AbstractSearchResultListener>) Class .forName(searchListenerClassname); final AbstractSearchResultListener searchResultListener = cl.newInstance(); searchResultListener.setCommandLineOptions(commandLineOptions); searchResultListener.setLDAPConnection(ldapConnection); return searchResultListener; } /** * Notify each error listener in their natural ordering that an error * has occurred. * * @param resultCode * The result code of an operation */ private void notifyErrorListeners(final ResultCode resultCode) { Validator.ensureNotNull(resultCode); for (final ErrorListener<ResultCode> l : errorListeners) { l.displayError(errStream, resultCode); } } /** * Transmits a search request on the {@code ldapConnection}. The * search result entries and search result references are handled by * the search result listener. * * @return The result code from the response result. * * @throws LDAPSearchException * If the server rejects the request, or if a problem is * encountered while sending the request or reading the * response. */ private ResultCode search() throws LDAPSearchException { final SearchResult searchResult = ldapConnection.search(searchRequest); return searchResult.getResultCode(); } /** * Set options on the connection to the server. The options are taken * from the parameters of the command line arguments. * * @see CommandLineOptions */ private void setConnectionOptions() { final LDAPConnectionOptions connectionOptions = commandLineOptions.newLDAPConnectionOptions(); ldapConnection.setConnectionOptions(connectionOptions); } private final EveryEntryCommandLineOptions commandLineOptions; private final PrintStream errStream; private final List<ErrorListener<ResultCode>> errorListeners; private final LDAPConnection ldapConnection; /** * interested parties to {@code LdapExceptionEvents} */ private volatile Vector<LdapExceptionListener> ldapExceptionListeners = new Vector<LdapExceptionListener>(); private final String searchListenerClassname; private final SearchRequest searchRequest; } /** * An implementation of the {@code ErrorListener} that sends a message * on the error stream that includes a {@code ResultCode}. */ @NotMutable final class ResultCodeErrorListener implements ErrorListener<ResultCode> { /** * {@inheritDoc} * <p/> * Sends a message on the {@code errStream} that will include the * {@code resultCode}. */ @Override public void displayError(final PrintStream errStream, final ResultCode resultCode) { Validator.ensureNotNull(resultCode); final String msg = String.format("An error condition occurred: %s", resultCode); final LogRecord record = new LogRecord(Level.SEVERE, msg); errStream.println(new MinimalLogFormatter().format(record)); } }