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     */
020    package org.sonar.batch.maven;
021    
022    import com.google.common.annotations.VisibleForTesting;
023    import com.google.common.base.Function;
024    import com.google.common.base.Predicate;
025    import com.google.common.collect.Collections2;
026    import com.google.common.collect.Lists;
027    import com.google.common.collect.Maps;
028    import org.apache.commons.lang.StringUtils;
029    import org.apache.maven.model.CiManagement;
030    import org.apache.maven.model.IssueManagement;
031    import org.apache.maven.model.Scm;
032    import org.apache.maven.project.MavenProject;
033    import org.sonar.api.CoreProperties;
034    import org.sonar.api.batch.SupportedEnvironment;
035    import org.sonar.api.batch.bootstrap.ProjectDefinition;
036    import org.sonar.api.batch.maven.MavenUtils;
037    import org.sonar.api.task.TaskExtension;
038    import org.sonar.api.utils.MessageException;
039    import org.sonar.batch.scan.filesystem.DefaultModuleFileSystem;
040    import org.sonar.java.api.JavaUtils;
041    
042    import javax.annotation.Nullable;
043    
044    import java.io.File;
045    import java.io.IOException;
046    import java.util.Arrays;
047    import java.util.Collection;
048    import java.util.List;
049    import java.util.Map;
050    
051    /**
052     * @deprecated since 4.3 kept only to support old version of SonarQube Mojo
053     */
054    @Deprecated
055    @SupportedEnvironment("maven")
056    public class MavenProjectConverter implements TaskExtension {
057    
058      private static final String UNABLE_TO_DETERMINE_PROJECT_STRUCTURE_EXCEPTION_MESSAGE = "Unable to determine structure of project." +
059        " Probably you use Maven Advanced Reactor Options, which is not supported by SonarQube and should not be used.";
060    
061      public ProjectDefinition configure(List<MavenProject> poms, MavenProject root) {
062        // projects by canonical path to pom.xml
063        Map<String, MavenProject> paths = Maps.newHashMap();
064        Map<MavenProject, ProjectDefinition> defs = Maps.newHashMap();
065    
066        try {
067          configureModules(poms, paths, defs);
068    
069          rebuildModuleHierarchy(paths, defs);
070        } catch (IOException e) {
071          throw new IllegalStateException("Cannot configure project", e);
072        }
073    
074        ProjectDefinition rootProject = defs.get(root);
075        if (rootProject == null) {
076          throw new IllegalStateException(UNABLE_TO_DETERMINE_PROJECT_STRUCTURE_EXCEPTION_MESSAGE);
077        }
078        return rootProject;
079      }
080    
081      private void rebuildModuleHierarchy(Map<String, MavenProject> paths, Map<MavenProject, ProjectDefinition> defs) throws IOException {
082        for (Map.Entry<String, MavenProject> entry : paths.entrySet()) {
083          MavenProject pom = entry.getValue();
084          for (Object m : pom.getModules()) {
085            String moduleId = (String) m;
086            File modulePath = new File(pom.getBasedir(), moduleId);
087            MavenProject module = findMavenProject(modulePath, paths);
088    
089            ProjectDefinition parentProject = defs.get(pom);
090            if (parentProject == null) {
091              throw new IllegalStateException(UNABLE_TO_DETERMINE_PROJECT_STRUCTURE_EXCEPTION_MESSAGE);
092            }
093            ProjectDefinition subProject = defs.get(module);
094            if (subProject == null) {
095              throw new IllegalStateException(UNABLE_TO_DETERMINE_PROJECT_STRUCTURE_EXCEPTION_MESSAGE);
096            }
097            parentProject.addSubProject(subProject);
098          }
099        }
100      }
101    
102      private void configureModules(List<MavenProject> poms, Map<String, MavenProject> paths, Map<MavenProject, ProjectDefinition> defs) throws IOException {
103        for (MavenProject pom : poms) {
104          paths.put(pom.getFile().getCanonicalPath(), pom);
105          ProjectDefinition def = ProjectDefinition.create();
106          merge(pom, def);
107          defs.put(pom, def);
108        }
109      }
110    
111      private static MavenProject findMavenProject(final File modulePath, Map<String, MavenProject> paths) throws IOException {
112        if (modulePath.exists() && modulePath.isDirectory()) {
113          for (Map.Entry<String, MavenProject> entry : paths.entrySet()) {
114            String pomFileParentDir = new File(entry.getKey()).getParent();
115            if (pomFileParentDir.equals(modulePath.getCanonicalPath())) {
116              return entry.getValue();
117            }
118          }
119          return null;
120        }
121        return paths.get(modulePath.getCanonicalPath());
122      }
123    
124      @VisibleForTesting
125      void merge(MavenProject pom, ProjectDefinition definition) {
126        String key = getSonarKey(pom);
127        // IMPORTANT NOTE : reference on properties from POM model must not be saved,
128        // instead they should be copied explicitly - see SONAR-2896
129        definition
130          .setProperties(pom.getModel().getProperties())
131          .setKey(key)
132          .setVersion(pom.getVersion())
133          .setName(pom.getName())
134          .setDescription(pom.getDescription())
135          .addContainerExtension(pom);
136        guessJavaVersion(pom, definition);
137        guessEncoding(pom, definition);
138        convertMavenLinksToProperties(definition, pom);
139        synchronizeFileSystem(pom, definition);
140      }
141    
142      private static String getSonarKey(MavenProject pom) {
143        return new StringBuilder().append(pom.getGroupId()).append(":").append(pom.getArtifactId()).toString();
144      }
145    
146      private static void guessEncoding(MavenProject pom, ProjectDefinition definition) {
147        // See http://jira.codehaus.org/browse/SONAR-2151
148        String encoding = MavenUtils.getSourceEncoding(pom);
149        if (encoding != null) {
150          definition.setProperty(CoreProperties.ENCODING_PROPERTY, encoding);
151        }
152      }
153    
154      private static void guessJavaVersion(MavenProject pom, ProjectDefinition definition) {
155        // See http://jira.codehaus.org/browse/SONAR-2148
156        // Get Java source and target versions from maven-compiler-plugin.
157        String version = MavenUtils.getJavaSourceVersion(pom);
158        if (version != null) {
159          definition.setProperty(JavaUtils.JAVA_SOURCE_PROPERTY, version);
160        }
161        version = MavenUtils.getJavaVersion(pom);
162        if (version != null) {
163          definition.setProperty(JavaUtils.JAVA_TARGET_PROPERTY, version);
164        }
165      }
166    
167      /**
168       * For SONAR-3676
169       */
170      private static void convertMavenLinksToProperties(ProjectDefinition definition, MavenProject pom) {
171        setPropertyIfNotAlreadyExists(definition, CoreProperties.LINKS_HOME_PAGE, pom.getUrl());
172    
173        Scm scm = pom.getScm();
174        if (scm == null) {
175          scm = new Scm();
176        }
177        setPropertyIfNotAlreadyExists(definition, CoreProperties.LINKS_SOURCES, scm.getUrl());
178        setPropertyIfNotAlreadyExists(definition, CoreProperties.LINKS_SOURCES_DEV, scm.getDeveloperConnection());
179    
180        CiManagement ci = pom.getCiManagement();
181        if (ci == null) {
182          ci = new CiManagement();
183        }
184        setPropertyIfNotAlreadyExists(definition, CoreProperties.LINKS_CI, ci.getUrl());
185    
186        IssueManagement issues = pom.getIssueManagement();
187        if (issues == null) {
188          issues = new IssueManagement();
189        }
190        setPropertyIfNotAlreadyExists(definition, CoreProperties.LINKS_ISSUE_TRACKER, issues.getUrl());
191      }
192    
193      private static void setPropertyIfNotAlreadyExists(ProjectDefinition definition, String propertyKey, String propertyValue) {
194        if (StringUtils.isBlank(definition.getProperties().getProperty(propertyKey))) {
195          definition.setProperty(propertyKey, StringUtils.defaultString(propertyValue));
196        }
197      }
198    
199      public void synchronizeFileSystem(MavenProject pom, ProjectDefinition into) {
200        into.setBaseDir(pom.getBasedir());
201        File buildDir = getBuildDir(pom);
202        if (buildDir != null) {
203          into.setBuildDir(buildDir);
204          into.setWorkDir(getSonarWorkDir(pom));
205        }
206        into.setSourceDirs(toPaths(mainDirs(pom)));
207        into.setTestDirs(toPaths(testDirs(pom)));
208        File binaryDir = resolvePath(pom.getBuild().getOutputDirectory(), pom.getBasedir());
209        if (binaryDir != null) {
210          into.addBinaryDir(binaryDir);
211        }
212      }
213    
214      public static File getSonarWorkDir(MavenProject pom) {
215        return new File(getBuildDir(pom), "sonar");
216      }
217    
218      private static File getBuildDir(MavenProject pom) {
219        return resolvePath(pom.getBuild().getDirectory(), pom.getBasedir());
220      }
221    
222      public void synchronizeFileSystem(MavenProject pom, DefaultModuleFileSystem into) {
223        into.resetDirs(
224          pom.getBasedir(),
225          getBuildDir(pom),
226          mainDirs(pom),
227          testDirs(pom),
228          Arrays.asList(resolvePath(pom.getBuild().getOutputDirectory(), pom.getBasedir())));
229      }
230    
231      static File resolvePath(@Nullable String path, File basedir) {
232        if (path != null) {
233          File file = new File(StringUtils.trim(path));
234          if (!file.isAbsolute()) {
235            try {
236              file = new File(basedir, path).getCanonicalFile();
237            } catch (IOException e) {
238              throw new IllegalStateException("Unable to resolve path '" + path + "'", e);
239            }
240          }
241          return file;
242        }
243        return null;
244      }
245    
246      static List<File> resolvePaths(List<String> paths, File basedir) {
247        List<File> result = Lists.newArrayList();
248        for (String path : paths) {
249          File dir = resolvePath(path, basedir);
250          if (dir != null) {
251            result.add(dir);
252          }
253        }
254        return result;
255      }
256    
257      private List<File> mainDirs(MavenProject pom) {
258        return sourceDirs(pom, ProjectDefinition.SOURCE_DIRS_PROPERTY, pom.getCompileSourceRoots());
259      }
260    
261      private List<File> testDirs(MavenProject pom) {
262        return sourceDirs(pom, ProjectDefinition.TEST_DIRS_PROPERTY, pom.getTestCompileSourceRoots());
263      }
264    
265      private List<File> sourceDirs(MavenProject pom, String propertyKey, List mavenDirs) {
266        List<String> paths;
267        String prop = pom.getProperties().getProperty(propertyKey);
268        if (prop != null) {
269          paths = Arrays.asList(StringUtils.split(prop, ","));
270          // do not remove dirs that do not exist. They must be kept in order to
271          // notify users that value of sonar.sources has a typo.
272          return existingDirsOrFail(resolvePaths(paths, pom.getBasedir()), pom, propertyKey);
273        }
274    
275        List<File> dirs = resolvePaths(mavenDirs, pom.getBasedir());
276    
277        // Maven provides some directories that do not exist. They
278        // should be removed
279        return keepExistingDirs(dirs);
280      }
281    
282      private List<File> existingDirsOrFail(List<File> dirs, MavenProject pom, String propertyKey) {
283        for (File dir : dirs) {
284          if (!dir.isDirectory() || !dir.exists()) {
285            throw MessageException.of(String.format(
286              "The directory '%s' does not exist for Maven module %s. Please check the property %s",
287              dir.getAbsolutePath(), pom.getId(), propertyKey));
288          }
289        }
290        return dirs;
291      }
292    
293      private static List<File> keepExistingDirs(List<File> files) {
294        return Lists.newArrayList(Collections2.filter(files, new Predicate<File>() {
295          @Override
296          public boolean apply(File dir) {
297            return dir != null && dir.exists() && dir.isDirectory();
298          }
299        }));
300      }
301    
302      private static String[] toPaths(Collection<File> dirs) {
303        Collection<String> paths = Collections2.transform(dirs, new Function<File, String>() {
304          @Override
305          public String apply(File dir) {
306            return dir.getAbsolutePath();
307          }
308        });
309        return paths.toArray(new String[paths.size()]);
310      }
311    }