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.File;
024import java.nio.charset.Charset;
025import java.nio.file.Path;
026import java.util.Arrays;
027import java.util.function.Consumer;
028
029import javax.annotation.CheckForNull;
030import org.sonar.api.batch.fs.InputFile;
031import org.sonar.api.batch.fs.TextPointer;
032import org.sonar.api.batch.fs.TextRange;
033
034/**
035 * @since 4.2
036 * To create {@link InputFile} in tests, use {@link TestInputFileBuilder}.
037 */
038public class DefaultInputFile extends DefaultInputComponent implements InputFile {
039  private final DefaultIndexedFile indexedFile;
040  private final Consumer<DefaultInputFile> metadataGenerator;
041  private Status status;
042  private Charset charset;
043  private Metadata metadata;
044  private boolean publish;
045
046  public DefaultInputFile(DefaultIndexedFile indexedFile, Consumer<DefaultInputFile> metadataGenerator) {
047    super(indexedFile.batchId());
048    this.indexedFile = indexedFile;
049    this.metadataGenerator = metadataGenerator;
050    this.metadata = null;
051    this.publish = false;
052  }
053
054  public void checkMetadata() {
055    if (metadata == null) {
056      metadataGenerator.accept(this);
057    }
058  }
059
060  /**
061   * @since 6.3
062   */
063  public DefaultInputFile setPublish(boolean publish) {
064    this.publish = publish;
065    return this;
066  }
067
068  /**
069   * @since 6.3
070   */
071  public boolean publish() {
072    return publish;
073  }
074
075  @Override
076  public String relativePath() {
077    return indexedFile.relativePath();
078  }
079
080  @Override
081  public String absolutePath() {
082    return indexedFile.absolutePath();
083  }
084
085  @Override
086  public File file() {
087    return indexedFile.file();
088  }
089
090  @Override
091  public Path path() {
092    return indexedFile.path();
093  }
094
095  @CheckForNull
096  @Override
097  public String language() {
098    return indexedFile.language();
099  }
100
101  @Override
102  public Type type() {
103    return indexedFile.type();
104  }
105
106  /**
107   * Component key (without branch).
108   */
109  @Override
110  public String key() {
111    return indexedFile.key();
112  }
113
114  public String moduleKey() {
115    return indexedFile.moduleKey();
116  }
117
118  @Override
119  public int hashCode() {
120    return indexedFile.hashCode();
121  }
122
123  @Override
124  public String toString() {
125    return indexedFile.toString();
126  }
127
128  /**
129   * {@link #setStatus(org.sonar.api.batch.fs.InputFile.Status)}
130   */
131  @Override
132  public Status status() {
133    checkMetadata();
134    return status;
135  }
136
137  @Override
138  public int lines() {
139    checkMetadata();
140    return metadata.lines();
141  }
142
143  @Override
144  public boolean isEmpty() {
145    checkMetadata();
146    return metadata.lastValidOffset() == 0;
147  }
148
149  @Override
150  public Charset charset() {
151    checkMetadata();
152    return charset;
153  }
154
155  public int lastValidOffset() {
156    checkMetadata();
157    Preconditions.checkState(metadata.lastValidOffset() >= 0, "InputFile is not properly initialized.");
158    return metadata.lastValidOffset();
159  }
160
161  /**
162   * Digest hash of the file.
163   */
164  public String hash() {
165    checkMetadata();
166    return metadata.hash();
167  }
168
169  public int nonBlankLines() {
170    checkMetadata();
171    return metadata.nonBlankLines();
172  }
173
174  public int[] originalLineOffsets() {
175    checkMetadata();
176    Preconditions.checkState(metadata.originalLineOffsets() != null, "InputFile is not properly initialized.");
177    Preconditions.checkState(metadata.originalLineOffsets().length == metadata.lines(),
178      "InputFile is not properly initialized. 'originalLineOffsets' property length should be equal to 'lines'");
179    return metadata.originalLineOffsets();
180  }
181
182  @Override
183  public TextPointer newPointer(int line, int lineOffset) {
184    checkMetadata();
185    DefaultTextPointer textPointer = new DefaultTextPointer(line, lineOffset);
186    checkValid(textPointer, "pointer");
187    return textPointer;
188  }
189
190  @Override
191  public TextRange newRange(TextPointer start, TextPointer end) {
192    checkMetadata();
193    checkValid(start, "start pointer");
194    checkValid(end, "end pointer");
195    return newRangeValidPointers(start, end, false);
196  }
197
198  @Override
199  public TextRange newRange(int startLine, int startLineOffset, int endLine, int endLineOffset) {
200    checkMetadata();
201    TextPointer start = newPointer(startLine, startLineOffset);
202    TextPointer end = newPointer(endLine, endLineOffset);
203    return newRangeValidPointers(start, end, false);
204  }
205
206  @Override
207  public TextRange selectLine(int line) {
208    checkMetadata();
209    TextPointer startPointer = newPointer(line, 0);
210    TextPointer endPointer = newPointer(line, lineLength(line));
211    return newRangeValidPointers(startPointer, endPointer, true);
212  }
213
214  public void validate(TextRange range) {
215    checkMetadata();
216    checkValid(range.start(), "start pointer");
217    checkValid(range.end(), "end pointer");
218  }
219
220  /**
221   * Create Range from global offsets. Used for backward compatibility with older API.
222   */
223  public TextRange newRange(int startOffset, int endOffset) {
224    checkMetadata();
225    return newRangeValidPointers(newPointer(startOffset), newPointer(endOffset), false);
226  }
227
228  public TextPointer newPointer(int globalOffset) {
229    checkMetadata();
230    Preconditions.checkArgument(globalOffset >= 0, "%s is not a valid offset for a file", globalOffset);
231    Preconditions.checkArgument(globalOffset <= lastValidOffset(), "%s is not a valid offset for file %s. Max offset is %s", globalOffset, this, lastValidOffset());
232    int line = findLine(globalOffset);
233    int startLineOffset = originalLineOffsets()[line - 1];
234    return new DefaultTextPointer(line, globalOffset - startLineOffset);
235  }
236
237  public DefaultInputFile setStatus(Status status) {
238    this.status = status;
239    return this;
240  }
241
242  public DefaultInputFile setCharset(Charset charset) {
243    this.charset = charset;
244    return this;
245  }
246
247  private void checkValid(TextPointer pointer, String owner) {
248    Preconditions.checkArgument(pointer.line() >= 1, "%s is not a valid line for a file", pointer.line());
249    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());
250    Preconditions.checkArgument(pointer.lineOffset() >= 0, "%s is not a valid line offset for a file", pointer.lineOffset());
251    int lineLength = lineLength(pointer.line());
252    Preconditions.checkArgument(pointer.lineOffset() <= lineLength,
253      "%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());
254  }
255
256  private int lineLength(int line) {
257    return lastValidGlobalOffsetForLine(line) - originalLineOffsets()[line - 1];
258  }
259
260  private int lastValidGlobalOffsetForLine(int line) {
261    return line < this.metadata.lines() ? (originalLineOffsets()[line] - 1) : lastValidOffset();
262  }
263
264  private static TextRange newRangeValidPointers(TextPointer start, TextPointer end, boolean acceptEmptyRange) {
265    Preconditions.checkArgument(acceptEmptyRange ? (start.compareTo(end) <= 0) : (start.compareTo(end) < 0),
266      "Start pointer %s should be before end pointer %s", start, end);
267    return new DefaultTextRange(start, end);
268  }
269
270  private int findLine(int globalOffset) {
271    return Math.abs(Arrays.binarySearch(originalLineOffsets(), globalOffset) + 1);
272  }
273
274  public DefaultInputFile setMetadata(Metadata metadata) {
275    this.metadata = metadata;
276    return this;
277  }
278
279  @Override
280  public boolean equals(Object o) {
281    if (this == o) {
282      return true;
283    }
284
285    // Use instanceof to support DeprecatedDefaultInputFile
286    if (!(o instanceof DefaultInputFile)) {
287      return false;
288    }
289
290    DefaultInputFile that = (DefaultInputFile) o;
291    return this.moduleKey().equals(that.moduleKey()) && this.relativePath().equals(that.relativePath());
292  }
293
294  @Override
295  public boolean isFile() {
296    return true;
297  }
298
299}