001    /*
002     * Sonar, open source software quality management tool.
003     * Copyright (C) 2009 SonarSource SA
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 org.apache.commons.io.FileUtils;
023    import org.apache.commons.io.FilenameUtils;
024    import org.apache.commons.io.filefilter.*;
025    import org.apache.commons.lang.StringUtils;
026    import org.sonar.api.batch.maven.MavenUtils;
027    import org.sonar.api.utils.SonarException;
028    import org.sonar.api.utils.WildcardPattern;
029    
030    import java.io.File;
031    import java.io.IOException;
032    import java.nio.charset.Charset;
033    import java.util.ArrayList;
034    import java.util.Arrays;
035    import java.util.List;
036    
037    /**
038     * An implementation  of ProjectFileSystem
039     * @since 1.10
040     */
041    public class DefaultProjectFileSystem implements ProjectFileSystem {
042    
043      private Project project;
044    
045      /**
046       * Creates a DefaultProjectFileSystem based on a project
047       * @param project
048       */
049      public DefaultProjectFileSystem(Project project) {
050        this.project = project;
051      }
052    
053      /**
054       * Source encoding. Never null, it returns the default plateform charset if it is not defined in project.
055       */
056      public Charset getSourceCharset() {
057        return MavenUtils.getSourceCharset(project.getPom());
058      }
059    
060    
061      /**
062       * Basedir is the project root directory.
063       */
064      public File getBasedir() {
065        return project.getPom().getBasedir();
066      }
067    
068      /**
069       * Build directory is by default "target" in maven projects.
070       */
071      public File getBuildDir() {
072        return resolvePath(project.getPom().getBuild().getDirectory());
073      }
074    
075      /**
076       * Directory where classes are placed. By default "target/classes" in maven projects.
077       */
078      public File getBuildOutputDir() {
079        return resolvePath(project.getPom().getBuild().getOutputDirectory());
080      }
081    
082      /**
083       * The list of directories for sources
084       */
085      public List<File> getSourceDirs() {
086        return resolvePaths(project.getPom().getCompileSourceRoots());
087      }
088    
089      /**
090       * Adds a source directory
091    
092       * @return the current object
093       */
094      public DefaultProjectFileSystem addSourceDir(File dir) {
095        if (dir == null) {
096          throw new IllegalArgumentException("Can not add null to project source dirs");
097        }
098        project.getPom().getCompileSourceRoots().add(0, dir.getAbsolutePath());
099        return this;
100      }
101    
102      /**
103       * The list of directories for tests
104       */
105      public List<File> getTestDirs() {
106        return resolvePaths(project.getPom().getTestCompileSourceRoots());
107      }
108    
109      /**
110       * Adds a test directory
111       *
112       * @return the current object
113       */
114      public DefaultProjectFileSystem addTestDir(File dir) {
115        if (dir == null) {
116          throw new IllegalArgumentException("Can not add null to project test dirs");
117        }
118        project.getPom().getTestCompileSourceRoots().add(0, dir.getAbsolutePath());
119        return this;
120      }
121    
122      /**
123       * @return the directory where reporting is placed. Default is target/sites
124       */
125      public File getReportOutputDir() {
126        return resolvePath(project.getPom().getReporting().getOutputDirectory());
127      }
128    
129      /**
130       * @return the Sonar working directory. Default is "target/sonar"
131       */
132      public File getSonarWorkingDirectory() {
133        try {
134          File dir = new File(project.getPom().getBuild().getDirectory(), "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          file = new File(project.getPom().getBasedir(), path);
147        }
148        return file;
149      }
150    
151      private List<File> resolvePaths(List<String> paths) {
152        List<File> result = new ArrayList<File>();
153        if (paths != null) {
154          for (String path : paths) {
155            result.add(resolvePath(path));
156          }
157        }
158    
159        return result;
160      }
161    
162      /**
163       * Gets the list of source files for given languages
164       *
165       * @param langs language filter. If null or empty, will return empty list
166       */
167      public List<File> getSourceFiles(Language... langs) {
168        return getFiles(getSourceDirs(), true, langs);
169      }
170    
171      /**
172       * Gets the list of java source files
173       * @return
174       */
175      public List<File> getJavaSourceFiles() {
176        return getSourceFiles(Java.INSTANCE);
177      }
178    
179      /**
180       * @return whether there are java source
181       */
182      public boolean hasJavaSourceFiles() {
183        return !getJavaSourceFiles().isEmpty();
184      }
185    
186      /**
187       * Gets the list of test files for given languages
188       *
189       * @param langs language filter. If null or empty, will return empty list
190       */
191      public List<File> getTestFiles(Language... langs) {
192        return getFiles(getTestDirs(), false, langs);
193      }
194    
195      /**
196       * @return whether there are tests files
197       */
198      public boolean hasTestFiles(Language lang) {
199        return !getTestFiles(lang).isEmpty();
200      }
201    
202      private List<File> getFiles(List<File> directories, boolean applyExclusionPatterns, Language... langs) {
203        List<File> result = new ArrayList<File>();
204        if (directories == null || langs == null) {
205          return result;
206        }
207    
208        IOFileFilter suffixFilter = getFileSuffixFilter(langs);
209        WildcardPattern[] exclusionPatterns = getExclusionPatterns(applyExclusionPatterns);
210    
211        for (File dir : directories) {
212          if (dir.exists()) {
213            IOFileFilter exclusionFilter = new ExclusionFilter(dir, exclusionPatterns);
214            result.addAll(FileUtils.listFiles(dir, new AndFileFilter(suffixFilter, exclusionFilter), TrueFileFilter.INSTANCE));
215          }
216        }
217        return result;
218      }
219    
220      private WildcardPattern[] getExclusionPatterns(boolean applyExclusionPatterns) {
221        WildcardPattern[] exclusionPatterns;
222        if (applyExclusionPatterns) {
223          exclusionPatterns = WildcardPattern.create(project.getExclusionPatterns());
224        } else {
225          exclusionPatterns = new WildcardPattern[0];
226        }
227        return exclusionPatterns;
228      }
229    
230      private IOFileFilter getFileSuffixFilter(Language... langs) {
231        IOFileFilter suffixFilter;
232        if (langs.length == 0) {
233          suffixFilter = FileFilterUtils.trueFileFilter();
234    
235        } else {
236          List<String> suffixes = new ArrayList<String>();
237          for (Language lang : langs) {
238            if (lang.getFileSuffixes() != null) {
239              suffixes.addAll(Arrays.asList(lang.getFileSuffixes()));
240            }
241          }
242          suffixFilter = new SuffixFileFilter(suffixes);
243        }
244        return suffixFilter;
245      }
246    
247      private static class ExclusionFilter implements IOFileFilter {
248        File sourceDir;
249        WildcardPattern[] patterns;
250    
251        ExclusionFilter(File sourceDir, WildcardPattern[] patterns) {
252          this.sourceDir = sourceDir;
253          this.patterns = patterns;
254        }
255    
256        public boolean accept(File file) {
257          String relativePath = getRelativePath(file, sourceDir);
258          if (relativePath == null) {
259            return false;
260          }
261          for (WildcardPattern pattern : patterns) {
262            if (pattern.match(relativePath)) {
263              return false;
264            }
265          }
266          return true;
267        }
268    
269        public boolean accept(File file, String name) {
270          return accept(file);
271        }
272      }
273    
274      /**
275       * Save data into a new file of Sonar working directory.
276       *
277       * @return the created file
278       */
279      public File writeToWorkingDirectory(String content, String fileName) throws IOException {
280        return writeToFile(content, getSonarWorkingDirectory(), fileName);
281      }
282    
283      protected static File writeToFile(String content, File dir, String fileName) throws IOException {
284        File file = new File(dir, fileName);
285        FileUtils.writeStringToFile(file, content, "UTF-8");
286        return file;
287      }
288    
289      /**
290       * getRelativePath("c:/foo/src/my/package/Hello.java", "c:/foo/src") is "my/package/Hello.java"
291       *
292       * @return null if file is not in dir (including recursive subdirectories)
293       */
294      public static String getRelativePath(File file, File dir) {
295        return getRelativePath(file, Arrays.asList(dir));
296      }
297    
298      /**
299       * getRelativePath("c:/foo/src/my/package/Hello.java", ["c:/bar", "c:/foo/src"]) is "my/package/Hello.java".
300       * <p/>
301       * <p>Relative path is composed of slashes. Windows backslaches are replaced by /</p>
302       *
303       * @return null if file is not in dir (including recursive subdirectories)
304       */
305      public static String getRelativePath(File file, List<File> dirs) {
306        List<String> stack = new ArrayList<String>();
307        String path = FilenameUtils.normalize(file.getAbsolutePath());
308        File cursor = new File(path);
309        while (cursor != null) {
310          if (containsFile(dirs, cursor)) {
311            return StringUtils.join(stack, "/");
312          }
313          stack.add(0, cursor.getName());
314          cursor = cursor.getParentFile();
315        }
316        return null;
317      }
318    
319      public File getFileFromBuildDirectory(String filename) {
320        File file = new File(getBuildDir(), filename);
321        return (file.exists() ? file : null);
322      }
323    
324      public Resource toResource(File file) {
325        if (file == null || !file.exists()) {
326          return null;
327        }
328    
329        String relativePath = getRelativePath(file, getSourceDirs());
330        if (relativePath == null) {
331          return null;
332        }
333    
334        return (file.isFile() ? new org.sonar.api.resources.File(relativePath) : new org.sonar.api.resources.Directory(relativePath));
335      }
336    
337      private static boolean containsFile(List<File> dirs, File cursor) {
338        for (File dir : dirs) {
339          if (FilenameUtils.equalsNormalizedOnSystem(dir.getAbsolutePath(), cursor.getAbsolutePath())) {
340            return true;
341          }
342        }
343        return false;
344      }
345    
346    }