001/*
002 * SonarQube
003 * Copyright (C) 2009-2016 SonarSource SA
004 * mailto:contact 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.BufferedInputStream;
024import java.io.File;
025import java.io.FileInputStream;
026import java.io.FileNotFoundException;
027import java.io.InputStream;
028import java.io.StringReader;
029import java.nio.charset.Charset;
030import java.nio.file.Path;
031import java.util.Arrays;
032import javax.annotation.CheckForNull;
033import javax.annotation.Nullable;
034import org.sonar.api.batch.fs.InputFile;
035import org.sonar.api.batch.fs.TextPointer;
036import org.sonar.api.batch.fs.TextRange;
037import org.sonar.api.batch.fs.internal.FileMetadata.Metadata;
038import org.sonar.api.utils.PathUtils;
039
040/**
041 * @since 4.2
042 */
043public class DefaultInputFile extends DefaultInputComponent implements InputFile, org.sonar.api.resources.InputFile {
044
045  private final String relativePath;
046  private final String moduleKey;
047  private Path moduleBaseDir;
048  private String language;
049  private Type type = Type.MAIN;
050  private Status status;
051  private int lines = -1;
052  private Charset charset;
053  private int lastValidOffset = -1;
054  private String hash;
055  private int nonBlankLines;
056  private int[] originalLineOffsets;
057
058  public DefaultInputFile(String moduleKey, String relativePath) {
059    this.moduleKey = moduleKey;
060    this.relativePath = PathUtils.sanitize(relativePath);
061  }
062
063  @Override
064  public String relativePath() {
065    return relativePath;
066  }
067
068  @Override
069  public String absolutePath() {
070    return PathUtils.sanitize(path().toString());
071  }
072
073  @Override
074  public File file() {
075    return path().toFile();
076  }
077
078  @Override
079  public Path path() {
080    if (moduleBaseDir == null) {
081      throw new IllegalStateException("Can not return the java.nio.file.Path because module baseDir is not set (see method setModuleBaseDir(java.io.File))");
082    }
083    return moduleBaseDir.resolve(relativePath);
084  }
085
086  @CheckForNull
087  @Override
088  public String language() {
089    return language;
090  }
091
092  @Override
093  public Type type() {
094    return type;
095  }
096
097  /**
098   * {@link #setStatus(org.sonar.api.batch.fs.InputFile.Status)}
099   */
100  @Override
101  public Status status() {
102    return status;
103  }
104
105  @Override
106  public int lines() {
107    return lines;
108  }
109
110  @Override
111  public boolean isEmpty() {
112    return lastValidOffset == 0;
113  }
114
115  /**
116   * Component key.
117   */
118  @Override
119  public String key() {
120    return new StringBuilder().append(moduleKey).append(":").append(relativePath).toString();
121  }
122
123  public String moduleKey() {
124    return moduleKey;
125  }
126
127  public Charset charset() {
128    return charset;
129  }
130
131  /**
132   * For testing purpose. Will be automaticall set when file is added to {@link DefaultFileSystem}
133   */
134  public DefaultInputFile setModuleBaseDir(Path moduleBaseDir) {
135    this.moduleBaseDir = moduleBaseDir.normalize();
136    return this;
137  }
138
139  public DefaultInputFile setLanguage(@Nullable String language) {
140    this.language = language;
141    return this;
142  }
143
144  public DefaultInputFile setType(Type type) {
145    this.type = type;
146    return this;
147  }
148
149  public DefaultInputFile setStatus(Status status) {
150    this.status = status;
151    return this;
152  }
153
154  public DefaultInputFile setLines(int lines) {
155    this.lines = lines;
156    return this;
157  }
158
159  public DefaultInputFile setCharset(Charset charset) {
160    this.charset = charset;
161    return this;
162  }
163
164  public int lastValidOffset() {
165    Preconditions.checkState(lastValidOffset >= 0, "InputFile is not properly initialized. Please set 'lastValidOffset' property.");
166    return lastValidOffset;
167  }
168
169  public DefaultInputFile setLastValidOffset(int lastValidOffset) {
170    this.lastValidOffset = lastValidOffset;
171    return this;
172  }
173
174  /**
175   * Digest hash of the file.
176   */
177  public String hash() {
178    return hash;
179  }
180
181  public int nonBlankLines() {
182    return nonBlankLines;
183  }
184
185  public int[] originalLineOffsets() {
186    Preconditions.checkState(originalLineOffsets != null, "InputFile is not properly initialized. Please set 'originalLineOffsets' property.");
187    Preconditions.checkState(originalLineOffsets.length == lines, "InputFile is not properly initialized. 'originalLineOffsets' property length should be equal to 'lines'");
188    return originalLineOffsets;
189  }
190
191  public DefaultInputFile setHash(String hash) {
192    this.hash = hash;
193    return this;
194  }
195
196  public DefaultInputFile setNonBlankLines(int nonBlankLines) {
197    this.nonBlankLines = nonBlankLines;
198    return this;
199  }
200
201  public DefaultInputFile setOriginalLineOffsets(int[] originalLineOffsets) {
202    this.originalLineOffsets = originalLineOffsets;
203    return this;
204  }
205
206  @Override
207  public TextPointer newPointer(int line, int lineOffset) {
208    DefaultTextPointer textPointer = new DefaultTextPointer(line, lineOffset);
209    checkValid(textPointer, "pointer");
210    return textPointer;
211  }
212
213  private void checkValid(TextPointer pointer, String owner) {
214    Preconditions.checkArgument(pointer.line() >= 1, "%s is not a valid line for a file", pointer.line());
215    Preconditions.checkArgument(pointer.line() <= this.lines, "%s is not a valid line for %s. File %s has %s line(s)", pointer.line(), owner, this, lines);
216    Preconditions.checkArgument(pointer.lineOffset() >= 0, "%s is not a valid line offset for a file", pointer.lineOffset());
217    int lineLength = lineLength(pointer.line());
218    Preconditions.checkArgument(pointer.lineOffset() <= lineLength,
219      "%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());
220  }
221
222  private int lineLength(int line) {
223    return lastValidGlobalOffsetForLine(line) - originalLineOffsets()[line - 1];
224  }
225
226  private int lastValidGlobalOffsetForLine(int line) {
227    return line < this.lines ? (originalLineOffsets()[line] - 1) : lastValidOffset();
228  }
229
230  @Override
231  public TextRange newRange(TextPointer start, TextPointer end) {
232    checkValid(start, "start pointer");
233    checkValid(end, "end pointer");
234    return newRangeValidPointers(start, end);
235  }
236
237  @Override
238  public TextRange newRange(int startLine, int startLineOffset, int endLine, int endLineOffset) {
239    TextPointer start = newPointer(startLine, startLineOffset);
240    TextPointer end = newPointer(endLine, endLineOffset);
241    return newRangeValidPointers(start, end);
242  }
243
244  @Override
245  public TextRange selectLine(int line) {
246    TextPointer startPointer = newPointer(line, 0);
247    TextPointer endPointer = newPointer(line, lineLength(line));
248    return newRangeValidPointers(startPointer, endPointer);
249  }
250
251  public void validate(TextRange range) {
252    checkValid(range.start(), "start pointer");
253    checkValid(range.end(), "end pointer");
254  }
255
256  private static TextRange newRangeValidPointers(TextPointer start, TextPointer end) {
257    Preconditions.checkArgument(start.compareTo(end) <= 0, "Start pointer %s should be before end pointer %s", start, end);
258    return new DefaultTextRange(start, end);
259  }
260
261  /**
262   * Create Range from global offsets. Used for backward compatibility with older API.
263   */
264  public TextRange newRange(int startOffset, int endOffset) {
265    return newRangeValidPointers(newPointer(startOffset), newPointer(endOffset));
266  }
267
268  public TextPointer newPointer(int globalOffset) {
269    Preconditions.checkArgument(globalOffset >= 0, "%s is not a valid offset for a file", globalOffset);
270    Preconditions.checkArgument(globalOffset <= lastValidOffset(), "%s is not a valid offset for file %s. Max offset is %s", globalOffset, this, lastValidOffset());
271    int line = findLine(globalOffset);
272    int startLineOffset = originalLineOffsets()[line - 1];
273    return new DefaultTextPointer(line, globalOffset - startLineOffset);
274  }
275
276  private int findLine(int globalOffset) {
277    return Math.abs(Arrays.binarySearch(originalLineOffsets(), globalOffset) + 1);
278  }
279
280  public DefaultInputFile initMetadata(Metadata metadata) {
281    this.setLines(metadata.lines);
282    this.setLastValidOffset(metadata.lastValidOffset);
283    this.setNonBlankLines(metadata.nonBlankLines);
284    this.setHash(metadata.hash);
285    this.setOriginalLineOffsets(metadata.originalLineOffsets);
286    return this;
287  }
288
289  /**
290   * For testing purpose
291   */
292  public DefaultInputFile initMetadata(String content) {
293    return initMetadata(new FileMetadata().readMetadata(new StringReader(content)));
294  }
295
296  @Override
297  public boolean equals(Object o) {
298    if (this == o) {
299      return true;
300    }
301
302    // Use instanceof to support DeprecatedDefaultInputFile
303    if (!(o instanceof DefaultInputFile)) {
304      return false;
305    }
306
307    DefaultInputFile that = (DefaultInputFile) o;
308    return moduleKey.equals(that.moduleKey) && relativePath.equals(that.relativePath);
309  }
310
311  @Override
312  public int hashCode() {
313    return moduleKey.hashCode() + relativePath.hashCode() * 13;
314  }
315
316  @Override
317  public String toString() {
318    return "[moduleKey=" + moduleKey + ", relative=" + relativePath + ", basedir=" + moduleBaseDir + "]";
319  }
320
321  @Override
322  public File getFileBaseDir() {
323    return moduleBaseDir.toFile();
324  }
325
326  @Override
327  public File getFile() {
328    return file();
329  }
330
331  @Override
332  public String getRelativePath() {
333    return relativePath();
334  }
335
336  @Override
337  public InputStream getInputStream() throws FileNotFoundException {
338    return new BufferedInputStream(new FileInputStream(file()));
339  }
340
341  @Override
342  public boolean isFile() {
343    return true;
344  }
345
346}