View Javadoc
1   /*
2    * #%L
3    * wcm.io
4    * %%
5    * Copyright (C) 2015 wcm.io
6    * %%
7    * Licensed under the Apache License, Version 2.0 (the "License");
8    * you may not use this file except in compliance with the License.
9    * You may obtain a copy of the License at
10   *
11   *      http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing, software
14   * distributed under the License is distributed on an "AS IS" BASIS,
15   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   * See the License for the specific language governing permissions and
17   * limitations under the License.
18   * #L%
19   */
20  package io.wcm.devops.conga.tooling.maven.plugin;
21  
22  import java.io.IOException;
23  import java.io.InputStream;
24  import java.net.URL;
25  import java.util.ArrayList;
26  import java.util.Arrays;
27  import java.util.List;
28  import java.util.Properties;
29  import java.util.SortedSet;
30  import java.util.function.Predicate;
31  import java.util.stream.Collectors;
32  
33  import org.apache.commons.io.FilenameUtils;
34  import org.apache.commons.lang3.StringUtils;
35  import org.apache.maven.artifact.resolver.ResolutionErrorHandler;
36  import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
37  import org.apache.maven.execution.MavenSession;
38  import org.apache.maven.plugin.MojoExecutionException;
39  import org.apache.maven.plugin.MojoFailureException;
40  import org.apache.maven.plugins.annotations.Component;
41  import org.apache.maven.plugins.annotations.LifecyclePhase;
42  import org.apache.maven.plugins.annotations.Mojo;
43  import org.apache.maven.plugins.annotations.Parameter;
44  import org.apache.maven.plugins.annotations.ResolutionScope;
45  import org.apache.maven.project.MavenProject;
46  import org.eclipse.aether.RepositorySystem;
47  import org.eclipse.aether.RepositorySystemSession;
48  import org.eclipse.aether.repository.RemoteRepository;
49  import org.sonatype.plexus.build.incremental.BuildContext;
50  import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
51  
52  import io.wcm.devops.conga.generator.GeneratorException;
53  import io.wcm.devops.conga.generator.GeneratorOptions;
54  import io.wcm.devops.conga.generator.UrlFileManager;
55  import io.wcm.devops.conga.generator.handlebars.HandlebarsManager;
56  import io.wcm.devops.conga.generator.spi.context.PluginContextOptions;
57  import io.wcm.devops.conga.generator.spi.context.UrlFilePluginContext;
58  import io.wcm.devops.conga.generator.util.PluginManager;
59  import io.wcm.devops.conga.generator.util.PluginManagerImpl;
60  import io.wcm.devops.conga.model.environment.Environment;
61  import io.wcm.devops.conga.model.reader.EnvironmentReader;
62  import io.wcm.devops.conga.model.reader.RoleReader;
63  import io.wcm.devops.conga.model.role.Role;
64  import io.wcm.devops.conga.resource.Resource;
65  import io.wcm.devops.conga.resource.ResourceCollection;
66  import io.wcm.devops.conga.resource.ResourceInfo;
67  import io.wcm.devops.conga.resource.ResourceLoader;
68  import io.wcm.devops.conga.tooling.maven.plugin.util.ClassLoaderUtil;
69  import io.wcm.devops.conga.tooling.maven.plugin.util.MavenContext;
70  import io.wcm.devops.conga.tooling.maven.plugin.util.PathUtil;
71  import io.wcm.devops.conga.tooling.maven.plugin.util.VersionInfoUtil;
72  import io.wcm.devops.conga.tooling.maven.plugin.validation.DefinitionValidator;
73  import io.wcm.devops.conga.tooling.maven.plugin.validation.ModelValidator;
74  import io.wcm.devops.conga.tooling.maven.plugin.validation.NoValueProviderInRoleValidator;
75  import io.wcm.devops.conga.tooling.maven.plugin.validation.RoleTemplateFileValidator;
76  import io.wcm.devops.conga.tooling.maven.plugin.validation.TemplateValidator;
77  
78  /**
79   * Validates definitions by trying to parse them with model reader or compile them via handlebars.
80   * Validates that the CONGA maven plugin version and CONGA plugin versions match or are newer than those versions
81   * used when generating the dependency artifacts.
82   */
83  @Mojo(name = "validate", defaultPhase = LifecyclePhase.VALIDATE, requiresProject = true, threadSafe = true,
84      requiresDependencyResolution = ResolutionScope.COMPILE)
85  public class ValidateMojo extends AbstractCongaMojo {
86  
87    /**
88     * Selected environments to validate.
89     */
90    @Parameter(property = "conga.environments")
91    private String[] environments;
92  
93    @Parameter(property = "project", required = true, readonly = true)
94    private MavenProject project;
95  
96    @Component
97    private org.apache.maven.repository.RepositorySystem repositorySystem;
98    @Component
99    private ResolutionErrorHandler resolutionErrorHandler;
100   @Component
101   private RepositorySystem repoSystem;
102   @Component
103   private BuildContext buildContext;
104   @Parameter(defaultValue = "${repositorySystemSession}", readonly = true)
105   private RepositorySystemSession repoSession;
106   @Parameter(defaultValue = "${project.remoteProjectRepositories}", readonly = true)
107   private List<RemoteRepository> remoteRepos;
108   @Parameter(defaultValue = "${session}", readonly = true, required = false)
109   private MavenSession session;
110 
111   @Override
112   public void execute() throws MojoExecutionException, MojoFailureException {
113     List<URL> mavenProjectClasspathUrls = ClassLoaderUtil.getMavenProjectClasspathUrls(project);
114     ClassLoader mavenProjectClassLoader = ClassLoaderUtil.buildClassLoader(mavenProjectClasspathUrls);
115     ResourceLoader mavenProjectResourceLoader = new ResourceLoader(mavenProjectClassLoader);
116 
117     ResourceCollection roleDir = mavenProjectResourceLoader.getResourceCollection(ResourceLoader.FILE_PREFIX + getRoleDir());
118     ResourceCollection templateDir = mavenProjectResourceLoader.getResourceCollection(ResourceLoader.FILE_PREFIX + getTemplateDir());
119     ResourceCollection environmentDir = mavenProjectResourceLoader.getResourceCollection(ResourceLoader.FILE_PREFIX + getEnvironmentDir());
120 
121     // validate role definition syntax
122     validateFiles(roleDir, roleDir, new ModelValidator<Role>("Role", new RoleReader()));
123 
124     PluginManager pluginManager = new PluginManagerImpl();
125 
126     MavenContext mavenContext = new MavenContext()
127         .project(project)
128         .session(session)
129         .setRepositorySystem(repositorySystem)
130         .resolutionErrorHandler(resolutionErrorHandler)
131         .buildContext(buildContext)
132         .log(getLog())
133         .repoSystem(repoSystem)
134         .repoSession(repoSession)
135         .remoteRepos(remoteRepos)
136         .artifactTypeMappings(getArtifactTypeMappings());
137 
138     UrlFilePluginContext urlFilePluginContext = new UrlFilePluginContext()
139         .baseDir(project.getBasedir())
140         .resourceClassLoader(mavenProjectClassLoader)
141         .pluginContextOptions(new PluginContextOptions()
142           .containerContext(mavenContext));
143     UrlFileManager urlFileManager = new UrlFileManager(pluginManager, urlFilePluginContext);
144 
145     PluginContextOptions pluginContextOptions = new PluginContextOptions()
146         .pluginManager(pluginManager)
147         .urlFileManager(urlFileManager)
148         .valueProviderConfig(getValueProviderConfig())
149         .genericPluginConfig(getPluginConfig())
150         .containerContext(mavenContext)
151         .logger(new MavenSlf4jLogFacade(getLog()));
152 
153     // validate that all templates can be compiled
154     HandlebarsManager handlebarsManager = new HandlebarsManager(List.of(templateDir), pluginContextOptions);
155     validateFiles(templateDir, templateDir, new TemplateValidator(templateDir, handlebarsManager));
156 
157     // validate that roles reference existing templates
158     validateFiles(roleDir, roleDir, new RoleTemplateFileValidator(handlebarsManager));
159 
160     // validate that no value providers are used in role files - they should be only used in environment
161     validateFiles(roleDir, roleDir, new NoValueProviderInRoleValidator());
162 
163     // validate environment definition syntax
164     List<Environment> environmentList = validateFiles(environmentDir, environmentDir, new ModelValidator<Environment>("Environment", new EnvironmentReader()),
165         // filter environments
166         resourceInfo -> {
167           if (this.environments == null || this.environments.length == 0) {
168             return true;
169           }
170           for (String environment : this.environments) {
171             if (StringUtils.equals(environment, FilenameUtils.getBaseName(resourceInfo.getName()))) {
172               return true;
173             }
174           }
175           return false;
176         });
177 
178     // validate version information - for each environment separately
179     for (Environment environment : environmentList) {
180       UrlFilePluginContext environmentUrlFilePluginContext = new UrlFilePluginContext()
181           .baseDir(project.getBasedir())
182           .resourceClassLoader(mavenProjectClassLoader)
183           .environment(environment)
184           .pluginContextOptions(new PluginContextOptions()
185               .containerContext(mavenContext));
186       UrlFileManager environmentUrlFileManager = new UrlFileManager(pluginManager, environmentUrlFilePluginContext);
187 
188       PluginContextOptions environmentPluginContextOptions = new PluginContextOptions()
189           .pluginContextOptions(pluginContextOptions)
190           .urlFileManager(environmentUrlFileManager);
191       validateVersionInfo(environment, mavenProjectClasspathUrls, environmentPluginContextOptions);
192     }
193   }
194 
195   // ===== FILE VALIDATION =====
196 
197   private <T> List<T> validateFiles(ResourceCollection sourceDir, ResourceCollection rootSourceDir, DefinitionValidator<T> validator)
198       throws MojoFailureException {
199     return validateFiles(sourceDir, rootSourceDir, validator, resourceInfo -> true);
200   }
201 
202   private <T> List<T> validateFiles(ResourceCollection sourceDir, ResourceCollection rootSourceDir, DefinitionValidator<T> validator,
203       Predicate<ResourceInfo> resourceFilter) throws MojoFailureException {
204     if (!sourceDir.exists()) {
205       return List.of();
206     }
207     SortedSet<Resource> files = sourceDir.getResources();
208     SortedSet<ResourceCollection> dirs = sourceDir.getResourceCollections();
209     if (files.isEmpty() && dirs.isEmpty()) {
210       return List.of();
211     }
212 
213     List<T> result = new ArrayList<>();
214     for (Resource file : files) {
215       if (resourceFilter.test(file)) {
216         result.add(validator.validate(file, getPathForLog(rootSourceDir, file)));
217       }
218     }
219     for (ResourceCollection dir : dirs) {
220       if (resourceFilter.test(dir)) {
221         result.addAll(validateFiles(dir, rootSourceDir, validator, resourceFilter));
222       }
223     }
224     return result;
225   }
226 
227   @SuppressWarnings("java:S1075") // no filesystem path
228   private static String getPathForLog(ResourceCollection rootSourceDir, Resource file) {
229     String path = PathUtil.unifySlashes(file.getCanonicalPath());
230     String rootPath = PathUtil.unifySlashes(rootSourceDir.getCanonicalPath()) + "/";
231     return StringUtils.substringAfter(path, rootPath);
232   }
233 
234   // ===== PLUGIN VERSION INFO VALIDATION =====
235 
236   /**
237    * Validates that the CONGA maven plugin version and CONGA plugin versions match or are newer than those versions used
238    * when generating the dependency artifacts.
239    * @param environment Environment
240    * @param mavenProjectClasspathUrls Classpath URLs of maven project
241    * @param pluginContextOptions Plugin context options
242    */
243   private void validateVersionInfo(Environment environment, List<URL> mavenProjectClasspathUrls, PluginContextOptions pluginContextOptions)
244       throws MojoExecutionException {
245 
246     // build combined classpath for dependencies defined in environment and maven project
247     List<URL> classpathUrls = new ArrayList<>();
248     classpathUrls.addAll(getEnvironmentClasspathUrls(environment.getDependencies(), pluginContextOptions));
249     classpathUrls.addAll(mavenProjectClasspathUrls);
250     ClassLoader environmentDependenciesClassLoader = ClassLoaderUtil.buildClassLoader(classpathUrls);
251 
252     // get version info from this project
253     Properties currentVersionInfo = VersionInfoUtil.getVersionInfoProperties(project);
254 
255     // validate current version info against dependency version infos
256     for (Properties dependencyVersionInfo : getDependencyVersionInfos(environmentDependenciesClassLoader)) {
257       validateVersionInfo(currentVersionInfo, dependencyVersionInfo);
258     }
259 
260   }
261 
262   private List<URL> getEnvironmentClasspathUrls(List<String> dependencyUrls, PluginContextOptions pluginContextOptions) {
263     return dependencyUrls.stream()
264         .map(dependencyUrl -> {
265           String resolvedDependencyUrl = ClassLoaderUtil.resolveDependencyUrl(dependencyUrl, pluginContextOptions);
266           try {
267             return pluginContextOptions.getUrlFileManager().getFileUrlsWithDependencies(resolvedDependencyUrl);
268           }
269           catch (IOException ex) {
270             throw new GeneratorException("Unable to resolve: " + resolvedDependencyUrl, ex);
271           }
272         })
273         .flatMap(List::stream)
274         .collect(Collectors.toList());
275   }
276 
277   private void validateVersionInfo(Properties currentVersionInfo, Properties dependencyVersionInfo) throws MojoExecutionException {
278     for (Object keyObject : currentVersionInfo.keySet()) {
279       String key = keyObject.toString();
280       String currentVersionString = currentVersionInfo.getProperty(key);
281       String dependencyVersionString = dependencyVersionInfo.getProperty(key);
282       if (StringUtils.isEmpty(currentVersionString) || StringUtils.isEmpty(dependencyVersionString)) {
283         continue;
284       }
285       DefaultArtifactVersion currentVersion = new DefaultArtifactVersion(currentVersionString);
286       DefaultArtifactVersion dependencyVersion = new DefaultArtifactVersion(dependencyVersionString);
287       if (currentVersion.compareTo(dependencyVersion) < 0) {
288         throw new MojoExecutionException("Newer CONGA maven plugin or plugin version required: " + key + ":" + dependencyVersion.toString());
289       }
290     }
291   }
292 
293   private List<Properties> getDependencyVersionInfos(ClassLoader classLoader) throws MojoExecutionException {
294     PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(classLoader);
295     try {
296       org.springframework.core.io.Resource[] resources = resolver.getResources(
297           "classpath*:" + GeneratorOptions.CLASSPATH_PREFIX + BuildConstants.FILE_VERSION_INFO);
298       return Arrays.stream(resources)
299           .map(this::toProperties)
300           .collect(Collectors.toList());
301     }
302     catch (IOException ex) {
303       throw new MojoExecutionException("Unable to get classpath resources: " + ex.getMessage(), ex);
304     }
305   }
306 
307   private Properties toProperties(org.springframework.core.io.Resource resource) {
308     try (InputStream is = resource.getInputStream()) {
309       Properties props = new Properties();
310       props.load(is);
311       return props;
312     }
313     catch (IOException ex) {
314       throw new IllegalArgumentException("Unable to read properties file: " + resource.toString(), ex);
315     }
316   }
317 
318 }