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