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.Function;
023import com.google.common.collect.Iterables;
024import java.io.File;
025import java.io.IOException;
026import java.nio.charset.Charset;
027import java.nio.file.LinkOption;
028import java.nio.file.Path;
029import java.util.ArrayList;
030import java.util.Collections;
031import java.util.HashMap;
032import java.util.Iterator;
033import java.util.Map;
034import java.util.SortedSet;
035import java.util.TreeSet;
036import javax.annotation.CheckForNull;
037import javax.annotation.Nullable;
038import org.sonar.api.batch.fs.FilePredicate;
039import org.sonar.api.batch.fs.FilePredicates;
040import org.sonar.api.batch.fs.FileSystem;
041import org.sonar.api.batch.fs.InputDir;
042import org.sonar.api.batch.fs.InputFile;
043import org.sonar.api.scan.filesystem.PathResolver;
044import org.sonar.api.utils.PathUtils;
045
046/**
047 * @since 4.2
048 */
049public class DefaultFileSystem implements FileSystem {
050
051  private final Cache cache;
052  private final SortedSet<String> languages = new TreeSet<>();
053  private final Path baseDir;
054  private Path workDir;
055  private Charset encoding;
056  protected final FilePredicates predicates;
057  private FilePredicate defaultPredicate;
058
059  /**
060   * Only for testing
061   */
062  public DefaultFileSystem(Path baseDir) {
063    this(baseDir.toFile(), new MapCache());
064  }
065
066  /**
067   * Only for testing
068   */
069  public DefaultFileSystem(File baseDir) {
070    this(baseDir, new MapCache());
071  }
072
073  protected DefaultFileSystem(@Nullable File baseDir, Cache cache) {
074    // Basedir can be null with views
075    try {
076      this.baseDir = baseDir != null ? baseDir.toPath().toRealPath(LinkOption.NOFOLLOW_LINKS) : new File(".").toPath().toAbsolutePath().normalize();
077    } catch (IOException e) {
078      throw new IllegalStateException(e);
079    }
080    this.cache = cache;
081    this.predicates = new DefaultFilePredicates(this.baseDir);
082  }
083
084  public Path baseDirPath() {
085    return baseDir;
086  }
087
088  @Override
089  public File baseDir() {
090    return baseDir.toFile();
091  }
092
093  public DefaultFileSystem setEncoding(@Nullable Charset e) {
094    this.encoding = e;
095    return this;
096  }
097
098  @Override
099  public Charset encoding() {
100    return encoding == null ? Charset.defaultCharset() : encoding;
101  }
102
103  public boolean isDefaultJvmEncoding() {
104    return encoding == null;
105  }
106
107  public DefaultFileSystem setWorkDir(File d) {
108    this.workDir = d.getAbsoluteFile().toPath().normalize();
109    return this;
110  }
111
112  public DefaultFileSystem setDefaultPredicate(@Nullable FilePredicate predicate) {
113    this.defaultPredicate = predicate;
114    return this;
115  }
116
117  @Override
118  public File workDir() {
119    return workDir.toFile();
120  }
121
122  @Override
123  public InputFile inputFile(FilePredicate predicate) {
124    Iterable<InputFile> files = inputFiles(predicate);
125    Iterator<InputFile> iterator = files.iterator();
126    if (!iterator.hasNext()) {
127      return null;
128    }
129    InputFile first = iterator.next();
130    if (!iterator.hasNext()) {
131      return first;
132    }
133
134    StringBuilder sb = new StringBuilder();
135    sb.append("expected one element but was: <" + first);
136    for (int i = 0; i < 4 && iterator.hasNext(); i++) {
137      sb.append(", " + iterator.next());
138    }
139    if (iterator.hasNext()) {
140      sb.append(", ...");
141    }
142    sb.append('>');
143
144    throw new IllegalArgumentException(sb.toString());
145
146  }
147
148  /**
149   * Bypass default predicate to get all files/dirs indexed.
150   * Default predicate is used when some files/dirs should not be processed by sensors.
151   */
152  public Iterable<InputFile> inputFiles() {
153    doPreloadFiles();
154    return OptimizedFilePredicateAdapter.create(predicates.all()).get(cache);
155  }
156
157  @Override
158  public Iterable<InputFile> inputFiles(FilePredicate predicate) {
159    doPreloadFiles();
160    FilePredicate combinedPredicate = predicate;
161    if (defaultPredicate != null) {
162      combinedPredicate = predicates().and(defaultPredicate, predicate);
163    }
164    return OptimizedFilePredicateAdapter.create(combinedPredicate).get(cache);
165  }
166
167  @Override
168  public boolean hasFiles(FilePredicate predicate) {
169    return inputFiles(predicate).iterator().hasNext();
170  }
171
172  @Override
173  public Iterable<File> files(FilePredicate predicate) {
174    doPreloadFiles();
175    return Iterables.transform(inputFiles(predicate), new Function<InputFile, File>() {
176      @Override
177      public File apply(InputFile input) {
178        return input.file();
179      }
180    });
181  }
182
183  @Override
184  public InputDir inputDir(File dir) {
185    doPreloadFiles();
186    String relativePath = PathUtils.sanitize(new PathResolver().relativePath(baseDir.toFile(), dir));
187    if (relativePath == null) {
188      return null;
189    }
190    return cache.inputDir(relativePath);
191  }
192
193  /**
194   * Adds InputFile to the list and registers its language, if present.
195   * Synchronized because PersistIt Exchange is not concurrent
196   */
197  public synchronized DefaultFileSystem add(DefaultInputFile inputFile) {
198    if (this.baseDir == null) {
199      throw new IllegalStateException("Please set basedir on filesystem before adding files");
200    }
201    inputFile.setModuleBaseDir(this.baseDir);
202    cache.add(inputFile);
203    String language = inputFile.language();
204    if (language != null) {
205      languages.add(language);
206    }
207    return this;
208  }
209
210  /**
211   * Adds InputDir to the list.
212   * Synchronized because PersistIt Exchange is not concurrent
213   */
214  public synchronized DefaultFileSystem add(DefaultInputDir inputDir) {
215    if (this.baseDir == null) {
216      throw new IllegalStateException("Please set basedir on filesystem before adding dirs");
217    }
218    inputDir.setModuleBaseDir(this.baseDir);
219    cache.add(inputDir);
220    return this;
221  }
222
223  /**
224   * Adds a language to the list. To be used only for unit tests that need to use {@link #languages()} without
225   * using {@link #add(DefaultInputFile)}.
226   */
227  public DefaultFileSystem addLanguages(String language, String... others) {
228    languages.add(language);
229    Collections.addAll(languages, others);
230    return this;
231  }
232
233  @Override
234  public SortedSet<String> languages() {
235    doPreloadFiles();
236    return languages;
237  }
238
239  @Override
240  public FilePredicates predicates() {
241    return predicates;
242  }
243
244  /**
245   * This method is called before each search of files.
246   */
247  protected void doPreloadFiles() {
248    // nothing to do by default
249  }
250
251  public abstract static class Cache implements Index {
252    @Override
253    public abstract Iterable<InputFile> inputFiles();
254
255    @Override
256    @CheckForNull
257    public abstract InputFile inputFile(String relativePath);
258
259    @Override
260    @CheckForNull
261    public abstract InputDir inputDir(String relativePath);
262
263    protected abstract void doAdd(InputFile inputFile);
264
265    protected abstract void doAdd(InputDir inputDir);
266
267    final void add(InputFile inputFile) {
268      doAdd(inputFile);
269    }
270
271    public void add(InputDir inputDir) {
272      doAdd(inputDir);
273    }
274
275  }
276
277  /**
278   * Used only for testing
279   */
280  private static class MapCache extends Cache {
281    private final Map<String, InputFile> fileMap = new HashMap<>();
282    private final Map<String, InputDir> dirMap = new HashMap<>();
283
284    @Override
285    public Iterable<InputFile> inputFiles() {
286      return new ArrayList<>(fileMap.values());
287    }
288
289    @Override
290    public InputFile inputFile(String relativePath) {
291      return fileMap.get(relativePath);
292    }
293
294    @Override
295    public InputDir inputDir(String relativePath) {
296      return dirMap.get(relativePath);
297    }
298
299    @Override
300    protected void doAdd(InputFile inputFile) {
301      fileMap.put(inputFile.relativePath(), inputFile);
302    }
303
304    @Override
305    protected void doAdd(InputDir inputDir) {
306      dirMap.put(inputDir.relativePath(), inputDir);
307    }
308  }
309
310  @Override
311  public File resolvePath(String path) {
312    File file = new File(path);
313    if (!file.isAbsolute()) {
314      try {
315        file = new File(baseDir(), path).getCanonicalFile();
316      } catch (IOException e) {
317        throw new IllegalArgumentException("Unable to resolve path '" + path + "'", e);
318      }
319    }
320    return file;
321  }
322}