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