Bug 501283 - Lots of hash collisions during indexing (legacy indexer)

backport to 4.6.x branch, original commit by Stefan Xenos <sxenos@gmail.com>

Change-Id: Iff29aebed516daee8028e3872199e98c5becc72e
Signed-off-by: Igor Fedorenko <igor@ifedorenko.com>
diff --git a/org.eclipse.jdt.core.tests.compiler/src/org/eclipse/jdt/core/tests/compiler/regression/AbstractRegressionTest.java b/org.eclipse.jdt.core.tests.compiler/src/org/eclipse/jdt/core/tests/compiler/regression/AbstractRegressionTest.java
index 052a8c1..d2e0f7e 100644
--- a/org.eclipse.jdt.core.tests.compiler/src/org/eclipse/jdt/core/tests/compiler/regression/AbstractRegressionTest.java
+++ b/org.eclipse.jdt.core.tests.compiler/src/org/eclipse/jdt/core/tests/compiler/regression/AbstractRegressionTest.java
@@ -35,12 +35,15 @@
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 import java.util.StringTokenizer;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 import javax.annotation.processing.AbstractProcessor;
 import javax.annotation.processing.Processor;
@@ -88,8 +91,6 @@
 import org.eclipse.jdt.internal.core.util.Messages;
 import org.osgi.framework.Bundle;
 
-import java.util.regex.Pattern;
-
 @SuppressWarnings({ "unchecked", "rawtypes" })
 public abstract class AbstractRegressionTest extends AbstractCompilerTest implements StopableTestCase {
 
@@ -948,10 +949,11 @@
 		String computedProblemLog = Util.convertToIndependantLineDelimiter(requestor.problemLog.toString());
 		if (this.shouldSwallowCaptureId)
 			computedProblemLog = Pattern.compile("capture#(\\d+)").matcher(computedProblemLog).replaceAll("capture");
-		  
+
+		ProblemLog problemLog = new ProblemLog(computedProblemLog);
 		int i;
 		for (i = 0; i < alternatePlatformIndependantExpectedLogs.length; i++) {
-			if (alternatePlatformIndependantExpectedLogs[i].equals(computedProblemLog))
+			if (problemLog.sameAs(alternatePlatformIndependantExpectedLogs[i]))
 				return; // OK
 		}
 		logTestTitle();
@@ -962,6 +964,48 @@
 		}
     }
 
+	/**
+	 * Used for performing order-independent log comparisons.
+	 */
+	private static final class ProblemLog {
+		final Set<String> logEntry = new HashSet<>();
+
+		public ProblemLog(String log) {
+			String[] entries = log.split("----------\n");
+			Pattern pattern = Pattern.compile("\\A(\\d*\\. )");
+
+			for (String entry : entries) {
+				Matcher matcher = pattern.matcher(entry);
+				if (matcher.find()) {
+					entry = entry.substring(matcher.end());
+				}
+				this.logEntry.add(entry);
+			}
+		}
+
+		public boolean sameAs(String toTest) {
+			ProblemLog log = new ProblemLog(toTest);
+			return equals(log);
+		}
+
+		@Override
+		public int hashCode() {
+			return this.logEntry.hashCode();
+		}
+
+		@Override
+		public boolean equals(Object obj) {
+			if (this == obj)
+				return true;
+			if (obj == null)
+				return false;
+			if (getClass() != obj.getClass())
+				return false;
+			ProblemLog other = (ProblemLog) obj;
+			return this.logEntry.equals(other.logEntry);
+		}
+	}
+
 	protected void dualPrintln(String message) {
 		System.out.println(message);
 		javacFullLog.println(message);
diff --git a/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/AbstractJavaModelTests.java b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/AbstractJavaModelTests.java
index a04e279..674ad0c 100644
--- a/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/AbstractJavaModelTests.java
+++ b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/AbstractJavaModelTests.java
@@ -365,8 +365,19 @@
 	protected void assertSearchResults(String message, String expected, Object collector) {
 		assertSearchResults(message, expected, collector, true /* assertion */);
 	}
-	protected void assertSearchResults(String message, String expected, Object collector, boolean assertion) {
-		String actual = collector.toString();
+	private static String sortLines(String toSplit) {
+		String[] split = toSplit.split("\n");
+		Arrays.sort(split);
+		StringBuilder reJoined = new StringBuilder();
+		for (int i = 0; i < split.length; i++) {
+			reJoined.append(split[i]);
+			if (i < split.length -1) reJoined.append('\n');
+		}
+		return reJoined.toString();
+	}
+	protected void assertSearchResults(String message, String expectedString, Object collector, boolean assertion) {
+		String expected = sortLines(expectedString);
+		String actual = sortLines(collector.toString());
 		if (!expected.equals(actual)) {
 			if (this.displayName) System.out.println(getName()+" actual result is:");
 			System.out.print(displayString(actual, this.tabs));
@@ -656,6 +667,9 @@
 		assertElementsEqual(message, expected, elements, false/*don't show key*/);
 	}
 	protected void assertElementsEqual(String message, String expected, IJavaElement[] elements, boolean showResolvedInfo) {
+		assertElementsEqual(message, expected, elements, showResolvedInfo, false);
+	}
+	protected void assertElementsEqual(String message, String expected, IJavaElement[] elements, boolean showResolvedInfo, boolean sorted) {
 		StringBuffer buffer = new StringBuffer();
 		if (elements != null) {
 			for (int i = 0, length = elements.length; i < length; i++){
@@ -671,6 +685,9 @@
 			buffer.append("<null>");
 		}
 		String actual = buffer.toString();
+		if (sorted) {
+			actual = sortLines(actual);
+		}
 		if (!expected.equals(actual)) {
 			if (this.displayName) System.out.println(getName()+" actual result is:");
 			System.out.println(displayString(actual, this.tabs) + this.endChar);
diff --git a/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/JavadocMethodCompletionModelTest.java b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/JavadocMethodCompletionModelTest.java
index 8278f54..50bad4c 100644
--- a/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/JavadocMethodCompletionModelTest.java
+++ b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/JavadocMethodCompletionModelTest.java
@@ -338,7 +338,7 @@
 		"	}\n" +
 		"}\n";
 	completeInJavadoc("/Completion/src/javadoc/methods/tags/BasicTestMethods.java", source, true, "I");
-	assertResults(
+	assertSortedResults(
 		"IllegalMonitorStateException[TYPE_REF]{IllegalMonitorStateException, java.lang, Ljava.lang.IllegalMonitorStateException;, null, null, "+this.positions+R_DRICUNRE+"}\n" +
 		"InterruptedException[TYPE_REF]{InterruptedException, java.lang, Ljava.lang.InterruptedException;, null, null, "+this.positions+R_DRICUNRE+"}"
 	);
@@ -357,9 +357,9 @@
 		"	}\n" +
 		"}\n";
 	completeInJavadoc("/Completion/src/javadoc/methods/tags/BasicTestMethods.java", source, true, "java.lang.I");
-	assertResults(
-		"IllegalMonitorStateException[TYPE_REF]{IllegalMonitorStateException, java.lang, Ljava.lang.IllegalMonitorStateException;, null, null, "+this.positions+R_DRICNRE+"}\n" +
-		"InterruptedException[TYPE_REF]{InterruptedException, java.lang, Ljava.lang.InterruptedException;, null, null, "+this.positions+R_DRICNREEET+"}"
+	assertSortedResults(
+		"InterruptedException[TYPE_REF]{InterruptedException, java.lang, Ljava.lang.InterruptedException;, null, null, "+this.positions+R_DRICNREEET+"}\n" +
+		"IllegalMonitorStateException[TYPE_REF]{IllegalMonitorStateException, java.lang, Ljava.lang.IllegalMonitorStateException;, null, null, "+this.positions+R_DRICNRE+"}"
 	);
 }
 
diff --git a/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/ResolveTests2.java b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/ResolveTests2.java
index fc42448..0c82ecd 100644
--- a/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/ResolveTests2.java
+++ b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/ResolveTests2.java
@@ -926,7 +926,9 @@
 			"Unexpected elements",
 			"IResource [in IResource.class [in test1 [in "+outputDirectory+File.separator+"bug232880a.jar]]]\n" + 
 			"IResource [in IResource.class [in test2 [in "+outputDirectory+File.separator+"bug232880a.jar]]]",
-			elements
+			elements,
+			false,
+			true
 		);
 	} finally {
 		this.deleteExternalFile(externalJar1);
diff --git a/org.eclipse.jdt.core/compiler/org/eclipse/jdt/core/compiler/CharOperation.java b/org.eclipse.jdt.core/compiler/org/eclipse/jdt/core/compiler/CharOperation.java
index 7eafeee..23b3817 100644
--- a/org.eclipse.jdt.core/compiler/org/eclipse/jdt/core/compiler/CharOperation.java
+++ b/org.eclipse.jdt.core/compiler/org/eclipse/jdt/core/compiler/CharOperation.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2000, 2015 IBM Corporation and others.
+ * Copyright (c) 2000, 2016 IBM Corporation and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
@@ -9,9 +9,12 @@
  *     IBM Corporation - initial API and implementation
  *     Luiz-Otavio Zorzella <zorzella at gmail dot com> - Improve CamelCase algorithm
  *     Gábor Kövesdán - Contribution for Bug 350000 - [content assist] Include non-prefix matches in auto-complete suggestions
+ *     Stefan Xenos <sxenos@gmail.com> (Google) - Bug 501283 - Lots of hash collisions during indexing
  *******************************************************************************/
 package org.eclipse.jdt.core.compiler;
 
+import java.util.Arrays;
+
 import org.eclipse.jdt.internal.compiler.parser.ScannerHelper;
 
 /**
@@ -2281,19 +2284,9 @@
  *
  * @param array the array for which a hashcode is required
  * @return the hashcode
- * @throws NullPointerException if array is null
  */
 public static final int hashCode(char[] array) {
-	int length = array.length;
-	int hash = length == 0 ? 31 : array[0];
-	if (length < 8) {
-		for (int i = length; --i > 0;)
-			hash = (hash * 31) + array[i];
-	} else {
-		// 8 characters is enough to compute a decent hash code, don't waste time examining every character
-		for (int i = length - 1, last = i > 16 ? i - 16 : 0; i > last; i -= 2)
-			hash = (hash * 31) + array[i];
-	}
+	int hash = Arrays.hashCode(array);
 	return hash & 0x7FFFFFFF;
 }