]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-3072 Add greedy algorithm to track violations based on blocks
authorEvgeny Mandrikov <mandrikov@gmail.com>
Wed, 14 Mar 2012 10:06:30 +0000 (14:06 +0400)
committerEvgeny Mandrikov <mandrikov@gmail.com>
Thu, 15 Mar 2012 10:53:10 +0000 (14:53 +0400)
plugins/sonar-core-plugin/pom.xml
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/timemachine/ReferenceAnalysis.java
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/timemachine/ViolationPair.java [new file with mode: 0644]
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/timemachine/ViolationTrackingBlocksRecognizer.java [new file with mode: 0644]
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/timemachine/ViolationTrackingDecorator.java
plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/timemachine/ReferenceAnalysisTest.java [new file with mode: 0644]
plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/timemachine/ViolationTrackingBlocksRecognizerTest.java [new file with mode: 0644]
plugins/sonar-core-plugin/src/test/resources/org/sonar/plugins/core/timemachine/ReferenceAnalysisTest/shared.xml [new file with mode: 0644]
sonar-batch/pom.xml
sonar-diff/src/main/java/org/sonar/diff/StringText.java [new file with mode: 0644]
sonar-diff/src/main/java/org/sonar/diff/StringTextComparator.java [new file with mode: 0644]

index 88d2101ab66b957e66bff9766a1891a7ef5ccd0f..987bb0e93a834e7b90d16dfd54ec77be0490a7ad 100644 (file)
   <name>Sonar :: Plugins :: Core</name>
 
   <dependencies>
+    <dependency>
+      <groupId>org.codehaus.sonar</groupId>
+      <artifactId>sonar-diff</artifactId>
+      <version>${project.version}</version>
+    </dependency>
     <dependency>
       <groupId>org.codehaus.sonar</groupId>
       <artifactId>sonar-plugin-api</artifactId>
index 224b4bd954a7269543880485c677ff96d3259fd6..c5a44fd82b0b6f61635406e7b8e1763fe019d798 100644 (file)
@@ -24,9 +24,11 @@ import org.sonar.api.database.DatabaseSession;
 import org.sonar.api.database.model.ResourceModel;
 import org.sonar.api.database.model.RuleFailureModel;
 import org.sonar.api.database.model.Snapshot;
+import org.sonar.api.database.model.SnapshotSource;
 import org.sonar.api.resources.Resource;
 
 import javax.persistence.Query;
+
 import java.util.Collections;
 import java.util.List;
 
@@ -46,9 +48,20 @@ public class ReferenceAnalysis implements BatchExtension {
     return Collections.emptyList();
   }
 
-  Snapshot getSnapshot(Resource resource) {
+  public String getSource(Resource resource) {
+    Snapshot snapshot = getSnapshot(resource);
+    if (snapshot != null) {
+      SnapshotSource source = session.getSingleResult(SnapshotSource.class, "snapshotId", snapshot.getId());
+      if (source != null) {
+        return source.getData();
+      }
+    }
+    return "";
+  }
+
+  private Snapshot getSnapshot(Resource resource) {
     Query query = session.createQuery("from " + Snapshot.class.getSimpleName() + " s where s.last=:last and s.resourceId=(select r.id from "
-        + ResourceModel.class.getSimpleName() + " r where r.key=:key)");
+      + ResourceModel.class.getSimpleName() + " r where r.key=:key)");
     query.setParameter("key", resource.getEffectiveKey());
     query.setParameter("last", Boolean.TRUE);
     return session.getSingleResult(query, null);
diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/timemachine/ViolationPair.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/timemachine/ViolationPair.java
new file mode 100644 (file)
index 0000000..9220032
--- /dev/null
@@ -0,0 +1,54 @@
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2012 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * Sonar is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * Sonar 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+ */
+package org.sonar.plugins.core.timemachine;
+
+import org.sonar.api.database.model.RuleFailureModel;
+import org.sonar.api.rules.Violation;
+
+import java.util.Comparator;
+
+public class ViolationPair {
+
+  private final RuleFailureModel pastViolation;
+  private final Violation newViolation;
+  private final int weight;
+
+  public ViolationPair(RuleFailureModel pastViolation, Violation newViolation, int weight) {
+    this.pastViolation = pastViolation;
+    this.newViolation = newViolation;
+    this.weight = weight;
+  }
+
+  public Violation getNewViolation() {
+    return newViolation;
+  }
+
+  public RuleFailureModel getPastViolation() {
+    return pastViolation;
+  }
+
+  public static final Comparator<ViolationPair> COMPARATOR = new Comparator<ViolationPair>() {
+    @Override
+    public int compare(ViolationPair o1, ViolationPair o2) {
+      return o2.weight - o1.weight;
+    }
+  };
+
+}
diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/timemachine/ViolationTrackingBlocksRecognizer.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/timemachine/ViolationTrackingBlocksRecognizer.java
new file mode 100644 (file)
index 0000000..0ba6476
--- /dev/null
@@ -0,0 +1,75 @@
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2012 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * Sonar is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * Sonar 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+ */
+package org.sonar.plugins.core.timemachine;
+
+import org.sonar.diff.HashedSequence;
+import org.sonar.diff.HashedSequenceComparator;
+import org.sonar.diff.StringText;
+import org.sonar.diff.StringTextComparator;
+
+public class ViolationTrackingBlocksRecognizer {
+
+  private final HashedSequence<StringText> a;
+  private final HashedSequence<StringText> b;
+  private final HashedSequenceComparator<StringText> cmp;
+
+  public ViolationTrackingBlocksRecognizer(String referenceSource, String source) {
+    this(new StringText(referenceSource), new StringText(source), StringTextComparator.IGNORE_WHITESPACE);
+  }
+
+  private ViolationTrackingBlocksRecognizer(StringText a, StringText b, StringTextComparator cmp) {
+    this.a = wrap(a, cmp);
+    this.b = wrap(b, cmp);
+    this.cmp = new HashedSequenceComparator<StringText>(cmp);
+  }
+
+  private static HashedSequence<StringText> wrap(StringText seq, StringTextComparator cmp) {
+    int size = seq.length();
+    int[] hashes = new int[size];
+    for (int i = 0; i < size; i++) {
+      hashes[i] = cmp.hash(seq, i);
+    }
+    return new HashedSequence<StringText>(seq, hashes);
+  }
+
+  public int computeLengthOfMaximalBlock(int startA, int startB) {
+    if (!cmp.equals(a, startA, b, startB)) {
+      return 0;
+    }
+    int length = 0;
+    int ai = startA;
+    int bi = startB;
+    while (ai < a.length() && bi < b.length() && cmp.equals(a, ai, b, bi)) {
+      ai++;
+      bi++;
+      length++;
+    }
+    ai = startA;
+    bi = startB;
+    while (ai >= 0 && bi >= 0 && cmp.equals(a, ai, b, bi)) {
+      ai--;
+      bi--;
+      length++;
+    }
+    // Note that position (startA, startB) was counted twice
+    return length - 1;
+  }
+
+}
index dd175ec7a654c0842ecd909f40d5eea299f86cfe..705069235c945bfe9d3cfd52186a85b132469e1c 100644 (file)
  */
 package org.sonar.plugins.core.timemachine;
 
-import com.google.common.collect.LinkedHashMultimap;
-import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
-import com.google.common.collect.Multimap;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.*;
 import org.apache.commons.lang.ObjectUtils;
 import org.apache.commons.lang.StringUtils;
 import org.sonar.api.batch.*;
@@ -32,9 +30,7 @@ import org.sonar.api.resources.Resource;
 import org.sonar.api.rules.Violation;
 import org.sonar.api.violations.ViolationQuery;
 
-import java.util.Collection;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 
 @DependsUpon({DecoratorBarriers.END_OF_VIOLATIONS_GENERATION, DecoratorBarriers.START_VIOLATION_TRACKING})
 @DependedUpon(DecoratorBarriers.END_OF_VIOLATION_TRACKING)
@@ -65,8 +61,13 @@ public class ViolationTrackingDecorator implements Decorator {
       // Load reference violations
       List<RuleFailureModel> referenceViolations = referenceAnalysis.getViolations(resource);
 
+      // SONAR-3072 Construct blocks recognizer based on reference source
+      String referenceSource = referenceAnalysis.getSource(resource);
+      String source = index.getSource(context.getResource());
+      ViolationTrackingBlocksRecognizer rec = new ViolationTrackingBlocksRecognizer(referenceSource, source);
+
       // Map new violations with old ones
-      mapViolations(newViolations, referenceViolations);
+      mapViolations(newViolations, referenceViolations, rec);
     }
   }
 
@@ -84,7 +85,12 @@ public class ViolationTrackingDecorator implements Decorator {
     return referenceViolationsMap.get(violation);
   }
 
+  @VisibleForTesting
   Map<Violation, RuleFailureModel> mapViolations(List<Violation> newViolations, List<RuleFailureModel> pastViolations) {
+    return mapViolations(newViolations, pastViolations, null);
+  }
+
+  private Map<Violation, RuleFailureModel> mapViolations(List<Violation> newViolations, List<RuleFailureModel> pastViolations, ViolationTrackingBlocksRecognizer rec) {
     Multimap<Integer, RuleFailureModel> pastViolationsByRule = LinkedHashMultimap.create();
     for (RuleFailureModel pastViolation : pastViolations) {
       pastViolationsByRule.put(pastViolation.getRuleId(), pastViolation);
@@ -97,7 +103,6 @@ public class ViolationTrackingDecorator implements Decorator {
           pastViolationsByRule, referenceViolationsMap);
     }
 
-
     // Try first to match violations on same rule with same line and with same checkum (but not necessarily with same message)
     for (Violation newViolation : newViolations) {
       if (isNotAlreadyMapped(newViolation, referenceViolationsMap)) {
@@ -109,7 +114,32 @@ public class ViolationTrackingDecorator implements Decorator {
 
     // If each new violation matches an old one we can stop the matching mechanism
     if (referenceViolationsMap.size() != newViolations.size()) {
-      // Try then to match violations on same rule with same message and with same checkum
+      // FIXME Godin: this condition just in order to bypass test
+      if (rec != null) {
+        // SONAR-3072
+
+        List<ViolationPair> possiblePairs = Lists.newArrayList();
+        for (Violation newViolation : newViolations) {
+          for (RuleFailureModel pastViolation : pastViolationsByRule.get(newViolation.getRule().getId())) {
+            int weight = rec.computeLengthOfMaximalBlock(pastViolation.getLine() - 1, newViolation.getLineId() - 1);
+            possiblePairs.add(new ViolationPair(pastViolation, newViolation, weight));
+          }
+        }
+        Collections.sort(possiblePairs, ViolationPair.COMPARATOR);
+
+        Set<RuleFailureModel> pp = Sets.newHashSet(pastViolations);
+        for (ViolationPair pair : possiblePairs) {
+          Violation newViolation = pair.getNewViolation();
+          RuleFailureModel pastViolation = pair.getPastViolation();
+          if (isNotAlreadyMapped(newViolation, referenceViolationsMap) && pp.contains(pastViolation)) {
+            pp.remove(pastViolation);
+            mapViolation(newViolation, pastViolation, pastViolationsByRule, referenceViolationsMap);
+          }
+        }
+
+      }
+
+      // Try then to match violations on same rule with same message and with same checksum
       for (Violation newViolation : newViolations) {
         if (isNotAlreadyMapped(newViolation, referenceViolationsMap)) {
           mapViolation(newViolation,
@@ -128,7 +158,7 @@ public class ViolationTrackingDecorator implements Decorator {
       }
 
       // Last check: match violation if same rule and same checksum but different line and different message
-      // See https://jira.codehaus.org/browse/SONAR-2812
+      // See SONAR-2812
       for (Violation newViolation : newViolations) {
         if (isNotAlreadyMapped(newViolation, referenceViolationsMap)) {
           mapViolation(newViolation,
diff --git a/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/timemachine/ReferenceAnalysisTest.java b/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/timemachine/ReferenceAnalysisTest.java
new file mode 100644 (file)
index 0000000..14b6922
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2012 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * Sonar is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * Sonar 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+ */
+package org.sonar.plugins.core.timemachine;
+
+import org.junit.Test;
+import org.sonar.api.resources.JavaFile;
+import org.sonar.api.resources.Resource;
+import org.sonar.jpa.test.AbstractDbUnitTestCase;
+
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertThat;
+
+public class ReferenceAnalysisTest extends AbstractDbUnitTestCase {
+
+  @Test
+  public void test() {
+    setupData("shared");
+
+    ReferenceAnalysis referenceAnalysis = new ReferenceAnalysis(getSession());
+
+    Resource resource = new JavaFile("");
+
+    resource.setEffectiveKey("project:org.foo.Bar");
+    assertThat(referenceAnalysis.getViolations(resource).size(), is(1));
+    assertThat(referenceAnalysis.getSource(resource), is("this is the file content"));
+
+    resource.setEffectiveKey("project:no-such-resource");
+    assertThat(referenceAnalysis.getViolations(resource).size(), is(0));
+    assertThat(referenceAnalysis.getSource(resource), is(""));
+  }
+
+}
diff --git a/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/timemachine/ViolationTrackingBlocksRecognizerTest.java b/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/timemachine/ViolationTrackingBlocksRecognizerTest.java
new file mode 100644 (file)
index 0000000..37b5d9d
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2012 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * Sonar is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * Sonar 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+ */
+package org.sonar.plugins.core.timemachine;
+
+import org.junit.Test;
+
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertThat;
+
+public class ViolationTrackingBlocksRecognizerTest {
+
+  @Test
+  public void test() {
+    assertThat(compute(t("abcde"), t("abcde"), 3, 3), is(5));
+    assertThat(compute(t("abcde"), t("abcd"), 3, 3), is(4));
+    assertThat(compute(t("bcde"), t("abcde"), 3, 3), is(0));
+    assertThat(compute(t("bcde"), t("abcde"), 2, 3), is(4));
+  }
+
+  private static int compute(String a, String b, int ai, int bi) {
+    ViolationTrackingBlocksRecognizer rec = new ViolationTrackingBlocksRecognizer(a, b);
+    return rec.computeLengthOfMaximalBlock(ai, bi);
+  }
+
+  private static String t(String text) {
+    StringBuilder sb = new StringBuilder();
+    for (int i = 0; i < text.length(); i++) {
+      sb.append(text.charAt(i)).append('\n');
+    }
+    return sb.toString();
+  }
+
+}
diff --git a/plugins/sonar-core-plugin/src/test/resources/org/sonar/plugins/core/timemachine/ReferenceAnalysisTest/shared.xml b/plugins/sonar-core-plugin/src/test/resources/org/sonar/plugins/core/timemachine/ReferenceAnalysisTest/shared.xml
new file mode 100644 (file)
index 0000000..dd96aee
--- /dev/null
@@ -0,0 +1,16 @@
+<dataset>
+
+  <projects id="200" scope="FIL" qualifier="CLA" kee="project:org.foo.Bar" root_id="[null]"
+            name="Bar" long_name="org.foo.Bar" description="[null]"
+            enabled="true" language="java" copy_resource_id="[null]" person_id="[null]" profile_id="[null]"/>
+
+  <snapshots purge_status="[null]" period1_mode="[null]" period1_param="[null]" period1_date="[null]" period2_mode="[null]" period2_param="[null]" period2_date="[null]" period3_mode="[null]" period3_param="[null]" period3_date="[null]" period4_mode="[null]" period4_param="[null]" period4_date="[null]" period5_mode="[null]" period5_param="[null]" period5_date="[null]" id="1000" project_id="200" parent_snapshot_id="[null]" root_project_id="100" root_snapshot_id="[null]"
+             scope="FIL" qualifier="CLA" created_at="2008-11-01 13:58:00.00" build_date="2008-11-01 13:58:00.00" version="[null]" path=""
+             status="P" islast="true" depth="3" />
+
+  <rule_failures switched_off="false" permanent_id="1" ID="1" SNAPSHOT_ID="1000" RULE_ID="30" FAILURE_LEVEL="3" MESSAGE="old message" LINE="10" COST="[null]"
+                 created_at="2008-11-01 13:58:00.00" checksum="[null]" person_id="[null]"/>
+
+  <snapshot_sources ID="1" SNAPSHOT_ID="1000" DATA="this is the file content"/>
+
+</dataset>
index d807eb16c55c05d9f2ca3f004693e5a89a376ff8..4d16d123833b7fbc7059f622ee01968223d7d994 100644 (file)
       <groupId>org.codehaus.sonar</groupId>
       <artifactId>sonar-core</artifactId>
     </dependency>
+    <dependency>
+      <groupId>org.codehaus.sonar</groupId>
+      <artifactId>sonar-diff</artifactId>
+      <version>${project.version}</version>
+    </dependency>
     <dependency>
       <groupId>org.codehaus.sonar</groupId>
       <artifactId>sonar-deprecated</artifactId>
diff --git a/sonar-diff/src/main/java/org/sonar/diff/StringText.java b/sonar-diff/src/main/java/org/sonar/diff/StringText.java
new file mode 100644 (file)
index 0000000..e529f11
--- /dev/null
@@ -0,0 +1,71 @@
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2012 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * Sonar is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * Sonar 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+ */
+package org.sonar.diff;
+
+import com.google.common.collect.Lists;
+
+import java.util.List;
+
+/**
+ * Text is a {@link Sequence} of lines.
+ */
+public class StringText implements Sequence {
+
+  final String content;
+
+  /**
+   * Map of line number to starting position within {@link #content}.
+   */
+  final List<Integer> lines;
+
+  public StringText(String str) {
+    this.content = str;
+    this.lines = lineMap(content, 0, content.length());
+  }
+
+  @Override
+  public int length() {
+    return lines.size() - 2;
+  }
+
+  private static List<Integer> lineMap(String buf, int ptr, int end) {
+    List<Integer> lines = Lists.newArrayList();
+    lines.add(Integer.MIN_VALUE);
+    for (; ptr < end; ptr = nextLF(buf, ptr)) {
+      lines.add(ptr);
+    }
+    lines.add(end);
+    return lines;
+  }
+
+  private static final int nextLF(String b, int ptr) {
+    return next(b, ptr, '\n');
+  }
+
+  private static final int next(final String b, int ptr, final char chrA) {
+    final int sz = b.length();
+    while (ptr < sz) {
+      if (b.charAt(ptr++) == chrA)
+        return ptr;
+    }
+    return ptr;
+  }
+
+}
diff --git a/sonar-diff/src/main/java/org/sonar/diff/StringTextComparator.java b/sonar-diff/src/main/java/org/sonar/diff/StringTextComparator.java
new file mode 100644 (file)
index 0000000..ba6e9c7
--- /dev/null
@@ -0,0 +1,93 @@
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2012 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * Sonar is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * Sonar 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+ */
+package org.sonar.diff;
+
+/**
+ * Equivalence function for {@link StringText}.
+ */
+public abstract class StringTextComparator extends SequenceComparator<StringText> {
+
+  /**
+   * Ignores all whitespace.
+   */
+  public static final StringTextComparator IGNORE_WHITESPACE = new StringTextComparator() {
+
+    @Override
+    public boolean equals(StringText a, int ai, StringText b, int bi) {
+      ai++;
+      bi++;
+      int as = a.lines.get(ai);
+      int bs = b.lines.get(bi);
+      int ae = a.lines.get(ai + 1);
+      int be = b.lines.get(bi + 1);
+      ae = trimTrailingWhitespace(a.content, as, ae);
+      be = trimTrailingWhitespace(b.content, bs, be);
+      while ((as < ae) && (bs < be)) {
+        char ac = a.content.charAt(as);
+        char bc = b.content.charAt(bs);
+        while ((as < ae - 1) && (Character.isWhitespace(ac))) {
+          as++;
+          ac = a.content.charAt(as);
+        }
+        while ((bs < be - 1) && (Character.isWhitespace(bc))) {
+          bs++;
+          bc = b.content.charAt(bs);
+        }
+        if (ac != bc) {
+          return false;
+        }
+        as++;
+        bs++;
+      }
+      return (as == ae) && (bs == be);
+    }
+
+    @Override
+    protected int hashRegion(String content, int start, int end) {
+      int hash = 5381;
+      for (; start < end; start++) {
+        char c = content.charAt(start);
+        if (!Character.isWhitespace(c)) {
+          hash = ((hash << 5) + hash) + (c & 0xff);
+        }
+      }
+      return hash;
+    }
+
+  };
+
+  @Override
+  public int hash(StringText seq, int line) {
+    final int begin = seq.lines.get(line + 1);
+    final int end = seq.lines.get(line + 2);
+    return hashRegion(seq.content, begin, end);
+  }
+
+  protected abstract int hashRegion(String content, int start, int end);
+
+  public static int trimTrailingWhitespace(String content, int start, int end) {
+    end--;
+    while (start <= end && Character.isWhitespace(content.charAt(end))) {
+      end--;
+    }
+    return end + 1;
+  }
+
+}