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}