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