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