001/*
002 * Sonar, open source software quality management tool.
003 * Copyright (C) 2008-2012 SonarSource
004 * mailto:contact AT sonarsource DOT com
005 *
006 * Sonar 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 * Sonar 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
017 * License along with Sonar; if not, write to the Free Software
018 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
019 */
020package org.sonar.api.resources;
021
022import com.google.common.base.Predicate;
023import com.google.common.collect.ImmutableList;
024import com.google.common.collect.Iterables;
025import com.google.common.collect.Lists;
026import com.google.common.collect.Sets;
027import org.apache.commons.io.FileUtils;
028import org.apache.commons.io.FilenameUtils;
029import org.apache.commons.io.filefilter.*;
030import org.apache.commons.lang.CharEncoding;
031import org.apache.commons.lang.StringUtils;
032import org.sonar.api.CoreProperties;
033import org.sonar.api.batch.FileFilter;
034import org.sonar.api.utils.Logs;
035import org.sonar.api.utils.SonarException;
036import org.sonar.api.utils.WildcardPattern;
037
038import javax.annotation.Nullable;
039import java.io.File;
040import java.io.IOException;
041import java.nio.charset.Charset;
042import java.util.*;
043
044/**
045 * An implementation of {@link ProjectFileSystem}.
046 * For internal use only.
047 *
048 * @since 1.10
049 * @deprecated in 2.8. In fact this class should not be located in sonar-plugin-api and most of the methods were overridden by a component in sonar-batch.
050 */
051@Deprecated
052public class DefaultProjectFileSystem implements ProjectFileSystem {
053
054  protected static final Predicate<File> DIRECTORY_EXISTS = new Predicate<File>() {
055    public boolean apply(@Nullable File input) {
056      return input != null && input.exists() && input.isDirectory();
057    }
058  };
059
060  private Project project;
061  private Languages languages;
062  private List<IOFileFilter> filters = Lists.newArrayList();
063
064  public DefaultProjectFileSystem(Project project, Languages languages) {
065    this.project = project;
066    this.languages = languages;
067  }
068
069  public DefaultProjectFileSystem(Project project, Languages languages, FileFilter... fileFilters) {
070    this(project, languages);
071    for (FileFilter fileFilter : fileFilters) {
072      filters.add(new DelegateFileFilter(fileFilter));
073    }
074  }
075
076  public Charset getSourceCharset() {
077    String encoding = project.getConfiguration().getString(CoreProperties.ENCODING_PROPERTY);
078    if (StringUtils.isNotEmpty(encoding)) {
079      try {
080        return Charset.forName(encoding);
081      } catch (Exception e) {
082        Logs.INFO.warn("Can not get project charset", e);
083      }
084    }
085    return Charset.defaultCharset();
086  }
087
088  public File getBasedir() {
089    return project.getPom().getBasedir();
090  }
091
092  public File getBuildDir() {
093    return resolvePath(project.getPom().getBuild().getDirectory());
094  }
095
096  public File getBuildOutputDir() {
097    return resolvePath(project.getPom().getBuild().getOutputDirectory());
098  }
099
100  /**
101   * Maven can modify source directories during Sonar execution - see MavenPhaseExecutor.
102   */
103  public List<File> getSourceDirs() {
104    return ImmutableList.copyOf(Iterables.filter(resolvePaths(project.getPom().getCompileSourceRoots()), DIRECTORY_EXISTS));
105  }
106
107  /**
108   * @deprecated since 2.6, because should be immutable
109   */
110  @Deprecated
111  public DefaultProjectFileSystem addSourceDir(File dir) {
112    if (dir == null) {
113      throw new IllegalArgumentException("Can not add null to project source dirs");
114    }
115    project.getPom().getCompileSourceRoots().add(0, dir.getAbsolutePath());
116    return this;
117  }
118
119  /**
120   * Maven can modify test directories during Sonar execution - see MavenPhaseExecutor.
121   */
122  public List<File> getTestDirs() {
123    return ImmutableList.copyOf(Iterables.filter(resolvePaths(project.getPom().getTestCompileSourceRoots()), DIRECTORY_EXISTS));
124  }
125
126  /**
127   * @deprecated since 2.6, because should be immutable
128   */
129  @Deprecated
130  public DefaultProjectFileSystem addTestDir(File dir) {
131    if (dir == null) {
132      throw new IllegalArgumentException("Can not add null to project test dirs");
133    }
134    project.getPom().getTestCompileSourceRoots().add(0, dir.getAbsolutePath());
135    return this;
136  }
137
138  public File getReportOutputDir() {
139    return resolvePath(project.getPom().getReporting().getOutputDirectory());
140  }
141
142  public File getSonarWorkingDirectory() {
143    try {
144      File dir = new File(getBuildDir(), "sonar");
145      FileUtils.forceMkdir(dir);
146      return dir;
147
148    } catch (IOException e) {
149      throw new SonarException("Unable to retrieve Sonar working directory.", e);
150    }
151  }
152
153  public File resolvePath(String path) {
154    File file = new File(path);
155    if (!file.isAbsolute()) {
156      try {
157        file = new File(getBasedir(), path).getCanonicalFile();
158      } catch (IOException e) {
159        throw new SonarException("Unable to resolve path '" + path + "'", e);
160      }
161    }
162    return file;
163  }
164
165  protected List<File> resolvePaths(List<String> paths) {
166    List<File> result = Lists.newArrayList();
167    if (paths != null) {
168      for (String path : paths) {
169        result.add(resolvePath(path));
170      }
171    }
172    return result;
173  }
174
175  /**
176   * @deprecated in 2.6, use {@link #mainFiles(String...)} instead
177   */
178  @Deprecated
179  public List<File> getSourceFiles(Language... langs) {
180    return toFiles(mainFiles(getLanguageKeys(langs)));
181  }
182
183  /**
184   * @deprecated in 2.6, use {@link #mainFiles(String...)} instead
185   */
186  @Deprecated
187  public List<File> getJavaSourceFiles() {
188    return getSourceFiles(Java.INSTANCE);
189  }
190
191  public boolean hasJavaSourceFiles() {
192    return !mainFiles(Java.KEY).isEmpty();
193  }
194
195  /**
196   * @deprecated in 2.6, use {@link #testFiles(String...)} instead
197   */
198  @Deprecated
199  public List<File> getTestFiles(Language... langs) {
200    return toFiles(testFiles(getLanguageKeys(langs)));
201  }
202
203  /**
204   * @deprecated in 2.6
205   */
206  @Deprecated
207  public boolean hasTestFiles(Language lang) {
208    return !testFiles(lang.getKey()).isEmpty();
209  }
210
211  List<InputFile> getFiles(List<File> directories, List<File> initialFiles, boolean applyExclusionPatterns, String... langs) {
212    List<InputFile> result = Lists.newArrayList();
213    if (directories == null) {
214      return result;
215    }
216
217    IOFileFilter suffixFilter = getFileSuffixFilter(langs);
218    WildcardPattern[] exclusionPatterns = getExclusionPatterns(applyExclusionPatterns);
219
220    IOFileFilter initialFilesFilter = TrueFileFilter.INSTANCE;
221    if (initialFiles != null && !initialFiles.isEmpty()) {
222      initialFilesFilter = new FileSelectionFilter(initialFiles);
223    }
224
225    for (File dir : directories) {
226      if (dir.exists()) {
227        IOFileFilter exclusionFilter = new ExclusionFilter(dir, exclusionPatterns);
228        IOFileFilter visibleFileFilter = HiddenFileFilter.VISIBLE;
229        List<IOFileFilter> fileFilters = Lists.newArrayList(visibleFileFilter, suffixFilter, exclusionFilter, initialFilesFilter);
230        fileFilters.addAll(this.filters);
231
232        IOFileFilter dotPrefixDirFilter = FileFilterUtils.notFileFilter(FileFilterUtils.prefixFileFilter("."));
233        List<File> files = (List<File>) FileUtils.listFiles(dir, new AndFileFilter(fileFilters), FileFilterUtils.and(HiddenFileFilter.VISIBLE, dotPrefixDirFilter));
234        for (File file : files) {
235          String relativePath = DefaultProjectFileSystem.getRelativePath(file, dir);
236          result.add(InputFileUtils.create(dir, relativePath));
237        }
238      }
239    }
240    return result;
241  }
242
243  private WildcardPattern[] getExclusionPatterns(boolean applyExclusionPatterns) {
244    WildcardPattern[] exclusionPatterns;
245    if (applyExclusionPatterns) {
246      exclusionPatterns = WildcardPattern.create(project.getExclusionPatterns());
247    } else {
248      exclusionPatterns = new WildcardPattern[0];
249    }
250    return exclusionPatterns;
251  }
252
253  private IOFileFilter getFileSuffixFilter(String... langKeys) {
254    IOFileFilter suffixFilter = FileFilterUtils.trueFileFilter();
255    if (langKeys != null && langKeys.length > 0) {
256      List<String> suffixes = Arrays.asList(languages.getSuffixes(langKeys));
257      if (!suffixes.isEmpty()) {
258        suffixFilter = new SuffixFileFilter(suffixes);
259      }
260    }
261    return suffixFilter;
262  }
263
264  private static class ExclusionFilter implements IOFileFilter {
265    File sourceDir;
266    WildcardPattern[] patterns;
267
268    ExclusionFilter(File sourceDir, WildcardPattern[] patterns) {
269      this.sourceDir = sourceDir;
270      this.patterns = patterns;
271    }
272
273    public boolean accept(File file) {
274      String relativePath = getRelativePath(file, sourceDir);
275      if (relativePath == null) {
276        return false;
277      }
278      for (WildcardPattern pattern : patterns) {
279        if (pattern.match(relativePath)) {
280          return false;
281        }
282      }
283      return true;
284    }
285
286    public boolean accept(File file, String name) {
287      return accept(file);
288    }
289  }
290
291  static class FileSelectionFilter implements IOFileFilter {
292    private Set<File> files;
293
294    public FileSelectionFilter(List<File> f) {
295      files = Sets.newHashSet(f);
296    }
297
298    public boolean accept(File file) {
299      return files.contains(file);
300    }
301
302    public boolean accept(File file, String name) {
303      return accept(file);
304    }
305  }
306
307  public File writeToWorkingDirectory(String content, String fileName) throws IOException {
308    return writeToFile(content, getSonarWorkingDirectory(), fileName);
309  }
310
311  protected static File writeToFile(String content, File dir, String fileName) throws IOException {
312    File file = new File(dir, fileName);
313    FileUtils.writeStringToFile(file, content, CharEncoding.UTF_8);
314    return file;
315  }
316
317  /**
318   * getRelativePath("c:/foo/src/my/package/Hello.java", "c:/foo/src") is "my/package/Hello.java"
319   *
320   * @return null if file is not in dir (including recursive subdirectories)
321   */
322  public static String getRelativePath(File file, File dir) {
323    return getRelativePath(file, Arrays.asList(dir));
324  }
325
326  /**
327   * getRelativePath("c:/foo/src/my/package/Hello.java", ["c:/bar", "c:/foo/src"]) is "my/package/Hello.java".
328   * <p>
329   * Relative path is composed of slashes. Windows backslaches are replaced by /
330   * </p>
331   *
332   * @return null if file is not in dir (including recursive subdirectories)
333   */
334  public static String getRelativePath(File file, List<File> dirs) {
335    List<String> stack = new ArrayList<String>();
336    String path = FilenameUtils.normalize(file.getAbsolutePath());
337    File cursor = new File(path);
338    while (cursor != null) {
339      if (containsFile(dirs, cursor)) {
340        return StringUtils.join(stack, "/");
341      }
342      stack.add(0, cursor.getName());
343      cursor = cursor.getParentFile();
344    }
345    return null;
346  }
347
348  public File getFileFromBuildDirectory(String filename) {
349    File file = new File(getBuildDir(), filename);
350    return (file.exists() ? file : null);
351  }
352
353  public Resource toResource(File file) {
354    if (file == null || !file.exists()) {
355      return null;
356    }
357
358    String relativePath = getRelativePath(file, getSourceDirs());
359    if (relativePath == null) {
360      return null;
361    }
362
363    return (file.isFile() ? new org.sonar.api.resources.File(relativePath) : new org.sonar.api.resources.Directory(relativePath));
364  }
365
366  private static boolean containsFile(List<File> dirs, File cursor) {
367    for (File dir : dirs) {
368      if (FilenameUtils.equalsNormalizedOnSystem(dir.getAbsolutePath(), cursor.getAbsolutePath())) {
369        return true;
370      }
371    }
372    return false;
373  }
374
375  /**
376   * Conversion from Language to key. Allows to provide backward compatibility.
377   */
378  private String[] getLanguageKeys(Language[] langs) {
379    String[] keys = new String[langs.length];
380    for (int i = 0; i < langs.length; i++) {
381      keys[i] = langs[i].getKey();
382    }
383    return keys;
384  }
385
386  /**
387   * Conversion from InputFile to File. Allows to provide backward compatibility.
388   */
389  private static List<File> toFiles(List<InputFile> files) {
390    List<File> result = Lists.newArrayList();
391    for (InputFile file : files) {
392      result.add(file.getFile());
393    }
394    return result;
395  }
396
397  /**
398   * @since 2.6
399   */
400  public List<InputFile> mainFiles(String... langs) {
401    return getFiles(getSourceDirs(), getInitialSourceFiles(), true, langs);
402  }
403
404  /**
405   * @since 2.6
406   */
407  public List<InputFile> testFiles(String... langs) {
408    return getFiles(getTestDirs(), getInitialTestFiles(), false /* FIXME should be true? */, langs);
409  }
410
411  protected List<File> getInitialSourceFiles() {
412    return Collections.emptyList();
413  }
414
415  protected List<File> getInitialTestFiles() {
416    return Collections.emptyList();
417  }
418}