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