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}