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