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