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 com.google.common.base.Preconditions;
023import java.io.ByteArrayInputStream;
024import java.io.ByteArrayOutputStream;
025import java.io.File;
026import java.io.IOException;
027import java.io.InputStream;
028import java.net.URI;
029import java.nio.charset.Charset;
030import java.nio.file.Files;
031import java.nio.file.Path;
032import java.util.Arrays;
033import java.util.function.Consumer;
034import javax.annotation.CheckForNull;
035import javax.annotation.Nullable;
036import org.apache.commons.io.ByteOrderMark;
037import org.apache.commons.io.input.BOMInputStream;
038import org.sonar.api.batch.fs.InputFile;
039import org.sonar.api.batch.fs.TextPointer;
040import org.sonar.api.batch.fs.TextRange;
041
042/**
043 * @since 4.2
044 * To create {@link InputFile} in tests, use {@link TestInputFileBuilder}.
045 */
046public class DefaultInputFile extends DefaultInputComponent implements InputFile {
047
048  private static final int DEFAULT_BUFFER_SIZE = 1024 * 4;
049
050  private final DefaultIndexedFile indexedFile;
051  private final String contents;
052  private final Consumer<DefaultInputFile> metadataGenerator;
053
054  private Status status;
055  private Charset charset;
056  private Metadata metadata;
057  private boolean published;
058  private boolean excludedForCoverage;
059
060  public DefaultInputFile(DefaultIndexedFile indexedFile, Consumer<DefaultInputFile> metadataGenerator) {
061    this(indexedFile, metadataGenerator, null);
062  }
063
064  // For testing
065  public DefaultInputFile(DefaultIndexedFile indexedFile, Consumer<DefaultInputFile> metadataGenerator, @Nullable String contents) {
066    super(indexedFile.batchId());
067    this.indexedFile = indexedFile;
068    this.metadataGenerator = metadataGenerator;
069    this.metadata = null;
070    this.published = false;
071    this.excludedForCoverage = false;
072    this.contents = contents;
073  }
074
075  public void checkMetadata() {
076    if (metadata == null) {
077      metadataGenerator.accept(this);
078    }
079  }
080
081  @Override
082  public InputStream inputStream() throws IOException {
083    return contents != null ? new ByteArrayInputStream(contents.getBytes(charset()))
084      : new BOMInputStream(Files.newInputStream(path()),
085        ByteOrderMark.UTF_8, ByteOrderMark.UTF_16LE, ByteOrderMark.UTF_16BE, ByteOrderMark.UTF_32LE, ByteOrderMark.UTF_32BE);
086  }
087
088  @Override
089  public String contents() throws IOException {
090    if (contents != null) {
091      return contents;
092    } else {
093      ByteArrayOutputStream result = new ByteArrayOutputStream();
094      try (InputStream inputStream = inputStream()) {
095        byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
096        int length;
097        while ((length = inputStream.read(buffer)) != -1) {
098          result.write(buffer, 0, length);
099        }
100      }
101      return result.toString(charset().name());
102    }
103  }
104
105  public DefaultInputFile setPublished(boolean published) {
106    this.published = published;
107    return this;
108  }
109
110  public boolean isPublished() {
111    return published;
112  }
113
114  public DefaultInputFile setExcludedForCoverage(boolean excludedForCoverage) {
115    this.excludedForCoverage = excludedForCoverage;
116    return this;
117  }
118
119  public boolean isExcludedForCoverage() {
120    return excludedForCoverage;
121  }
122
123  /**
124   * @deprecated since 6.6
125   */
126  @Deprecated
127  @Override
128  public String relativePath() {
129    return indexedFile.relativePath();
130  }
131
132  public String getModuleRelativePath() {
133    return indexedFile.getModuleRelativePath();
134  }
135
136  public String getProjectRelativePath() {
137    return indexedFile.getProjectRelativePath();
138  }
139
140  @Override
141  public String absolutePath() {
142    return indexedFile.absolutePath();
143  }
144
145  @Override
146  public File file() {
147    return indexedFile.file();
148  }
149
150  @Override
151  public Path path() {
152    return indexedFile.path();
153  }
154
155  @CheckForNull
156  @Override
157  public String language() {
158    return indexedFile.language();
159  }
160
161  @Override
162  public Type type() {
163    return indexedFile.type();
164  }
165
166  /**
167   * Component key (without branch).
168   */
169  @Override
170  public String key() {
171    return indexedFile.key();
172  }
173
174  public String moduleKey() {
175    return indexedFile.moduleKey();
176  }
177
178  @Override
179  public int hashCode() {
180    return indexedFile.hashCode();
181  }
182
183  @Override
184  public String toString() {
185    return indexedFile.toString();
186  }
187
188  /**
189   * {@link #setStatus(org.sonar.api.batch.fs.InputFile.Status)}
190   */
191  @Override
192  public Status status() {
193    checkMetadata();
194    return status;
195  }
196
197  @Override
198  public int lines() {
199    checkMetadata();
200    return metadata.lines();
201  }
202
203  @Override
204  public boolean isEmpty() {
205    checkMetadata();
206    return metadata.lastValidOffset() == 0;
207  }
208
209  @Override
210  public Charset charset() {
211    checkMetadata();
212    return charset;
213  }
214
215  public int lastValidOffset() {
216    checkMetadata();
217    Preconditions.checkState(metadata.lastValidOffset() >= 0, "InputFile is not properly initialized.");
218    return metadata.lastValidOffset();
219  }
220
221  /**
222   * Digest hash of the file.
223   */
224  public String hash() {
225    checkMetadata();
226    return metadata.hash();
227  }
228
229  public int nonBlankLines() {
230    checkMetadata();
231    return metadata.nonBlankLines();
232  }
233
234  public int[] originalLineOffsets() {
235    checkMetadata();
236    Preconditions.checkState(metadata.originalLineOffsets() != null, "InputFile is not properly initialized.");
237    Preconditions.checkState(metadata.originalLineOffsets().length == metadata.lines(),
238      "InputFile is not properly initialized. 'originalLineOffsets' property length should be equal to 'lines'");
239    return metadata.originalLineOffsets();
240  }
241
242  @Override
243  public TextPointer newPointer(int line, int lineOffset) {
244    checkMetadata();
245    DefaultTextPointer textPointer = new DefaultTextPointer(line, lineOffset);
246    checkValid(textPointer, "pointer");
247    return textPointer;
248  }
249
250  @Override
251  public TextRange newRange(TextPointer start, TextPointer end) {
252    checkMetadata();
253    checkValid(start, "start pointer");
254    checkValid(end, "end pointer");
255    return newRangeValidPointers(start, end, false);
256  }
257
258  @Override
259  public TextRange newRange(int startLine, int startLineOffset, int endLine, int endLineOffset) {
260    checkMetadata();
261    TextPointer start = newPointer(startLine, startLineOffset);
262    TextPointer end = newPointer(endLine, endLineOffset);
263    return newRangeValidPointers(start, end, false);
264  }
265
266  @Override
267  public TextRange selectLine(int line) {
268    checkMetadata();
269    TextPointer startPointer = newPointer(line, 0);
270    TextPointer endPointer = newPointer(line, lineLength(line));
271    return newRangeValidPointers(startPointer, endPointer, true);
272  }
273
274  public void validate(TextRange range) {
275    checkMetadata();
276    checkValid(range.start(), "start pointer");
277    checkValid(range.end(), "end pointer");
278  }
279
280  /**
281   * Create Range from global offsets. Used for backward compatibility with older API.
282   */
283  public TextRange newRange(int startOffset, int endOffset) {
284    checkMetadata();
285    return newRangeValidPointers(newPointer(startOffset), newPointer(endOffset), false);
286  }
287
288  public TextPointer newPointer(int globalOffset) {
289    checkMetadata();
290    Preconditions.checkArgument(globalOffset >= 0, "%s is not a valid offset for a file", globalOffset);
291    Preconditions.checkArgument(globalOffset <= lastValidOffset(), "%s is not a valid offset for file %s. Max offset is %s", globalOffset, this, lastValidOffset());
292    int line = findLine(globalOffset);
293    int startLineOffset = originalLineOffsets()[line - 1];
294    return new DefaultTextPointer(line, globalOffset - startLineOffset);
295  }
296
297  public DefaultInputFile setStatus(Status status) {
298    this.status = status;
299    return this;
300  }
301
302  public DefaultInputFile setCharset(Charset charset) {
303    this.charset = charset;
304    return this;
305  }
306
307  private void checkValid(TextPointer pointer, String owner) {
308    Preconditions.checkArgument(pointer.line() >= 1, "%s is not a valid line for a file", pointer.line());
309    Preconditions.checkArgument(pointer.line() <= this.metadata.lines(), "%s is not a valid line for %s. File %s has %s line(s)", pointer.line(), owner, this, metadata.lines());
310    Preconditions.checkArgument(pointer.lineOffset() >= 0, "%s is not a valid line offset for a file", pointer.lineOffset());
311    int lineLength = lineLength(pointer.line());
312    Preconditions.checkArgument(pointer.lineOffset() <= lineLength,
313      "%s is not a valid line offset for %s. File %s has %s character(s) at line %s", pointer.lineOffset(), owner, this, lineLength, pointer.line());
314  }
315
316  private int lineLength(int line) {
317    return lastValidGlobalOffsetForLine(line) - originalLineOffsets()[line - 1];
318  }
319
320  private int lastValidGlobalOffsetForLine(int line) {
321    return line < this.metadata.lines() ? (originalLineOffsets()[line] - 1) : lastValidOffset();
322  }
323
324  private static TextRange newRangeValidPointers(TextPointer start, TextPointer end, boolean acceptEmptyRange) {
325    Preconditions.checkArgument(acceptEmptyRange ? (start.compareTo(end) <= 0) : (start.compareTo(end) < 0),
326      "Start pointer %s should be before end pointer %s", start, end);
327    return new DefaultTextRange(start, end);
328  }
329
330  private int findLine(int globalOffset) {
331    return Math.abs(Arrays.binarySearch(originalLineOffsets(), globalOffset) + 1);
332  }
333
334  public DefaultInputFile setMetadata(Metadata metadata) {
335    this.metadata = metadata;
336    return this;
337  }
338
339  @Override
340  public boolean equals(Object obj) {
341    if (obj == null) {
342      return false;
343    }
344
345    if (this.getClass() != obj.getClass()) {
346      return false;
347    }
348
349    DefaultInputFile that = (DefaultInputFile) obj;
350    return this.getProjectRelativePath().equals(that.getProjectRelativePath());
351  }
352
353  @Override
354  public boolean isFile() {
355    return true;
356  }
357
358  @Override
359  public String filename() {
360    return indexedFile.filename();
361  }
362
363  @Override
364  public URI uri() {
365    return indexedFile.uri();
366  }
367
368}