Index: bin/jmeter.properties =================================================================== --- bin/jmeter.properties (revision 440277) +++ bin/jmeter.properties (working copy) @@ -407,6 +407,16 @@ #beanshell.server.port=9000 # The telnet server will be started on the next port +#--------------------------------------------------------------------------- +# LDAP Sampler configuration +#--------------------------------------------------------------------------- +# Maximum number of search results returned by a search that will be sorted +# to guarantee a stable ordering (if more results then this limit are retruned +# then no sorting is done). Set to 0 to turn off all sorting, in which case +# "Equals" response assertions will be very likely to fail against search results. +#ldapsampler.max_sorted_results=2000 +ldapsampler.max_sorted_results=2000 + # # Define the server initialisation file beanshell.server.file=../extras/startup.bsh @@ -507,10 +517,19 @@ #Should JMeter expand the tree when loading a test plan? #onload.expandtree=false +# Number of characters to log for each of three sections (starting matching section, diff section, +# ending matching section where not all sections will appear for all diffs) diff display when an Equals +# assertion fails. So a value of 100 means a maximum of 300 characters of diff text will be displayed +# (+ a number of extra characters like "..." and "[[["/"]]]" which are used to decorate it). +#assertion.equals_section_diff_len=100 +# test written out to log to signify start/end of diff delta +#assertion.equals_diff_delta_start=[[[ +#assertion.equals_diff_delta_end=]]] + # Should JMeter automatically load additional JMeter properties? # File name to look for (comment to disable) user.properties=user.properties # Should JMeter automatically load additional system properties? # File name to look for (comment to disable) -system.properties=system.properties \ No newline at end of file +system.properties=system.properties Index: src/components/org/apache/jmeter/assertions/gui/AssertionGui.java =================================================================== --- src/components/org/apache/jmeter/assertions/gui/AssertionGui.java (revision 440277) +++ src/components/org/apache/jmeter/assertions/gui/AssertionGui.java (working copy) @@ -81,6 +81,11 @@ private JRadioButton matchesBox; /** + * Radio button indicating if the field equals the first pattern. + */ + private JRadioButton equalsBox; + + /** * Checkbox indicating to test that the field does NOT contain/match the * patterns. */ @@ -142,11 +147,13 @@ if (containsBox.isSelected()) { ra.setToContainsType(); + } else if (matchesBox.isSelected()) { + ra.setToMatchType(); } else { - ra.setToMatchType(); - } + ra.setToEqualsType(); + } - if (notBox.isSelected()) { + if (notBox.isSelected()) { ra.setToNotType(); } else { ra.unsetNotType(); @@ -170,10 +177,16 @@ if (model.isContainsType()) { containsBox.setSelected(true); matchesBox.setSelected(false); + equalsBox.setSelected(false); + } else if (model.isMatchType()) { + containsBox.setSelected(false); + matchesBox.setSelected(true); + equalsBox.setSelected(false); } else { containsBox.setSelected(false); - matchesBox.setSelected(true); - } + matchesBox.setSelected(false); + equalsBox.setSelected(true); + } if (model.isNotType()) { notBox.setSelected(true); @@ -279,7 +292,11 @@ group.add(matchesBox); panel.add(matchesBox); - notBox = new JCheckBox(JMeterUtils.getResString("assertion_not")); + equalsBox = new JRadioButton(JMeterUtils.getResString("assertion_equals")); + group.add(equalsBox); + panel.add(equalsBox); + + notBox = new JCheckBox(JMeterUtils.getResString("assertion_not")); panel.add(notBox); return panel; Index: src/components/org/apache/jmeter/assertions/ResponseAssertion.java =================================================================== --- src/components/org/apache/jmeter/assertions/ResponseAssertion.java (revision 440277) +++ src/components/org/apache/jmeter/assertions/ResponseAssertion.java (working copy) @@ -28,6 +28,7 @@ import org.apache.jmeter.testelement.property.NullProperty; import org.apache.jmeter.testelement.property.PropertyIterator; import org.apache.jmeter.testelement.property.StringProperty; +import org.apache.jmeter.util.JMeterUtils; import org.apache.jorphan.logging.LoggingManager; import org.apache.jorphan.util.JOrphanUtils; import org.apache.log.Logger; @@ -72,21 +73,35 @@ private final static int NOT = 1 << 2; - private static ThreadLocal matcher = new ThreadLocal() { + private final static int EQUALS = 1 << 3; + + private static ThreadLocal matcher = new ThreadLocal() { protected Object initialValue() { return new Perl5Matcher(); } }; - private static final PatternCacheLRU patternCache = new PatternCacheLRU(1000, new Perl5Compiler()); + protected static final int EQUALS_SECTION_DIFF_LEN + = JMeterUtils.getPropDefault("assertion.equals_section_diff_len", 100); - /*************************************************************************** - * !ToDo (Constructor description) - **************************************************************************/ - public ResponseAssertion() { - setProperty(new CollectionProperty(TEST_STRINGS, new ArrayList())); - } + /** Signifies truncated text in diff display. */ + private static final String EQUALS_DIFF_TRUNC = "..."; + private static final PatternCacheLRU patternCache = new PatternCacheLRU(1000, new Perl5Compiler()); + private static final String RECEIVED_STR = "****** received : "; + private static final String COMPARISON_STR = "****** comparison: "; + private static final String DIFF_DELTA_START + = JMeterUtils.getPropDefault("assertion.equals_diff_delta_start", "[[["); + private static final String DIFF_DELTA_END + = JMeterUtils.getPropDefault("assertion.equals_diff_delta_end", "]]]"); + + /*************************************************************************** + * !ToDo (Constructor description) + **************************************************************************/ + public ResponseAssertion() { + setProperty(new CollectionProperty(TEST_STRINGS, new ArrayList())); + } + /*************************************************************************** * !ToDo (Constructor description) * @@ -217,6 +232,10 @@ return (CollectionProperty) getProperty(TEST_STRINGS); } + public boolean isEqualsType() { + return (getTestType() & EQUALS) > 0; + } + public boolean isContainsType() { return (getTestType() & CONTAINS) > 0; } @@ -230,14 +249,18 @@ } public void setToContainsType() { - setTestType((getTestType() | CONTAINS) & (~MATCH)); + setTestType((getTestType() | CONTAINS) & ~ (MATCH | EQUALS)); } public void setToMatchType() { - setTestType((getTestType() | MATCH) & (~CONTAINS)); + setTestType((getTestType() | MATCH) & ~(CONTAINS | EQUALS)); } - public void setToNotType() { + public void setToEqualsType() { + setTestType((getTestType() | EQUALS) & ~(MATCH | CONTAINS)); + } + + public void setToNotType() { setTestType((getTestType() | NOT)); } @@ -294,33 +317,48 @@ result.setError(false); boolean contains = isContainsType(); // do it once outside loop - boolean debugEnabled = log.isDebugEnabled(); + boolean equals = isEqualsType(); + boolean debugEnabled = log.isDebugEnabled(); if (debugEnabled){ log.debug("Type:" + (contains?"Contains":"Match") + (not? "(not)": "")); } - - try { + + try { // Get the Matcher for this thread Perl5Matcher localMatcher = (Perl5Matcher) matcher.get(); PropertyIterator iter = getTestStrings().iterator(); while (iter.hasNext()) { String stringPattern = iter.next().getStringValue(); - Pattern pattern = patternCache.getPattern(stringPattern, Perl5Compiler.READ_ONLY_MASK); - boolean found; - if (contains) { - found = localMatcher.contains(toCheck, pattern); - } else { - found = localMatcher.matches(toCheck, pattern); - } - pass = not ? !found : found; + boolean found; + + if (equals) { + found = toCheck.equals(stringPattern); + } else { + Pattern pattern = patternCache.getPattern(stringPattern, Perl5Compiler.READ_ONLY_MASK); + if (contains) { + found = localMatcher.contains(toCheck, pattern); + } else { + found = localMatcher.matches(toCheck, pattern); + } + } + pass = not ? !found : found; if (!pass) { - if (debugEnabled){log.debug("Failed: "+pattern);} - result.setFailure(true); - result.setFailureMessage(getFailText(stringPattern)); + String failText = getFailText(stringPattern); + + if (equals) { + final String compText = equalsComparisonText(toCheck, stringPattern); + + failText = failText.replaceFirst("/.*", "/ " + compText); + } + log.info(failText); + result.setFailure(true); + result.setFailureMessage(failText); break; } - if (debugEnabled){log.debug("Passed: "+pattern);} - } + if (debugEnabled){log.debug("Passed: (" + getName() + ") /" + stringPattern);} + if (equals) + break; + } } catch (MalformedCachePatternException e) { result.setError(true); result.setFailure(false); @@ -329,43 +367,186 @@ return result; } - /** - * Generate the failure reason from the TestType - * - * @param stringPattern - * @return the message for the assertion report - */ - // TODO strings should be resources - private String getFailText(String stringPattern) { - String text; - String what; - if (ResponseAssertion.RESPONSE_DATA.equals(getTestField())) { - what = "text"; - } else if (ResponseAssertion.RESPONSE_CODE.equals(getTestField())) { - what = "code"; - } else if (ResponseAssertion.RESPONSE_MESSAGE.equals(getTestField())) { - what = "message"; - } else // Assume it is the URL - { - what = "URL"; - } - switch (getTestType()) { - case CONTAINS: - text = " expected to contain "; - break; - case NOT | CONTAINS: - text = " expected not to contain "; - break; - case MATCH: - text = " expected to match "; - break; - case NOT | MATCH: - text = " expected not to match "; - break; - default:// should never happen... - text = " expected something using "; - } + private static String trunc(final boolean right, final String str) + { + if (str.length() <= EQUALS_SECTION_DIFF_LEN) + return str; + else if (right) + return str.substring(0, EQUALS_SECTION_DIFF_LEN) + EQUALS_DIFF_TRUNC; + else + return EQUALS_DIFF_TRUNC + str.substring(str.length() - EQUALS_SECTION_DIFF_LEN, str.length()); + } - return "Test failed, " + what + text + "/" + stringPattern + "/"; - } + /** + * Returns some helpful logging text to determine where equality between two strings + * is broken, with one pointer working from the front of the strings and another working + * backwards from the end. + * + * @param received String received from sampler. + * @param comparison String specified for "equals" response assertion. + * @return Two lines of text separated by newlines, and then forward and backward pointers + * denoting first position of difference. + */ + private static String equalsComparisonText(final String received, final String comparison) + { + final StringBuffer text; + int firstDiff; + int lastRecDiff = -1; + int lastCompDiff = -1; + final int recLength = received.length(); + final int compLength = comparison.length(); + final int minLength = Math.min(recLength, compLength); + final String startingEqSeq; + String recDeltaSeq = ""; + String compDeltaSeq = ""; + String endingEqSeq = ""; + final StringBuffer pad; + + + text = new StringBuffer(Math.max(recLength, compLength) * 2); + for (firstDiff = 0; firstDiff < minLength; firstDiff++) + if (received.charAt(firstDiff) != comparison.charAt(firstDiff)) + break; + if (firstDiff == 0) + startingEqSeq = ""; + else + startingEqSeq = trunc(false, received.substring(0, firstDiff)); + + lastRecDiff = recLength - 1; + lastCompDiff = compLength - 1; + + while ((lastRecDiff > firstDiff) && (lastCompDiff > firstDiff) + && received.charAt(lastRecDiff) == comparison.charAt(lastCompDiff)) + { + lastRecDiff--; + lastCompDiff--; + } + endingEqSeq = trunc(true, received.substring(lastRecDiff + 1, recLength)); + if (endingEqSeq.length() == 0) + { + recDeltaSeq = trunc(true, received.substring(firstDiff, recLength)); + compDeltaSeq = trunc(true, comparison.substring(firstDiff, compLength)); + } + else + { + recDeltaSeq = trunc(true, received.substring(firstDiff, lastRecDiff + 1)); + compDeltaSeq = trunc(true, comparison.substring(firstDiff, lastCompDiff + 1)); + } + pad = new StringBuffer(Math.abs(recDeltaSeq.length() - compDeltaSeq.length())); + for (int i = 0; i < pad.capacity(); i++) + pad.append(' '); + if (recDeltaSeq.length() > compDeltaSeq.length()) + compDeltaSeq += pad.toString(); + else + recDeltaSeq += pad.toString(); + + text.append("\n\n"); + text.append(RECEIVED_STR); + text.append(startingEqSeq); + text.append(DIFF_DELTA_START); + text.append(recDeltaSeq); + text.append(DIFF_DELTA_END); + text.append(endingEqSeq); + text.append("\n\n"); + text.append(COMPARISON_STR); + text.append(startingEqSeq); + text.append(DIFF_DELTA_START); + text.append(compDeltaSeq); + text.append(DIFF_DELTA_END); + text.append(endingEqSeq); + text.append("\n\n"); + return text.toString(); + } + +// public static void main(String[] args) +// { +// final String longA = +//"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + +//"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +// final String longZ = +//"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + ; +//"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"; +// String[][] tests = new String[][] { +// new String[] { "aaa", "zzz" }, +// new String[] { "aaa", "aaazzz" }, +// new String[] { "aaaz", "aaazzz" }, +// new String[] { "aaazzz", "aaaz" }, +// new String[] { "aaazzz", "aaabcdezzz" }, +// +// // all long delta +// new String[] { longA, longZ }, +// // all long v. short delta +// new String[] { longA, "yyy" }, +// // all long v. short delta +// new String[] { "yyy", longA }, +// +// // long intial, long delta +// new String[] { longA + longA, longA + longZ }, +// // long intial, long v. short delta +// new String[] { longA + "yyy", longA + longZ }, +// // long intial, long v. short delta +// new String[] { longA + longZ, longA + "yyy" }, +// +// // long intial, long delta, long final +// new String[] { longA + longA + longZ, longA + longZ + longZ}, +// // long intial, long delta v. short delta, long final +// new String[] { longA + longA + longZ, longA + "yyy" + longZ}, +// // long intial, long delta v. short delta, long final +// new String[] { longA + "yyy" + longZ, longA + longZ} +// }; +// +// for (int i = 0; i < tests.length; i++) +// { +// String[] test = tests[i]; +// +// System.out.println(); +// System.out.println(equalsComparisonText(test[0], test[1])); +// } +// } + + /** + * Generate the failure reason from the TestType + * + * @param stringPattern + * @return the message for the assertion report + */ + // TODO strings should be resources + private String getFailText(String stringPattern) { + String text; + String what; + if (ResponseAssertion.RESPONSE_DATA.equals(getTestField())) { + what = "text"; + } else if (ResponseAssertion.RESPONSE_CODE.equals(getTestField())) { + what = "code"; + } else if (ResponseAssertion.RESPONSE_MESSAGE.equals(getTestField())) { + what = "message"; + } else // Assume it is the URL + { + what = "URL"; + } + switch (getTestType()) { + case CONTAINS: + text = " expected to contain "; + break; + case NOT | CONTAINS: + text = " expected not to contain "; + break; + case MATCH: + text = " expected to match "; + break; + case NOT | MATCH: + text = " expected not to match "; + break; + case EQUALS: + text = " expected to equal "; + break; + case NOT | EQUALS: + text = " expected not to equal "; + break; + default:// should never happen... + text = " expected something using "; + } + + return "Test failed, (" + getName() + "): " + what + text + "/" + stringPattern + "/"; + } } Index: src/core/org/apache/jmeter/resources/messages.properties =================================================================== --- src/core/org/apache/jmeter/resources/messages.properties (revision 440277) +++ src/core/org/apache/jmeter/resources/messages.properties (working copy) @@ -46,6 +46,7 @@ assertion_code_resp=Response Code assertion_contains=Contains assertion_matches=Matches +assertion_equals=Equals assertion_message_resp=Response Message assertion_not=Not assertion_pattern_match_rules=Pattern Matching Rules @@ -797,4 +798,4 @@ you_must_enter_a_valid_number=You must enter a valid number zh_cn=Chinese (Simplified) zh_tw=Chinese (Traditional) -# Please add new entries in alphabetical order \ No newline at end of file +# Please add new entries in alphabetical order Index: src/core/org/apache/jmeter/util/JMeterVersion.java =================================================================== --- src/core/org/apache/jmeter/util/JMeterVersion.java (revision 440277) +++ src/core/org/apache/jmeter/util/JMeterVersion.java (working copy) @@ -41,7 +41,7 @@ * JMeterUtils This ensures that JMeterUtils always gets the correct * version, even if it is not re-compiled during the build. */ - private static final String VERSION = "2.1.2"; + private static final String VERSION = "2.1.2.20061004"; static final String COPYRIGHT = "Copyright (c) 1998-2006 The Apache Software Foundation"; Index: src/protocol/ldap/org/apache/jmeter/protocol/ldap/sampler/LDAPExtSampler.java =================================================================== --- src/protocol/ldap/org/apache/jmeter/protocol/ldap/sampler/LDAPExtSampler.java (revision 440277) +++ src/protocol/ldap/org/apache/jmeter/protocol/ldap/sampler/LDAPExtSampler.java (working copy) @@ -17,20 +17,12 @@ package org.apache.jmeter.protocol.ldap.sampler; -import java.util.Hashtable; -import java.util.Iterator; -import java.util.Map; +import java.util.*; +import java.io.UnsupportedEncodingException; import javax.naming.NamingEnumeration; import javax.naming.NamingException; -import javax.naming.directory.Attribute; -import javax.naming.directory.Attributes; -import javax.naming.directory.BasicAttribute; -import javax.naming.directory.BasicAttributes; -import javax.naming.directory.DirContext; -import javax.naming.directory.InitialDirContext; -import javax.naming.directory.ModificationItem; -import javax.naming.directory.SearchResult; +import javax.naming.directory.*; import org.apache.jmeter.config.Argument; import org.apache.jmeter.config.Arguments; @@ -44,6 +36,7 @@ import org.apache.jmeter.testelement.property.PropertyIterator; import org.apache.jmeter.testelement.property.StringProperty; import org.apache.jmeter.testelement.property.TestElementProperty; +import org.apache.jmeter.util.JMeterUtils; import org.apache.jorphan.logging.LoggingManager; import org.apache.log.Logger; @@ -56,6 +49,10 @@ private static final Logger log = LoggingManager.getLoggerForClass(); + /** Signature start of response data generated by this sample. */ + public static final String LDAPANSWER = ""; + public static final String LDAPANSWER_END = LDAPANSWER.replaceFirst("<", ""; + String responseData = LDAPANSWER; SampleResult res = new SampleResult(); res.setResponseData("successfull".getBytes()); res.setResponseMessage("Success"); @@ -761,39 +814,29 @@ responseData = responseData + "" + getPropertyAsString(NEWDN) + ""; renameTest(temp_client, dirContext, res); } else if (getPropertyAsString(TEST).equals(SEARCHBASE)) { - res.setSamplerData("Search with filter " + getPropertyAsString(SEARCHFILTER)); - responseData = responseData + "search"; - responseData = responseData + "" + getPropertyAsString(SEARCHFILTER) + ""; - responseData = responseData + "" + getPropertyAsString(SEARCHBASE) + "," - + getPropertyAsString(ROOTDN) + ""; - responseData = responseData + "" + getPropertyAsString(SCOPE) + ""; - responseData = responseData + "" + getPropertyAsString(COUNTLIM) + ""; - responseData = responseData + "" + getPropertyAsString(TIMELIM) + ""; - responseData = responseData + ""; + final StringBuffer sb = new StringBuffer(2 * 1024); + final String scopeStr = getPropertyAsString(SCOPE); + int scope; + + res.setSamplerData("Search with filter " + getPropertyAsString(SEARCHFILTER)); + sb.append(responseData); + sb.append(""); + writeSearchResponseHeader(sb); + scope = searchScopeFromString(scopeStr); + if (scope < 0) + // for backwards compatibility + scope = getPropertyAsInt(SCOPE); + res.sampleStart(); NamingEnumeration srch = temp_client.searchTest(dirContext, getPropertyAsString(SEARCHBASE), getPropertyAsString(SEARCHFILTER), - getPropertyAsInt(SCOPE), getPropertyAsLong(COUNTLIM), getPropertyAsInt(TIMELIM), + scope, getPropertyAsLong(COUNTLIM), getPropertyAsInt(TIMELIM), getRequestAttributes(getPropertyAsString(ATTRIBS)), getPropertyAsBoolean(RETOBJ), getPropertyAsBoolean(DEREF)); res.sampleEnd(); - while (srch.hasMore()) { - SearchResult sr = (SearchResult) srch.next(); - responseData = responseData + "" + sr.getName() + "," + getPropertyAsString(SEARCHBASE) + "," - + getRootdn() + ""; - responseData = responseData + "" + sr.getAttributes().size() + ""; - NamingEnumeration attrlist = sr.getAttributes().getIDs(); - while (attrlist.hasMore()) { - String iets = (String) attrlist.next(); - responseData = responseData + "" + iets - + ""; - responseData = responseData - + "" - + sr.getAttributes().get(iets).toString().substring( - iets.length() + 2) + ""; - } - } - responseData = responseData + ""; - } + writeSearchResults(sb, srch); + sb.append(""); + responseData = sb.toString(); + } } catch (NamingException ex) { String returnData = ex.toString(); @@ -818,10 +861,210 @@ return res; } - public void testStarted() { - testStarted(""); - } + public void writeSearchResponseHeader(final StringBuffer sb) + { + String scopeStr = getPropertyAsString(SCOPE); + sb.append("search"); + sb.append("").append(getPropertyAsString(SEARCHFILTER)).append(""); + sb.append("").append(getPropertyAsString(SEARCHBASE)).append(",") + .append(getPropertyAsString(ROOTDN)).append(""); + if (searchScopeFromString(scopeStr) == -1) + scopeStr = searchScopeToString(getPropertyAsInt(SCOPE)); + sb.append("").append(scopeStr).append(""); + sb.append("").append(getPropertyAsString(COUNTLIM)).append(""); + sb.append("").append(getPropertyAsString(TIMELIM)).append(""); + } + + /** + * Write out search results in a stable order (including order of all subelements which might + * be reordered like attributes and their values) so that simple textual comparison can be done, + * unless the number of results exceeds {@link #MAX_SORTED_RESULTS} in which case just stream + * the results out without sorting. + */ + public void writeSearchResults(final StringBuffer sb, final NamingEnumeration srch) + throws NamingException + { + final ArrayList sortedResults = new ArrayList(MAX_SORTED_RESULTS); + boolean abandonedSort; + final String searchBase = getPropertyAsString(SEARCHBASE); + final String rootDn = getRootdn(); + + sb.append(""); + // read all sortedResults into memory so we can guarantee ordering + while (srch.hasMore() && (sortedResults.size() < MAX_SORTED_RESULTS)) { + final SearchResult sr = (SearchResult) srch.next(); + + // must be done prior to sorting + normaliseSearchDN(sr, searchBase, rootDn); + sortedResults.add(sr); + } + + abandonedSort = (sortedResults.size() >= MAX_SORTED_RESULTS); + if (!abandonedSort) + { + Collections.sort(sortedResults, new Comparator() + { + private int compareToReverse(final String s1, final String s2) + { + int len1 = s1.length(); + int len2 = s2.length(); + int s1i = len1 - 1; + int s2i = len2 - 1; + + for ( ; (s1i >= 0) && (s2i >= 0); s1i--, s2i--) + { + char c1 = s1.charAt(s1i); + char c2 = s2.charAt(s2i); + + if (c1 != c2) + return c1 - c2; + } + return len1 - len2; + } + + public int compare(Object o1, Object o2) + { + String nm1 = ((SearchResult) o1).getName(); + String nm2 = ((SearchResult) o2).getName(); + + if (nm1 == null) + nm1 = ""; + if (nm2 == null) + nm2 = ""; + return compareToReverse(nm1, nm2); + } + }); + } + + for (Iterator it = sortedResults.iterator(); it.hasNext();) + { + final SearchResult sr = (SearchResult) it.next(); + writeSearchResult(sr, sb); + } + + if (abandonedSort) + { + // if abonded sort because there were too many items, then read the + // rest of the results now... + while (srch.hasMore()) { + final SearchResult sr = (SearchResult) srch.next(); + + normaliseSearchDN(sr, searchBase, rootDn); + writeSearchResult(sr, sb); + } + } + sb.append(""); + } + + private void writeSearchResult(final SearchResult sr, final StringBuffer responseData) + throws NamingException + { + String srName = sr.getName(); + final Attributes attrs = sr.getAttributes(); + final ArrayList sortedAttrs; + + responseData.append(""); + responseData.append(""); + responseData.append(sr.getName()); + responseData.append(""); + responseData.append("").append(attrs.size()).append(""); + sortedAttrs = new ArrayList(attrs.size()); + for (NamingEnumeration en = attrs.getAll(); en.hasMore(); ) + { + final Attribute attr = (Attribute) en.next(); + + sortedAttrs.add(attr); + } + Collections.sort(sortedAttrs, new Comparator() + { + public int compare(Object o1, Object o2) + { + String nm1 = ((Attribute) o1).getID(); + String nm2 = ((Attribute) o2).getID(); + + return nm1.compareTo(nm2); + } + }); + for (Iterator ait = sortedAttrs.iterator(); ait.hasNext();) + { + final Attribute attr = (Attribute) ait.next(); + + responseData.append("").append(attr.getID()).append(""); + responseData.append(""); + if (attr.size() == 1) + responseData.append(getWriteValue(attr.get())); + else + { + final ArrayList sortedVals = new ArrayList(attr.size()); + boolean first = true; + + for (NamingEnumeration ven = attr.getAll(); ven.hasMore(); ) + { + final Object value = getWriteValue(ven.next()); + + sortedVals.add(value.toString()); + } + + Collections.sort(sortedVals); + + for (Iterator vit = sortedVals.iterator(); vit.hasNext();) + { + final String value = (String) vit.next(); + + if (first) + first = false; + else + responseData.append(", "); + responseData.append(value); + } + } + responseData.append(""); + } + responseData.append(""); + } + + private String normaliseSearchDN(final SearchResult sr, final String searchBase, final String rootDn) + { + String srName = sr.getName(); + + if (!srName.endsWith(searchBase)) + { + if (srName.length() > 0) + srName = srName + ','; + srName = srName + searchBase; + } + if ((rootDn.length() > 0) && !srName.endsWith(rootDn)) + { + if (srName.length() > 0) + srName = srName + ','; + srName = srName + rootDn; + } + sr.setName(srName); + return srName; + } + + private String getWriteValue(final Object value) + { + if (value instanceof String) + // assume it's senstive data + return (String)value; + else if (value instanceof byte[]) + try + { + return new String((byte[])value, "UTF-8"); + } + catch (UnsupportedEncodingException e) + { + log.error("this can't happen: UTF-8 character encoding not supported", e); + } + return value.toString(); + } + + public void testStarted() { + testStarted(""); + } + public void testEnded() { testEnded(""); }