1   package com.atlassian.maven.plugins.amps;
2   
3   import com.atlassian.maven.plugins.amps.product.ProductHandler;
4   import com.atlassian.maven.plugins.amps.product.ProductHandlerFactory;
5   import com.atlassian.maven.plugins.amps.product.studio.StudioProductHandler;
6   import com.atlassian.maven.plugins.amps.util.ArtifactRetriever;
7   import com.atlassian.maven.plugins.amps.util.ProjectUtils;
8   import com.google.common.base.Predicate;
9   import com.google.common.collect.Iterables;
10  import com.google.common.collect.Lists;
11  
12  import org.apache.maven.artifact.factory.ArtifactFactory;
13  import org.apache.maven.artifact.repository.ArtifactRepository;
14  import org.apache.maven.artifact.resolver.ArtifactResolver;
15  import org.apache.maven.model.Resource;
16  import org.apache.maven.plugin.MojoExecutionException;
17  import org.apache.maven.plugin.MojoFailureException;
18  import org.apache.maven.project.MavenProject;
19  import org.jfrog.maven.annomojo.annotations.MojoComponent;
20  import org.jfrog.maven.annomojo.annotations.MojoParameter;
21  
22  import java.io.File;
23  import java.util.ArrayList;
24  import java.util.HashMap;
25  import java.util.Iterator;
26  import java.util.List;
27  import java.util.Map;
28  import java.util.Properties;
29  import java.util.Set;
30  
31  /**
32   * Base class for webapp mojos
33   */
34  public abstract class AbstractProductHandlerMojo extends AbstractProductHandlerAwareMojo {
35  
36      // ------ start inline product context
37  
38      private static final String DEFAULT_CONTAINER = "tomcat6x";
39      private static final String DEFAULT_SERVER = "localhost";
40      private static final String DEFAULT_PRODUCT_DATA_VERSION = "LATEST";
41      private static final String DEFAULT_PDK_VERSION = "0.4";
42      private static final String DEFAULT_WEB_CONSOLE_VERSION = "1.2.8";
43  
44      /**
45        * Default product startup timeout: three minutes
46       */
47      private static final int DEFAULT_PRODUCT_STARTUP_TIMEOUT = 1000 * 60 * 3;
48  
49      /**
50        * Default product shutdown timeout: three minutes
51        */
52      private static final int DEFAULT_PRODUCT_SHUTDOWN_TIMEOUT = 1000 * 60 * 3;
53  
54      /**
55       * Container to run in
56       */
57      @MojoParameter(expression = "${container}", defaultValue = DEFAULT_CONTAINER)
58      protected String containerId;
59  
60      /**
61       * HTTP port for the servlet containers
62       */
63      @MojoParameter(expression = "${http.port}", defaultValue = "0")
64      private int httpPort;
65  
66      /**
67       * Application context path
68       */
69      @MojoParameter(expression = "${context.path}")
70      protected String contextPath;
71  
72      /**
73       * Application server
74       */
75      @MojoParameter(expression = "${server}", defaultValue = DEFAULT_SERVER)
76      protected String server;
77  
78      /**
79       * Webapp version
80       */
81      @MojoParameter(expression = "${product.version}")
82      private String productVersion;
83  
84      /**
85       * JVM arguments to pass to cargo
86       */
87      @MojoParameter(expression = "${jvmargs}")
88      protected String jvmArgs;
89  
90      /**
91       * Product startup timeout in milliseconds
92       */
93      @MojoParameter(expression = "${product.start.timeout}")
94      private int startupTimeout;
95  
96      /**
97       * Product shutdown timeout in milliseconds
98       */
99      @MojoParameter(expression = "${product.stop.timeout}")
100     private int shutdownTimeout;
101 
102     /**
103      * System systemProperties to pass to cargo
104      *
105      * @deprecated Since 3.2, use systemPropertyVariables instead
106      */
107     @MojoParameter
108     @Deprecated
109     protected Properties systemProperties = new Properties();
110 
111     /**
112      * System Properties to pass to cargo using a more familiar syntax.
113      *
114      * @since 3.2
115      */
116     @MojoParameter
117     protected Map<String, Object> systemPropertyVariables = new HashMap<String, Object>();
118 
119 
120     /**
121      * A log4j systemProperties file
122      */
123     @MojoParameter
124     protected File log4jProperties;
125 
126     /**
127      * The test resources version
128      * @deprecated Since 3.0-beta2
129      */
130     @MojoParameter(expression = "${test.resources.version}")
131     private String testResourcesVersion;
132 
133     /**
134      * The test resources version
135      */
136     @MojoParameter(expression = "${product.data.version}", defaultValue = DEFAULT_PRODUCT_DATA_VERSION)
137     private String productDataVersion;
138 
139     /**
140      * The path to a custom test resources zip
141      */
142     @MojoParameter(expression = "${product.data.path}")
143     private String productDataPath;
144 
145     /**
146      */
147     @MojoParameter
148     private List<ProductArtifact> pluginArtifacts = new ArrayList<ProductArtifact>();
149 
150     /**
151      */
152     @MojoParameter
153     private List<ProductArtifact> libArtifacts = new ArrayList<ProductArtifact>();
154 
155     /**
156      */
157     @MojoParameter
158     private List<ProductArtifact> bundledArtifacts = new ArrayList<ProductArtifact>();
159 
160     /**
161      * SAL version
162      * @deprecated Since 3.2, use {@link #pluginArtifacts} instead
163      */
164     @MojoParameter
165     private String salVersion;
166 
167     /**
168      * Atlassian Plugin Development Kit (PDK) version
169      * @deprecated Since 3.2, use {@link #pluginArtifacts} instead
170      */
171     @MojoParameter(defaultValue = DEFAULT_PDK_VERSION)
172     private String pdkVersion;
173 
174     /**
175      * Atlassian REST module version
176      * @deprecated Since 3.2, use {@link #pluginArtifacts} instead
177      */
178     @MojoParameter
179     private String restVersion;
180 
181 
182     /**
183      * Felix OSGi web console version
184      * @deprecated Since 3.2, use {@link #pluginArtifacts} instead
185      */
186     @MojoParameter(defaultValue =  DEFAULT_WEB_CONSOLE_VERSION)
187     private String webConsoleVersion;
188 
189     // ---------------- end product context
190 
191     /**
192      * Comma-delimited list of plugin artifacts in GROUP_ID:ARTIFACT_ID:VERSION form, where version can be
193      * ommitted, defaulting to LATEST
194      */
195     @MojoParameter(expression = "${plugins}")
196     private String pluginArtifactsString;
197 
198     /**
199      * Comma-delimited list of lib artifacts in GROUP_ID:ARTIFACT_ID:VERSION form, where version can be
200      * ommitted, defaulting to LATEST
201      */
202     @MojoParameter(expression = "${lib.plugins}")
203     private String libArtifactsString;
204 
205     /**
206      * Comma-delimited list of bundled plugin artifacts in GROUP_ID:ARTIFACT_ID:VERSION form, where version can be
207      * ommitted, defaulting to LATEST
208      */
209     @MojoParameter(expression = "${bundled.plugins}")
210     private String bundledArtifactsString;
211 
212     /**
213      * The build directory
214      */
215     @MojoParameter(expression = "${project.build.directory}", required = true)
216     protected File targetDirectory;
217 
218     /**
219      * The jar name
220      */
221     @MojoParameter(expression = "${project.build.finalName}", required = true)
222     protected String finalName;
223 
224     /**
225      * If the plugin and optionally its test plugin should be installed
226      */
227     @MojoParameter (expression = "${install.plugin}", defaultValue = "true")
228     protected boolean installPlugin;
229 
230     /**
231      * The artifact resolver is used to dynamically resolve JARs that have to be in the embedded
232      * container's classpaths. Another solution would have been to statitically define them a
233      * dependencies in the plugin's POM. Resolving them in a dynamic manner is much better as only
234      * the required JARs for the defined embedded container are downloaded.
235      */
236     @MojoComponent
237     protected ArtifactResolver artifactResolver;
238 
239     /**
240      * The local Maven repository. This is used by the artifact resolver to download resolved
241      * JARs and put them in the local repository so that they won't have to be fetched again next
242      * time the plugin is executed.
243      */
244     @MojoParameter(expression = "${localRepository}")
245     protected ArtifactRepository localRepository;
246 
247 
248     /**
249      * The remote Maven repositories used by the artifact resolver to look for JARs.
250      */
251     @MojoParameter(expression = "${project.remoteArtifactRepositories}")
252     protected List repositories;
253 
254     /**
255      * The artifact factory is used to create valid Maven
256      * {@link org.apache.maven.artifact.Artifact} objects. This is used to pass Maven artifacts to
257      * the artifact resolver so that it can download the required JARs to put in the embedded
258      * container's classpaths.
259      */
260     @MojoComponent
261     protected ArtifactFactory artifactFactory;
262 
263     /**
264      * A list of product-specific configurations
265      */
266     @MojoParameter
267     protected List<Product> products = new ArrayList<Product>();
268 
269     /**
270      * File the container logging output will be sent to.
271      */
272     @MojoParameter
273     private String output;
274 
275 
276     protected Product createDefaultProductContext() throws MojoExecutionException
277     {
278         Product ctx = new Product();
279         ctx.setId(getProductId());
280         ctx.setContainerId(containerId);
281         ctx.setServer(server);
282         ctx.setContextPath(contextPath);
283         ctx.setJvmArgs(jvmArgs);
284         ctx.setStartupTimeout(startupTimeout);
285         ctx.setShutdownTimeout(shutdownTimeout);
286 
287         // If they aren't defined, define those system properties. They will override the product
288         // handler's properties.
289         setDefaultSystemProperty(systemPropertyVariables, "atlassian.dev.mode", "true");
290         setDefaultSystemProperty(systemPropertyVariables, "java.awt.headless", "true");
291         setDefaultSystemProperty(systemPropertyVariables, "plugin.resource.directories", buildResourcesList());
292         setDefaultSystemProperty(systemPropertyVariables, "plugin.root.directories", buildRootProperty());
293 
294         ctx.setSystemPropertyVariables(systemPropertyVariables);
295         ctx.setBundledArtifacts(bundledArtifacts);
296         ctx.setLibArtifacts(libArtifacts);
297         ctx.setPluginArtifacts(pluginArtifacts);
298         ctx.setLog4jProperties(log4jProperties);
299         ctx.setHttpPort(httpPort);
300 
301         ctx.setVersion(productVersion);
302         ctx.setDataVersion(productDataVersion);
303         ctx.setDataPath(productDataPath);
304 
305         // continue to have these work for now
306         ctx.setRestVersion(restVersion);
307         ctx.setSalVersion(salVersion);
308         ctx.setPdkVersion(pdkVersion);
309         ctx.setWebConsoleVersion(webConsoleVersion);
310 
311         ctx.setHttpPort(httpPort);
312         return ctx;
313     }
314 
315     /**
316      * @return a comma-separated list of resource directories.  If a test plugin is detected, the
317      * test resources directories are included as well.
318      */
319     private String buildResourcesList()
320     {
321         // collect all resource directories and make them available for
322         // on-the-fly reloading
323         StringBuilder resourceProp = new StringBuilder();
324         MavenProject mavenProject = getMavenContext().getProject();
325         @SuppressWarnings("unchecked") List<Resource> resList = mavenProject.getResources();
326         for (int i = 0; i < resList.size(); i++) {
327             resourceProp.append(resList.get(i).getDirectory());
328             if (i + 1 != resList.size()) {
329                 resourceProp.append(",");
330             }
331         }
332 
333         if (ProjectUtils.shouldDeployTestJar(getMavenContext()))
334         {
335             @SuppressWarnings("unchecked") List<Resource> testResList = mavenProject.getTestResources();
336             for (int i = 0; i < testResList.size(); i++) {
337                 if (i == 0 && resourceProp.length() > 0)
338                 {
339                     resourceProp.append(",");
340                 }
341                 resourceProp.append(testResList.get(i).getDirectory());
342                 if (i + 1 != testResList.size()) {
343                     resourceProp.append(",");
344                 }
345             }
346         }
347         return resourceProp.toString();
348     }
349 
350     /**
351      * @return the path of the project root, for the <tt>plugin.root.directories</tt> system property.
352      *
353      * @since 3.6
354      */
355     private String buildRootProperty()
356     {
357         MavenProject mavenProject = getMavenContext().getProject();
358         return mavenProject.getBasedir().getPath();
359     }
360 
361     private static void setDefaultSystemProperty(final Map<String,Object> props, final String key, final String value)
362     {
363         if (!props.containsKey(key))
364         {
365             props.put(key, System.getProperty(key, value));
366         }
367     }
368 
369     /**
370      * Set the default values for the product
371      * @param product the product
372      * @param handler the product handler associated to the product
373      */
374     protected void setDefaultValues(Product product, ProductHandler handler)
375     {
376         product.setInstanceId(getProductInstanceId(product));
377 
378         // If it's a Studio product, some defaults are different (ex: context path for Confluence is /wiki)
379         StudioProductHandler.setDefaultValues(product);
380 
381         //Apply the common default values
382         String dversion = System.getProperty("product.data.version", product.getDataVersion());
383         String pversion = System.getProperty("product.version", product.getVersion());
384         String dpath = System.getProperty("product.data.path", product.getDataPath());
385 
386         product.setDataPath(dpath);
387         product.setDataVersion(dversion);
388         product.setVersion(pversion);
389         product.setArtifactRetriever(new ArtifactRetriever(artifactResolver, artifactFactory, localRepository, repositories));
390 
391         if (product.getContainerId() == null)
392         {
393             product.setContainerId(DEFAULT_CONTAINER);
394         }
395 
396         if (product.getServer() == null)
397         {
398             product.setServer(DEFAULT_SERVER);
399         }
400 
401         if (product.getDataVersion() == null)
402         {
403             product.setDataVersion(DEFAULT_PRODUCT_DATA_VERSION);
404         }
405 
406         if (product.getPdkVersion() == null)
407         {
408             product.setPdkVersion(DEFAULT_PDK_VERSION);
409         }
410 
411         if (product.getWebConsoleVersion() == null)
412         {
413             product.setWebConsoleVersion(DEFAULT_WEB_CONSOLE_VERSION);
414         }
415 
416         if (product.getOutput() == null)
417         {
418             product.setOutput(output);
419         }
420 
421         if (product.getStartupTimeout() <= 0)
422         {
423             product.setStartupTimeout(DEFAULT_PRODUCT_STARTUP_TIMEOUT);
424         }
425 
426         if (product.getShutdownTimeout() <= 0)
427         {
428             product.setShutdownTimeout(DEFAULT_PRODUCT_SHUTDOWN_TIMEOUT);
429         }
430 
431         if (product.getHttpPort() == 0)
432         {
433             product.setHttpPort(handler.getDefaultHttpPort());
434         }
435 
436         if (product.getVersion() == null)
437         {
438             product.setVersion("RELEASE");
439         }
440 
441         if (product.getContextPath() == null)
442         {
443             product.setContextPath("/" + handler.getId());
444         }
445     }
446 
447     private List<ProductArtifact> stringToArtifactList(String val, List<ProductArtifact> artifacts)
448     {
449         if (val == null || val.trim().length() == 0)
450         {
451             return artifacts;
452         }
453 
454         for (String ptn : val.split(","))
455         {
456             String[] items = ptn.split(":");
457             if (items.length < 2 || items.length > 3)
458             {
459                 throw new IllegalArgumentException("Invalid artifact pattern: " + ptn);
460             }
461             String groupId = items[0];
462             String artifactId = items[1];
463             String version = (items.length == 3 ? items[2] : "LATEST");
464             artifacts.add(new ProductArtifact(groupId, artifactId, version));
465         }
466         return artifacts;
467     }
468 
469     public final void execute() throws MojoExecutionException, MojoFailureException
470     {
471         stringToArtifactList(pluginArtifactsString, pluginArtifacts);
472         stringToArtifactList(libArtifactsString, libArtifacts);
473         stringToArtifactList(bundledArtifactsString, bundledArtifacts);
474         systemPropertyVariables.putAll((Map) systemProperties);
475 
476         detectDeprecatedVersionOverrides();
477 
478         doExecute();
479     }
480 
481     private void detectDeprecatedVersionOverrides()
482     {
483         Properties props = getMavenContext().getProject().getProperties();
484         for (String deprecatedProperty : new String[] {"sal.version", "rest.version", "web.console.version", "pdk.version"})
485         {
486             if (props.containsKey(deprecatedProperty))
487             {
488                 getLog().warn("The property '" + deprecatedProperty + "' is no longer usable to override the related bundled plugin." +
489                         "  Use <pluginArtifacts> or <libArtifacts> to explicitly override bundled plugins and libraries, respectively.");
490             }
491         }
492     }
493 
494     /**
495      * Returns the Product objects that are defined in our maven-amps-plugins object:
496      * <ul>
497      * <li>Reads the {@literal <products>} tag</li>
498      * <li>Defaults the values</li>
499      * </ul>
500      * So the method looks short but it's quite central in the initialisation of products.
501      */
502     protected Map<String, Product> getProductContexts(MavenGoals goals) throws MojoExecutionException
503     {
504         Map<String, Product> productMap = new HashMap<String, Product>();
505 
506         // Products in the <products> tag inherit from the upper settings, e.g. when there's a <httpPort> tag for for all products
507         makeProductsInheritDefaultConfiguration(products, productMap);
508 
509         for (Product ctx : productMap.values())
510         {
511             ProductHandler handler = ProductHandlerFactory.create(ctx.getId(), getMavenContext(), goals);
512             setDefaultValues(ctx, handler);
513         }
514         return productMap;
515     }
516 
517     /**
518      * Puts the list of {@literal <products>} in productMap:
519      * <ul>
520      * <li>The {@literal <product>} from the maven-amps-plugin configuration (if missing, RefApp is used)</li>
521      * <li>The {@literal <products>} from the maven-amps-plugin configuration</li>
522      * </ul>
523      */
524     void makeProductsInheritDefaultConfiguration(List<Product> products, Map<String, Product> productMap) throws MojoExecutionException
525     {
526         productMap.put(getProductId(), createDefaultProductContext());
527         if (!products.isEmpty())
528         {
529             Product defaultProduct = createDefaultProductContext();
530             for (Product product : products)
531             {
532                 Product processedProduct = product.merge(defaultProduct);
533                 String instanceId = getProductInstanceId(processedProduct);
534                 productMap.put(instanceId, processedProduct);
535             }
536         }
537     }
538 
539     private String getProductInstanceId(Product processedProduct)
540     {
541         return processedProduct.getInstanceId() == null ? processedProduct.getId() : processedProduct.getInstanceId();
542     }
543 
544 
545     private Product createProductContext(String productNickname, String instanceId, ProductHandler handler) throws MojoExecutionException
546     {
547         getLog().info(String.format("Studio (instanceId=%s): No product with name %s is defined in the pom. Using a default product.", instanceId, productNickname));
548         Product product;
549         product = createDefaultProductContext();
550         product.setId(productNickname);
551         product.setInstanceId(instanceId);
552         setDefaultValues(product, handler);
553         return product;
554     }
555 
556 
557     private Iterator<ProductExecution> getStudioExecutions(final List<ProductExecution> productExecutions)
558     {
559         return Iterables.filter(productExecutions, new Predicate<ProductExecution>(){
560 
561             @Override
562             public boolean apply(ProductExecution input)
563             {
564                 return input.getProductHandler() instanceof StudioProductHandler;
565             }}).iterator();
566     }
567 
568 
569     /**
570      * If there is any Studio instance, returns a list with all products requested by this instance.
571      *
572      * Configures both the Studio instance and its dependent products.
573      *
574      * @param productExecutions the current list of products to run
575      * @param goals
576      * @return the complete list of products to run
577      * @throws MojoExecutionException
578      */
579     protected List<ProductExecution> includeStudioDependentProducts(final List<ProductExecution> productExecutions, final MavenGoals goals)
580             throws MojoExecutionException
581     {
582         // If one of the products is Studio, ask him/her which other products he/she wants to run
583         Iterator<ProductExecution> studioExecutions = getStudioExecutions(productExecutions);
584         if (!studioExecutions.hasNext())
585         {
586             return productExecutions;
587         }
588 
589         // We have studio execution(s), so we need to add all products requested by Studio
590         List<ProductExecution> productExecutionsIncludingStudio = Lists.newArrayList(productExecutions);
591         while (studioExecutions.hasNext())
592         {
593             ProductExecution studioExecution = studioExecutions.next();
594             Product studioProduct = studioExecution.getProduct();
595             StudioProductHandler studioProductHandler = (StudioProductHandler) studioExecution.getProductHandler();
596 
597             // Ask the Studio Product Handler the list of required products
598             final List<String> dependantProductIds = studioProductHandler.getDependantInstances(studioProduct);
599 
600             // Fetch the products
601             List<ProductExecution> dependantProducts = Lists.newArrayList();
602             Map<String, Product> allContexts = getProductContexts(goals);
603             for (String instanceId : dependantProductIds)
604             {
605                 Product product = allContexts.get(instanceId);
606                 ProductHandler handler;
607                 if (product == null)
608                 {
609                     handler = createProductHandler(instanceId);
610                     product = createProductContext(instanceId, instanceId, handler);
611                 }
612                 else
613                 {
614                     handler = createProductHandler(product.getId());
615                 }
616 
617                 dependantProducts.add(new ProductExecution(product, handler));
618             }
619 
620             // Submit those products to StudioProductHanlder for configuration
621             studioProductHandler.configure(studioProduct, dependantProducts);
622 
623             // If the user passes some system properties, we don't run some products
624             // We'll keep them configured and available in StudioProperties
625             Set<String> exclusions = studioProductHandler.getExcludedInstances(studioProduct);
626             if (exclusions != null)
627             {
628                 Iterator<ProductExecution> iterator = dependantProducts.iterator();
629                 while (iterator.hasNext())
630                 {
631                     String executedInstance = iterator.next().getProduct().getInstanceId();
632                     if (exclusions.contains(executedInstance))
633                     {
634                         iterator.remove();
635                     }
636                 }
637             }
638 
639             // Add everyone at the end of the list of products to execute. We don't check for duplicates, users shouldn't add studio products
640             // to test groups, especially if they already have a Studio.
641             productExecutionsIncludingStudio.addAll(dependantProducts);
642         }
643 
644         return productExecutionsIncludingStudio;
645     }
646 
647     protected abstract void doExecute() throws MojoExecutionException, MojoFailureException;
648 }