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