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