001    package org.sonar.ide.wsclient;
002    
003    import org.apache.commons.lang.StringUtils;
004    import org.slf4j.Logger;
005    import org.slf4j.LoggerFactory;
006    import org.sonar.ide.api.SourceCodeDiff;
007    import org.sonar.ide.api.SourceCodeDiffEngine;
008    import org.sonar.wsclient.services.Source;
009    
010    /**
011     * Actually this is an implementation of heuristic algorithm - magic happens here.
012     *
013     * @author Evgeny Mandrikov
014     */
015    public class SimpleSourceCodeDiffEngine implements SourceCodeDiffEngine {
016      private static final Logger LOG = LoggerFactory.getLogger(SimpleSourceCodeDiffEngine.class);
017    
018      public static SourceCodeDiffEngine getInstance() {
019        return new SimpleSourceCodeDiffEngine();
020      }
021    
022      public SourceCodeDiff diff(String local, String remote) {
023        return diff(split(local), split(remote));
024      }
025    
026      /**
027       * {@inheritDoc}
028       */
029      public SourceCodeDiff diff(String[] local, String remote[]) {
030        SourceCodeDiff result = new SourceCodeDiff();
031    
032        int[] hashCodes = getHashCodes(local);
033        // Works for O(S*L) time, where S - number of lines on server and L - number of lines in working copy.
034        for (int i = 0; i < remote.length; i++) {
035          int originalLine = i + 1;
036          int newLine = internalMatch(remote[i], hashCodes, originalLine);
037          if (newLine != -1) {
038            result.map(originalLine, newLine);
039          }
040        }
041    
042        return result;
043      }
044    
045      /**
046       * Currently this method just compares hash codes (see {@link #getHashCode(String)}).
047       *
048       * @return -1 if not found
049       */
050      private int internalMatch(String originalSourceLine, int[] hashCodes, int originalLine) {
051        int newLine = -1;
052        int originalHashCode = getHashCode(originalSourceLine);
053        // line might not exists in working copy
054        if (originalLine - 1 < hashCodes.length) {
055          if (hashCodes[originalLine - 1] == originalHashCode) {
056            newLine = originalLine;
057          }
058        }
059        for (int i = 0; i < hashCodes.length; i++) {
060          if (hashCodes[i] == originalHashCode) {
061            if (newLine != -1 && newLine != originalLine) {
062              // may be more than one match, but we take into account only first
063              LOG.warn("Found more than one match for line '{}'", originalSourceLine);
064              break;
065            }
066            newLine = i + 1;
067          }
068        }
069        return newLine;
070      }
071    
072      public static String[] split(String text) {
073        return StringUtils.splitPreserveAllTokens(text, '\n');
074      }
075    
076      /**
077       * Returns hash code for specified string after removing whitespaces.
078       *
079       * @param str string
080       * @return hash code for specified string after removing whitespaces
081       */
082      static int getHashCode(String str) {
083        if (str == null) {
084          return 0;
085        }
086        return StringUtils.deleteWhitespace(str).hashCode();
087      }
088    
089      /**
090       * Returns hash codes for specified strings after removing whitespaces.
091       *
092       * @param str strings
093       * @return hash codes for specified strings after removing whitespaces
094       */
095      private static int[] getHashCodes(String[] str) {
096        int[] hashCodes = new int[str.length];
097        for (int i = 0; i < str.length; i++) {
098          hashCodes[i] = getHashCode(str[i]);
099        }
100        return hashCodes;
101      }
102    
103      public static String[] getLines(Source source) {
104        String[] remote = new String[source.getLinesById().lastKey()];
105        for (int i = 0; i < remote.length; i++) {
106          remote[i] = source.getLine(i + 1);
107          if (remote[i] == null) {
108            remote[i] = "";
109          }
110        }
111        return remote;
112      }
113    
114    }