001/*
002 * SonarQube
003 * Copyright (C) 2009-2017 SonarSource SA
004 * mailto:info AT sonarsource DOT com
005 *
006 * This program is free software; you can redistribute it and/or
007 * modify it under the terms of the GNU Lesser General Public
008 * License as published by the Free Software Foundation; either
009 * version 3 of the License, or (at your option) any later version.
010 *
011 * This program is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014 * Lesser General Public License for more details.
015 *
016 * You should have received a copy of the GNU Lesser General Public License
017 * along with this program; if not, write to the Free Software Foundation,
018 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
019 */
020package org.sonar.api.batch.fs.internal;
021
022import java.io.BufferedReader;
023import java.io.IOException;
024import java.io.InputStream;
025import java.io.InputStreamReader;
026import java.io.Reader;
027import java.nio.charset.Charset;
028import java.nio.charset.StandardCharsets;
029
030import javax.annotation.Nullable;
031import javax.annotation.concurrent.Immutable;
032
033import org.sonar.api.batch.ScannerSide;
034import org.sonar.api.batch.fs.InputFile;
035import org.sonar.api.batch.fs.internal.charhandler.CharHandler;
036import org.sonar.api.batch.fs.internal.charhandler.FileHashComputer;
037import org.sonar.api.batch.fs.internal.charhandler.LineCounter;
038import org.sonar.api.batch.fs.internal.charhandler.LineHashComputer;
039import org.sonar.api.batch.fs.internal.charhandler.LineOffsetCounter;
040
041/**
042 * Computes hash of files. Ends of Lines are ignored, so files with
043 * same content but different EOL encoding have the same hash.
044 */
045@ScannerSide
046@Immutable
047public class FileMetadata {
048  private static final char LINE_FEED = '\n';
049  private static final char CARRIAGE_RETURN = '\r';
050
051  /**
052   * Compute hash of a file ignoring line ends differences.
053   * Maximum performance is needed.
054   */
055  public Metadata readMetadata(InputStream stream, Charset encoding, String filePath, @Nullable CharHandler otherHandler) {
056    LineCounter lineCounter = new LineCounter(filePath, encoding);
057    FileHashComputer fileHashComputer = new FileHashComputer(filePath);
058    LineOffsetCounter lineOffsetCounter = new LineOffsetCounter();
059
060    if (otherHandler != null) {
061      CharHandler[] handlers = {lineCounter, fileHashComputer, lineOffsetCounter, otherHandler};
062      readFile(stream, encoding, filePath, handlers);
063    } else {
064      CharHandler[] handlers = {lineCounter, fileHashComputer, lineOffsetCounter};
065      readFile(stream, encoding, filePath, handlers);
066    }
067    return new Metadata(lineCounter.lines(), lineCounter.nonBlankLines(), fileHashComputer.getHash(), lineOffsetCounter.getOriginalLineOffsets(),
068      lineOffsetCounter.getLastValidOffset());
069  }
070
071  public Metadata readMetadata(InputStream stream, Charset encoding, String filePath) {
072    return readMetadata(stream, encoding, filePath, null);
073  }
074
075  /**
076   * For testing purpose
077   */
078  public Metadata readMetadata(Reader reader) {
079    LineCounter lineCounter = new LineCounter("fromString", StandardCharsets.UTF_16);
080    FileHashComputer fileHashComputer = new FileHashComputer("fromString");
081    LineOffsetCounter lineOffsetCounter = new LineOffsetCounter();
082    CharHandler[] handlers = {lineCounter, fileHashComputer, lineOffsetCounter};
083
084    try {
085      read(reader, handlers);
086    } catch (IOException e) {
087      throw new IllegalStateException("Should never occur", e);
088    }
089    return new Metadata(lineCounter.lines(), lineCounter.nonBlankLines(), fileHashComputer.getHash(), lineOffsetCounter.getOriginalLineOffsets(),
090      lineOffsetCounter.getLastValidOffset());
091  }
092
093  public static void readFile(InputStream stream, Charset encoding, String filePath, CharHandler[] handlers) {
094    try (Reader reader = new BufferedReader(new InputStreamReader(stream, encoding))) {
095      read(reader, handlers);
096    } catch (IOException e) {
097      throw new IllegalStateException(String.format("Fail to read file '%s' with encoding '%s'", filePath, encoding), e);
098    }
099  }
100
101  private static void read(Reader reader, CharHandler[] handlers) throws IOException {
102    char c;
103    int i = reader.read();
104    boolean afterCR = false;
105    while (i != -1) {
106      c = (char) i;
107      if (afterCR) {
108        for (CharHandler handler : handlers) {
109          if (c == CARRIAGE_RETURN) {
110            handler.newLine();
111            handler.handleAll(c);
112          } else if (c == LINE_FEED) {
113            handler.handleAll(c);
114            handler.newLine();
115          } else {
116            handler.newLine();
117            handler.handleIgnoreEoL(c);
118            handler.handleAll(c);
119          }
120        }
121        afterCR = c == CARRIAGE_RETURN;
122      } else if (c == LINE_FEED) {
123        for (CharHandler handler : handlers) {
124          handler.handleAll(c);
125          handler.newLine();
126        }
127      } else if (c == CARRIAGE_RETURN) {
128        afterCR = true;
129        for (CharHandler handler : handlers) {
130          handler.handleAll(c);
131        }
132      } else {
133        for (CharHandler handler : handlers) {
134          handler.handleIgnoreEoL(c);
135          handler.handleAll(c);
136        }
137      }
138      i = reader.read();
139    }
140    for (CharHandler handler : handlers) {
141      if (afterCR) {
142        handler.newLine();
143      }
144      handler.eof();
145    }
146  }
147
148  @FunctionalInterface
149  public interface LineHashConsumer {
150    void consume(int lineIdx, @Nullable byte[] hash);
151  }
152
153  /**
154   * Compute a MD5 hash of each line of the file after removing of all blank chars
155   */
156  public static void computeLineHashesForIssueTracking(InputFile f, LineHashConsumer consumer) {
157    try {
158      readFile(f.inputStream(), f.charset(), f.absolutePath(), new CharHandler[] {new LineHashComputer(consumer, f.file())});
159    } catch (IOException e) {
160      throw new IllegalStateException("Failed to compute line hashes for " + f.absolutePath(), e);
161    }
162  }
163}