001/*
002 * SonarQube, open source software quality management tool.
003 * Copyright (C) 2008-2014 SonarSource
004 * mailto:contact AT sonarsource DOT com
005 *
006 * SonarQube 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 * SonarQube 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<String>();
054  private final Path baseDir;
055  private Path workDir;
056  private Charset encoding;
057  private final FilePredicates predicates;
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    this.baseDir = baseDir != null ? baseDir.toPath().toAbsolutePath().normalize() : new File(".").toPath();
076    this.cache = cache;
077    this.predicates = new DefaultFilePredicates(this.baseDir);
078  }
079
080  public Path baseDirPath() {
081    return baseDir;
082  }
083
084  @Override
085  public File baseDir() {
086    return baseDir.toFile();
087  }
088
089  public DefaultFileSystem setEncoding(@Nullable Charset e) {
090    this.encoding = e;
091    return this;
092  }
093
094  @Override
095  public Charset encoding() {
096    return encoding == null ? Charset.defaultCharset() : encoding;
097  }
098
099  public boolean isDefaultJvmEncoding() {
100    return encoding == null;
101  }
102
103  public DefaultFileSystem setWorkDir(File d) {
104    this.workDir = d.getAbsoluteFile().toPath().normalize();
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  @Override
140  public Iterable<InputFile> inputFiles(FilePredicate predicate) {
141    doPreloadFiles();
142    return OptimizedFilePredicateAdapter.create(predicate).get(cache);
143  }
144
145  @Override
146  public boolean hasFiles(FilePredicate predicate) {
147    return inputFiles(predicate).iterator().hasNext();
148  }
149
150  @Override
151  public Iterable<File> files(FilePredicate predicate) {
152    doPreloadFiles();
153    return Iterables.transform(inputFiles(predicate), new Function<InputFile, File>() {
154      @Override
155      public File apply(InputFile input) {
156        return input.file();
157      }
158    });
159  }
160
161  @Override
162  public InputDir inputDir(File dir) {
163    doPreloadFiles();
164    String relativePath = PathUtils.sanitize(new PathResolver().relativePath(baseDir.toFile(), dir));
165    if (relativePath == null) {
166      return null;
167    }
168    return cache.inputDir(relativePath);
169  }
170
171  /**
172   * Adds InputFile to the list and registers its language, if present.
173   * Synchronized because PersistIt Exchange is not concurrent
174   */
175  public synchronized DefaultFileSystem add(DefaultInputFile inputFile) {
176    if (this.baseDir == null) {
177      throw new IllegalStateException("Please set basedir on filesystem before adding files");
178    }
179    inputFile.setModuleBaseDir(this.baseDir);
180    cache.add(inputFile);
181    String language = inputFile.language();
182    if (language != null) {
183      languages.add(language);
184    }
185    return this;
186  }
187
188  /**
189   * Adds InputDir to the list.
190   * Synchronized because PersistIt Exchange is not concurrent
191   */
192  public synchronized DefaultFileSystem add(DefaultInputDir inputDir) {
193    if (this.baseDir == null) {
194      throw new IllegalStateException("Please set basedir on filesystem before adding dirs");
195    }
196    inputDir.setModuleBaseDir(this.baseDir);
197    cache.add(inputDir);
198    return this;
199  }
200
201  /**
202   * Adds a language to the list. To be used only for unit tests that need to use {@link #languages()} without
203   * using {@link #add(org.sonar.api.batch.fs.InputFile)}.
204   */
205  public DefaultFileSystem addLanguages(String language, String... others) {
206    languages.add(language);
207    Collections.addAll(languages, others);
208    return this;
209  }
210
211  @Override
212  public SortedSet<String> languages() {
213    doPreloadFiles();
214    return languages;
215  }
216
217  @Override
218  public FilePredicates predicates() {
219    return predicates;
220  }
221
222  /**
223   * This method is called before each search of files.
224   */
225  protected void doPreloadFiles() {
226    // nothing to do by default
227  }
228
229  public abstract static class Cache implements Index {
230    @Override
231    public abstract Iterable<InputFile> inputFiles();
232
233    @Override
234    @CheckForNull
235    public abstract InputFile inputFile(String relativePath);
236
237    @Override
238    @CheckForNull
239    public abstract InputDir inputDir(String relativePath);
240
241    protected abstract void doAdd(InputFile inputFile);
242
243    protected abstract void doAdd(InputDir inputDir);
244
245    final void add(InputFile inputFile) {
246      doAdd(inputFile);
247    }
248
249    public void add(InputDir inputDir) {
250      doAdd(inputDir);
251    }
252
253  }
254
255  /**
256   * Used only for testing
257   */
258  private static class MapCache extends Cache {
259    private final Map<String, InputFile> fileMap = new HashMap<String, InputFile>();
260    private final Map<String, InputDir> dirMap = new HashMap<String, InputDir>();
261
262    @Override
263    public Iterable<InputFile> inputFiles() {
264      return new ArrayList<InputFile>(fileMap.values());
265    }
266
267    @Override
268    public InputFile inputFile(String relativePath) {
269      return fileMap.get(relativePath);
270    }
271
272    @Override
273    public InputDir inputDir(String relativePath) {
274      return dirMap.get(relativePath);
275    }
276
277    @Override
278    protected void doAdd(InputFile inputFile) {
279      fileMap.put(inputFile.relativePath(), inputFile);
280    }
281
282    @Override
283    protected void doAdd(InputDir inputDir) {
284      dirMap.put(inputDir.relativePath(), inputDir);
285    }
286  }
287
288  @Override
289  public File resolvePath(String path) {
290    File file = new File(path);
291    if (!file.isAbsolute()) {
292      try {
293        file = new File(baseDir(), path).getCanonicalFile();
294      } catch (IOException e) {
295        throw new IllegalArgumentException("Unable to resolve path '" + path + "'", e);
296      }
297    }
298    return file;
299  }
300}