/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License, Version 1.0 only * (the "License"). You may not use this file except in compliance * with the License. * * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt * or http://forgerock.org/license/CDDLv1.0.html. * See the License for the specific language governing permissions * and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at legal-notices/CDDLv1_0.txt. * If applicable, add the following below this CDDL HEADER, with the * fields enclosed by brackets "[]" replaced with your own identifying * information: * Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END * * * Copyright 2006-2010 Sun Microsystems, Inc. */ package com.forgerock.opendj.ldap.tools; import static com.forgerock.opendj.ldap.tools.ToolsMessages.*; import static com.forgerock.opendj.util.StaticUtils.EOL; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.util.StringTokenizer; import org.forgerock.i18n.LocalizableMessage; import org.forgerock.opendj.ldap.ByteString; import org.forgerock.opendj.ldap.Connection; import org.forgerock.opendj.ldap.DecodeException; import org.forgerock.opendj.ldap.DecodeOptions; import org.forgerock.opendj.ldap.ErrorResultException; import org.forgerock.opendj.ldap.controls.AuthorizationIdentityRequestControl; import org.forgerock.opendj.ldap.controls.AuthorizationIdentityResponseControl; import org.forgerock.opendj.ldap.controls.GenericControl; import org.forgerock.opendj.ldap.controls.GetEffectiveRightsRequestControl; import org.forgerock.opendj.ldap.controls.PasswordExpiredResponseControl; import org.forgerock.opendj.ldap.controls.PasswordExpiringResponseControl; import org.forgerock.opendj.ldap.controls.PasswordPolicyErrorType; import org.forgerock.opendj.ldap.controls.PasswordPolicyRequestControl; import org.forgerock.opendj.ldap.controls.PasswordPolicyResponseControl; import org.forgerock.opendj.ldap.controls.PasswordPolicyWarningType; import org.forgerock.opendj.ldap.controls.SubtreeDeleteRequestControl; import org.forgerock.opendj.ldap.responses.BindResult; import com.forgerock.opendj.ldap.controls.AccountUsabilityRequestControl; import com.forgerock.opendj.ldap.tools.AuthenticatedConnectionFactory.AuthenticatedConnection; import com.forgerock.opendj.util.StaticUtils; /** * This class provides utility functions for all the client side tools. */ final class Utils { /** * The name of a command-line script used to launch a tool. */ static final String PROPERTY_SCRIPT_NAME = "com.forgerock.opendj.ldap.tools.scriptName"; /** * The column at which to wrap long lines of output in the command-line * tools. */ static final int MAX_LINE_WIDTH; static { int columns = 80; try { final String s = System.getenv("COLUMNS"); if (s != null) { columns = Integer.parseInt(s); } } catch (final Exception e) { // Do nothing. } MAX_LINE_WIDTH = columns - 1; } /** * Filters the provided value to ensure that it is appropriate for use as an * exit code. Exit code values are generally only allowed to be between 0 * and 255, so any value outside of this range will be converted to 255, * which is the typical exit code used to indicate an overflow value. * * @param exitCode * The exit code value to be processed. * @return An integer value between 0 and 255, inclusive. If the provided * exit code was already between 0 and 255, then the original value * will be returned. If the provided value was out of this range, * then 255 will be returned. */ static int filterExitCode(final int exitCode) { if (exitCode < 0) { return 255; } else if (exitCode > 255) { return 255; } else { return exitCode; } } /** * Parse the specified command line argument to create the appropriate * LDAPControl. The argument string should be in the format * controloid[:criticality[:value|::b64value|:<fileurl]] * * @param argString * The argument string containing the encoded control * information. * @return The control decoded from the provided string, or * null if an error occurs while parsing the argument * value. * @throws org.forgerock.opendj.ldap.DecodeException * If an error occurs. */ static GenericControl getControl(final String argString) throws DecodeException { String controlOID = null; boolean controlCriticality = false; ByteString controlValue = null; int idx = argString.indexOf(":"); if (idx < 0) { controlOID = argString; } else { controlOID = argString.substring(0, idx); } final String lowerOID = StaticUtils.toLowerCase(controlOID); if (lowerOID.equals("accountusable") || lowerOID.equals("accountusability")) { controlOID = AccountUsabilityRequestControl.OID; } else if (lowerOID.equals("authzid") || lowerOID.equals("authorizationidentity")) { controlOID = AuthorizationIdentityRequestControl.OID; } else if (lowerOID.equals("noop") || lowerOID.equals("no-op")) { // controlOID = OID_LDAP_NOOP_OPENLDAP_ASSIGNED; } else if (lowerOID.equals("subentries")) { // controlOID = OID_LDAP_SUBENTRIES; } else if (lowerOID.equals("managedsait")) { // controlOID = OID_MANAGE_DSAIT_CONTROL; } else if (lowerOID.equals("pwpolicy") || lowerOID.equals("passwordpolicy")) { controlOID = PasswordPolicyRequestControl.OID; } else if (lowerOID.equals("subtreedelete") || lowerOID.equals("treedelete")) { controlOID = SubtreeDeleteRequestControl.OID; } else if (lowerOID.equals("realattrsonly") || lowerOID.equals("realattributesonly")) { // controlOID = OID_REAL_ATTRS_ONLY; } else if (lowerOID.equals("virtualattrsonly") || lowerOID.equals("virtualattributesonly")) { // controlOID = OID_VIRTUAL_ATTRS_ONLY; } else if (lowerOID.equals("effectiverights") || lowerOID.equals("geteffectiverights")) { controlOID = GetEffectiveRightsRequestControl.OID; } if (idx < 0) { return GenericControl.newControl(controlOID); } final String remainder = argString.substring(idx + 1, argString.length()); idx = remainder.indexOf(":"); if (idx == -1) { if (remainder.equalsIgnoreCase("true")) { controlCriticality = true; } else if (remainder.equalsIgnoreCase("false")) { controlCriticality = false; } else { // TODO: I18N throw DecodeException.error(LocalizableMessage .raw("Invalid format for criticality value:" + remainder)); } return GenericControl.newControl(controlOID, controlCriticality); } final String critical = remainder.substring(0, idx); if (critical.equalsIgnoreCase("true")) { controlCriticality = true; } else if (critical.equalsIgnoreCase("false")) { controlCriticality = false; } else { // TODO: I18N throw DecodeException.error(LocalizableMessage .raw("Invalid format for criticality value:" + critical)); } final String valString = remainder.substring(idx + 1, remainder.length()); if (valString.charAt(0) == ':') { controlValue = ByteString.valueOf(valString.substring(1, valString.length())); } else if (valString.charAt(0) == '<') { // Read data from the file. final String filePath = valString.substring(1, valString.length()); try { final byte[] val = readBytesFromFile(filePath); controlValue = ByteString.wrap(val); } catch (final Exception e) { return null; } } else { controlValue = ByteString.valueOf(valString); } return GenericControl.newControl(controlOID, controlCriticality, controlValue); } /** * Prints a multi-line error message with the provided information to the * given print stream. * * @param app * The console app to use to write the error message. * @param ere * The error result. * @return The error code. */ static int printErrorMessage(final ConsoleApplication app, final ErrorResultException ere) { // if ((ere.getMessage() != null) && (ere.getMessage().length() > // 0)) // { // app.println(LocalizableMessage.raw(ere.getMessage())); // } if (ere.getResult().getResultCode().intValue() >= 0) { app.println(ERR_TOOL_RESULT_CODE.get(ere.getResult().getResultCode().intValue(), ere .getResult().getResultCode().toString())); } if ((ere.getResult().getDiagnosticMessage() != null) && (ere.getResult().getDiagnosticMessage().length() > 0)) { app.println(ERR_TOOL_ERROR_MESSAGE.get(ere.getResult().getDiagnosticMessage())); } if (ere.getResult().getMatchedDN() != null && ere.getResult().getMatchedDN().length() > 0) { app.println(ERR_TOOL_MATCHED_DN.get(ere.getResult().getMatchedDN())); } if (app.isVerbose() && ere.getResult().getCause() != null) { ere.getResult().getCause().printStackTrace(app.getErrorStream()); } return ere.getResult().getResultCode().intValue(); } static void printPasswordPolicyResults(final ConsoleApplication app, final Connection connection) { if (connection instanceof AuthenticatedConnection) { final AuthenticatedConnection conn = (AuthenticatedConnection) connection; final BindResult result = conn.getAuthenticatedBindResult(); try { final AuthorizationIdentityResponseControl control = result.getControl(AuthorizationIdentityResponseControl.DECODER, new DecodeOptions()); if (control != null) { final LocalizableMessage message = INFO_BIND_AUTHZID_RETURNED.get(control.getAuthorizationID()); app.println(message); } } catch (final DecodeException e) { app.println(ERR_DECODE_CONTROL_FAILURE.get(e.getLocalizedMessage())); } try { final PasswordExpiredResponseControl control = result.getControl(PasswordExpiredResponseControl.DECODER, new DecodeOptions()); if (control != null) { final LocalizableMessage message = INFO_BIND_PASSWORD_EXPIRED.get(); app.println(message); } } catch (final DecodeException e) { app.println(ERR_DECODE_CONTROL_FAILURE.get(e.getLocalizedMessage())); } try { final PasswordExpiringResponseControl control = result.getControl(PasswordExpiringResponseControl.DECODER, new DecodeOptions()); if (control != null) { final LocalizableMessage timeString = Utils.secondsToTimeString(control.getSecondsUntilExpiration()); final LocalizableMessage message = INFO_BIND_PASSWORD_EXPIRING.get(timeString); app.println(message); } } catch (final DecodeException e) { app.println(ERR_DECODE_CONTROL_FAILURE.get(e.getLocalizedMessage())); } try { final PasswordPolicyResponseControl control = result.getControl(PasswordPolicyResponseControl.DECODER, new DecodeOptions()); if (control != null) { final PasswordPolicyErrorType errorType = control.getErrorType(); if (errorType == PasswordPolicyErrorType.PASSWORD_EXPIRED) { final LocalizableMessage message = INFO_BIND_PASSWORD_EXPIRED.get(); app.println(message); } else if (errorType == PasswordPolicyErrorType.ACCOUNT_LOCKED) { final LocalizableMessage message = INFO_BIND_ACCOUNT_LOCKED.get(); app.println(message); } else if (errorType == PasswordPolicyErrorType.CHANGE_AFTER_RESET) { final LocalizableMessage message = INFO_BIND_MUST_CHANGE_PASSWORD.get(); app.println(message); } final PasswordPolicyWarningType warningType = control.getWarningType(); if (warningType == PasswordPolicyWarningType.TIME_BEFORE_EXPIRATION) { final LocalizableMessage timeString = Utils.secondsToTimeString(control.getWarningValue()); final LocalizableMessage message = INFO_BIND_PASSWORD_EXPIRING.get(timeString); app.println(message); } else if (warningType == PasswordPolicyWarningType.GRACE_LOGINS_REMAINING) { final LocalizableMessage message = INFO_BIND_GRACE_LOGINS_REMAINING.get(control.getWarningValue()); app.println(message); } } } catch (final DecodeException e) { app.println(ERR_DECODE_CONTROL_FAILURE.get(e.getLocalizedMessage())); } } } /** * Read the data from the specified file and return it in a byte array. * * @param filePath * The path to the file that should be read. * @return A byte array containing the contents of the requested file. * @throws IOException * If a problem occurs while trying to read the specified file. */ static byte[] readBytesFromFile(final String filePath) throws IOException { byte[] val = null; FileInputStream fis = null; try { final File file = new File(filePath); fis = new FileInputStream(file); final long length = file.length(); val = new byte[(int) length]; // Read in the bytes int offset = 0; int numRead = 0; while (offset < val.length && (numRead = fis.read(val, offset, val.length - offset)) >= 0) { offset += numRead; } // Ensure all the bytes have been read in if (offset < val.length) { throw new IOException("Could not completely read file " + filePath); } return val; } finally { if (fis != null) { fis.close(); } } } /** * Retrieves a user-friendly string that indicates the length of time (in * days, hours, minutes, and seconds) in the specified number of seconds. * * @param numSeconds * The number of seconds to be converted to a more user-friendly * value. * @return The user-friendly representation of the specified number of * seconds. */ static LocalizableMessage secondsToTimeString(final int numSeconds) { if (numSeconds < 60) { // We can express it in seconds. return INFO_TIME_IN_SECONDS.get(numSeconds); } else if (numSeconds < 3600) { // We can express it in minutes and seconds. final int m = numSeconds / 60; final int s = numSeconds % 60; return INFO_TIME_IN_MINUTES_SECONDS.get(m, s); } else if (numSeconds < 86400) { // We can express it in hours, minutes, and seconds. final int h = numSeconds / 3600; final int m = (numSeconds % 3600) / 60; final int s = numSeconds % 3600 % 60; return INFO_TIME_IN_HOURS_MINUTES_SECONDS.get(h, m, s); } else { // We can express it in days, hours, minutes, and seconds. final int d = numSeconds / 86400; final int h = (numSeconds % 86400) / 3600; final int m = (numSeconds % 86400 % 3600) / 60; final int s = numSeconds % 86400 % 3600 % 60; return INFO_TIME_IN_DAYS_HOURS_MINUTES_SECONDS.get(d, h, m, s); } } /** * Sets default system property settings for the xxxrate performance tools. */ static void setDefaultPerfToolProperties() { // Use SameThreadStrategy by default. if (System.getProperty("org.forgerock.opendj.transport.useWorkerThreads") == null) { System.setProperty("org.forgerock.opendj.transport.useWorkerThreads", "false"); } // Configure connections to be terminate immediately after closing (this // prevents port exhaustion in xxxrate tools when // connecting/disconnecting). if (System.getProperty("org.forgerock.opendj.transport.linger") == null) { System.setProperty("org.forgerock.opendj.transport.linger", "0"); } } /** * Inserts line breaks into the provided buffer to wrap text at no more than * the specified column width. Wrapping will only be done at space * boundaries and if there are no spaces within the specified width, then * wrapping will be performed at the first space after the specified column. * * @param message * The message to be wrapped. * @param width * The maximum number of characters to allow on a line if there * is a suitable breaking point. * @return The wrapped text. */ static String wrapText(final LocalizableMessage message, final int width) { return wrapText(message.toString(), width, 0); } /** * Inserts line breaks into the provided buffer to wrap text at no more than * the specified column width. Wrapping will only be done at space * boundaries and if there are no spaces within the specified width, then * wrapping will be performed at the first space after the specified column. * In addition each line will be indented by the specified amount. * * @param message * The message to be wrapped. * @param width * The maximum number of characters to allow on a line if there * is a suitable breaking point (including any indentation). * @param indent * The number of columns to indent each line. * @return The wrapped text. */ static String wrapText(final LocalizableMessage message, final int width, final int indent) { return wrapText(message.toString(), width, indent); } /** * Inserts line breaks into the provided buffer to wrap text at no more than * the specified column width. Wrapping will only be done at space * boundaries and if there are no spaces within the specified width, then * wrapping will be performed at the first space after the specified column. * * @param text * The text to be wrapped. * @param width * The maximum number of characters to allow on a line if there * is a suitable breaking point. * @return The wrapped text. */ static String wrapText(final String text, final int width) { return wrapText(text, width, 0); } /** * Inserts line breaks into the provided buffer to wrap text at no more than * the specified column width. Wrapping will only be done at space * boundaries and if there are no spaces within the specified width, then * wrapping will be performed at the first space after the specified column. * In addition each line will be indented by the specified amount. * * @param text * The text to be wrapped. * @param width * The maximum number of characters to allow on a line if there * is a suitable breaking point (including any indentation). * @param indent * The number of columns to indent each line. * @return The wrapped text. */ static String wrapText(final String text, int width, final int indent) { // Calculate the real width and indentation padding. width -= indent; final StringBuilder pb = new StringBuilder(); for (int i = 0; i < indent; i++) { pb.append(' '); } final String padding = pb.toString(); final StringBuilder buffer = new StringBuilder(); if (text != null) { final StringTokenizer lineTokenizer = new StringTokenizer(text, "\r\n", true); while (lineTokenizer.hasMoreTokens()) { final String line = lineTokenizer.nextToken(); if (line.equals("\r") || line.equals("\n")) { // It's an end-of-line character, so append it as-is. buffer.append(line); } else if (line.length() <= width) { // The line fits in the specified width, so append it as-is. buffer.append(padding); buffer.append(line); } else { // The line doesn't fit in the specified width, so it needs // to // be // wrapped. Do so at space boundaries. StringBuilder lineBuffer = new StringBuilder(); StringBuilder delimBuffer = new StringBuilder(); final StringTokenizer wordTokenizer = new StringTokenizer(line, " ", true); while (wordTokenizer.hasMoreTokens()) { final String word = wordTokenizer.nextToken(); if (word.equals(" ")) { // It's a space, so add it to the delim buffer only // if the // line // buffer is not empty. if (lineBuffer.length() > 0) { delimBuffer.append(word); } } else if (word.length() > width) { // This is a long word that can't be wrapped, so // we'll // just have // to make do. if (lineBuffer.length() > 0) { buffer.append(padding); buffer.append(lineBuffer); buffer.append(EOL); lineBuffer = new StringBuilder(); } buffer.append(padding); buffer.append(word); if (wordTokenizer.hasMoreTokens()) { // The next token must be a space, so remove it. // If // there are // still more tokens after that, then append an // EOL. wordTokenizer.nextToken(); if (wordTokenizer.hasMoreTokens()) { buffer.append(EOL); } } if (delimBuffer.length() > 0) { delimBuffer = new StringBuilder(); } } else { // It's not a space, so see if we can fit it on the // curent // line. final int newLineLength = lineBuffer.length() + delimBuffer.length() + word.length(); if (newLineLength < width) { // It does fit on the line, so add it. lineBuffer.append(delimBuffer).append(word); if (delimBuffer.length() > 0) { delimBuffer = new StringBuilder(); } } else { // It doesn't fit on the line, so end the // current line // and start // a new one. buffer.append(padding); buffer.append(lineBuffer); buffer.append(EOL); lineBuffer = new StringBuilder(); lineBuffer.append(word); if (delimBuffer.length() > 0) { delimBuffer = new StringBuilder(); } } } } // If there's anything left in the line buffer, then add it // to // the // final buffer. buffer.append(padding); buffer.append(lineBuffer); } } } return buffer.toString(); } // Prevent instantiation. private Utils() { // Do nothing. } }