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