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  import com.google.common.collect.Maps;
12  import org.apache.commons.lang.StringUtils;
13  import org.apache.maven.artifact.factory.ArtifactFactory;
14  import org.apache.maven.artifact.repository.ArtifactRepository;
15  import org.apache.maven.artifact.resolver.ArtifactResolver;
16  import org.apache.maven.model.Resource;
17  import org.apache.maven.plugin.MojoExecutionException;
18  import org.apache.maven.plugin.MojoFailureException;
19  import org.apache.maven.project.MavenProject;
20  import org.jfrog.maven.annomojo.annotations.MojoComponent;
21  import org.jfrog.maven.annomojo.annotations.MojoParameter;
22  
23  import java.io.File;
24  import java.io.IOException;
25  import java.net.HttpURLConnection;
26  import java.net.InetAddress;
27  import java.net.URL;
28  import java.net.UnknownHostException;
29  import java.util.ArrayList;
30  import java.util.HashMap;
31  import java.util.List;
32  import java.util.Map;
33  import java.util.Properties;
34  import java.util.concurrent.ExecutionException;
35  import java.util.concurrent.ExecutorService;
36  import java.util.concurrent.Executors;
37  import java.util.concurrent.Future;
38  import java.util.concurrent.TimeUnit;
39  import java.util.concurrent.TimeoutException;
40  
41  import static com.atlassian.maven.plugins.amps.product.ProductHandlerFactory.STUDIO;
42  
43  /**
44   * Base class for webapp mojos
45   */
46  public abstract class AbstractProductHandlerMojo extends AbstractProductHandlerAwareMojo {
47  
48      // ------ start inline product context
49  
50      protected static final String DEFAULT_CONTAINER = "tomcat6x";
51      private static final String DEFAULT_SERVER;
52      private static final String DEFAULT_PRODUCT_DATA_VERSION = "LATEST";
53      private static final String DEFAULT_PDK_VERSION = "0.4";
54      private static final String DEFAULT_WEB_CONSOLE_VERSION = "1.2.8";
55      private static final String DEFAULT_FASTDEV_VERSION = "1.9.1";
56      private static final String DEFAULT_DEV_TOOLBOX_VERSION = "1.1";
57      private static final String DEFAULT_PDE_VERSION = "1.2";
58  
59      /**
60        * Default product startup timeout: three minutes
61       */
62      private static final int DEFAULT_PRODUCT_STARTUP_TIMEOUT = 1000 * 60 * 3;
63  
64      /**
65        * Default product shutdown timeout: three minutes
66        */
67      private static final int DEFAULT_PRODUCT_SHUTDOWN_TIMEOUT = 1000 * 60 * 3;
68  
69      static
70      {
71          String localHostName = null;
72          try
73          {
74              localHostName = InetAddress.getLocalHost().getHostName();
75          }
76          catch (UnknownHostException e)
77          {
78              localHostName = "localhost";
79          }
80          DEFAULT_SERVER = localHostName;
81      }
82  
83      /**
84       * Container to run in
85       */
86      @MojoParameter(expression = "${container}", defaultValue = DEFAULT_CONTAINER)
87      protected String containerId;
88  
89      /**
90       * HTTP port for the servlet containers
91       */
92      @MojoParameter(expression = "${http.port}", defaultValue = "0")
93      private int httpPort;
94  
95      /**
96       * Application context path
97       */
98      @MojoParameter(expression = "${context.path}")
99      protected String contextPath;
100 
101     /**
102      * Application server
103      */
104     @MojoParameter(expression = "${server}")
105     protected String server;
106 
107     /**
108      * Webapp version
109      */
110     @MojoParameter(expression = "${product.version}")
111     private String productVersion;
112 
113     /**
114      * JVM arguments to pass to cargo
115      */
116     @MojoParameter(expression = "${jvmargs}")
117     protected String jvmArgs;
118 
119     /**
120      * Product startup timeout in milliseconds
121      */
122     @MojoParameter(expression = "${product.start.timeout}")
123     private int startupTimeout;
124 
125     /**
126      * Product shutdown timeout in milliseconds
127      */
128     @MojoParameter(expression = "${product.stop.timeout}")
129     private int shutdownTimeout;
130 
131     /**
132      * System systemProperties to pass to cargo
133      *
134      * @deprecated Since 3.2, use systemPropertyVariables instead
135      */
136     @MojoParameter
137     @Deprecated
138     protected Properties systemProperties = new Properties();
139 
140     /**
141      * System Properties to pass to cargo using a more familiar syntax.
142      *
143      * @since 3.2
144      */
145     @MojoParameter
146     protected Map<String, Object> systemPropertyVariables = new HashMap<String, Object>();
147 
148 
149     /**
150      * A log4j systemProperties file
151      */
152     @MojoParameter
153     protected File log4jProperties;
154 
155     /**
156      * The test resources version
157      * @deprecated Since 3.0-beta2
158      */
159     @Deprecated
160     @MojoParameter(expression = "${test.resources.version}")
161     private String testResourcesVersion;
162 
163     /**
164      * The test resources version
165      */
166     @MojoParameter(expression = "${product.data.version}", defaultValue = DEFAULT_PRODUCT_DATA_VERSION)
167     private String productDataVersion;
168 
169     /**
170      * The path to a custom test resources zip
171      */
172     @MojoParameter(expression = "${product.data.path}")
173     private String productDataPath;
174 
175     /**
176      * If FastDev should be enabled
177      */
178     @MojoParameter(expression = "${fastdev.enable}", defaultValue = "true")
179     protected boolean enableFastdev;
180 
181     /**
182      * The version of FastDev to bundle
183      */
184     @MojoParameter(expression = "${fastdev.version}", defaultValue = DEFAULT_FASTDEV_VERSION)
185     protected String fastdevVersion;
186 
187     /**
188      * If DevToolbox should be enabled
189      */
190     @MojoParameter(expression = "${devtoolbox.enable}", defaultValue = "true")
191     protected boolean enableDevToolbox;
192 
193     /**
194      * The version of DevToolbox to bundle
195      */
196     @MojoParameter(expression = "${devtoolbox.version}", defaultValue = DEFAULT_DEV_TOOLBOX_VERSION)
197     protected String devToolboxVersion;
198 
199     /**
200      * If PDE should be enabled
201      */
202     @MojoParameter(expression = "${pde.enable}", defaultValue = "true")
203     protected boolean enablePde;
204 
205     /**
206      * The version of the PDE to bundle
207      */
208     @MojoParameter(expression = "${pde.version}", defaultValue = DEFAULT_PDE_VERSION)
209     protected String pdeVersion;
210     
211     @MojoParameter
212     private List<ProductArtifact> pluginArtifacts = new ArrayList<ProductArtifact>();
213 
214     /**
215      */
216     @MojoParameter
217     private List<ProductArtifact> libArtifacts = new ArrayList<ProductArtifact>();
218 
219     /**
220      */
221     @MojoParameter
222     private List<ProductArtifact> bundledArtifacts = new ArrayList<ProductArtifact>();
223 
224     /**
225      * SAL version
226      * @deprecated Since 3.2, use {@link #pluginArtifacts} instead
227      */
228     @Deprecated
229     @MojoParameter
230     private String salVersion;
231 
232     /**
233      * Atlassian Plugin Development Kit (PDK) version
234      * @deprecated Since 3.2, use {@link #pluginArtifacts} instead
235      */
236     @Deprecated
237     @MojoParameter(defaultValue = DEFAULT_PDK_VERSION)
238     private String pdkVersion;
239 
240     /**
241      * Atlassian REST module version
242      * @deprecated Since 3.2, use {@link #pluginArtifacts} instead
243      */
244     @Deprecated
245     @MojoParameter
246     private String restVersion;
247 
248 
249     /**
250      * Felix OSGi web console version
251      * @deprecated Since 3.2, use {@link #pluginArtifacts} instead
252      */
253     @Deprecated
254     @MojoParameter(defaultValue =  DEFAULT_WEB_CONSOLE_VERSION)
255     private String webConsoleVersion;
256 
257     // ---------------- end product context
258 
259     /**
260      * Comma-delimited list of plugin artifacts in GROUP_ID:ARTIFACT_ID:VERSION form, where version can be
261      * ommitted, defaulting to LATEST
262      */
263     @MojoParameter(expression = "${plugins}")
264     private String pluginArtifactsString;
265 
266     /**
267      * Comma-delimited list of lib artifacts in GROUP_ID:ARTIFACT_ID:VERSION form, where version can be
268      * ommitted, defaulting to LATEST
269      */
270     @MojoParameter(expression = "${lib.plugins}")
271     private String libArtifactsString;
272 
273     /**
274      * Comma-delimited list of bundled plugin artifacts in GROUP_ID:ARTIFACT_ID:VERSION form, where version can be
275      * ommitted, defaulting to LATEST
276      */
277     @MojoParameter(expression = "${bundled.plugins}")
278     private String bundledArtifactsString;
279 
280     /**
281      * The build directory
282      */
283     @MojoParameter(expression = "${project.build.directory}", required = true)
284     protected File targetDirectory;
285 
286     /**
287      * The jar name
288      */
289     @MojoParameter(expression = "${project.build.finalName}", required = true)
290     protected String finalName;
291 
292     /**
293      * If the plugin and optionally its test plugin should be installed
294      */
295     @MojoParameter (expression = "${install.plugin}", defaultValue = "true")
296     protected boolean installPlugin;
297 
298     /**
299      * The artifact resolver is used to dynamically resolve JARs that have to be in the embedded
300      * container's classpaths. Another solution would have been to statitically define them a
301      * dependencies in the plugin's POM. Resolving them in a dynamic manner is much better as only
302      * the required JARs for the defined embedded container are downloaded.
303      */
304     @MojoComponent
305     protected ArtifactResolver artifactResolver;
306 
307     /**
308      * The local Maven repository. This is used by the artifact resolver to download resolved
309      * JARs and put them in the local repository so that they won't have to be fetched again next
310      * time the plugin is executed.
311      */
312     @MojoParameter(expression = "${localRepository}")
313     protected ArtifactRepository localRepository;
314 
315 
316     /**
317      * The remote Maven repositories used by the artifact resolver to look for JARs.
318      */
319     @MojoParameter(expression = "${project.remoteArtifactRepositories}")
320     protected List repositories;
321 
322     /**
323      * The artifact factory is used to create valid Maven
324      * {@link org.apache.maven.artifact.Artifact} objects. This is used to pass Maven artifacts to
325      * the artifact resolver so that it can download the required JARs to put in the embedded
326      * container's classpaths.
327      */
328     @MojoComponent
329     protected ArtifactFactory artifactFactory;
330 
331     /**
332      * A list of product-specific configurations (as literally provided in the pom.xml)
333      */
334     @MojoParameter
335     protected List<Product> products = new ArrayList<Product>();
336     
337     /**
338      * A map of {instanceId -> Product}, initialized by {@link #createProductContexts()}.
339      * Cannot be set by the user. 
340      */
341     private Map<String, Product> productMap;
342 
343     /**
344      * File the container logging output will be sent to.
345      */
346     @MojoParameter
347     private String output;
348 
349     /**
350      * Start the products in parallel (TestGroups and Studio).
351      */
352     @MojoParameter (expression = "${parallel}", defaultValue = "false")
353     protected boolean parallel;
354 
355 
356     protected Product createDefaultProductContext() throws MojoExecutionException
357     {
358         Product ctx = new Product();
359         ctx.setId(getProductId());
360         ctx.setContainerId(containerId);
361         ctx.setServer(server);
362         ctx.setContextPath(contextPath);
363         ctx.setJvmArgs(jvmArgs);
364         ctx.setStartupTimeout(startupTimeout);
365         ctx.setShutdownTimeout(shutdownTimeout);
366 
367         // If they aren't defined, define those system properties. They will override the product
368         // handler's properties.
369         Map<String, Object> properties = new HashMap<String, Object>(systemPropertyVariables);
370         properties.put("atlassian.sdk.version", getPluginInformation().getVersion());
371         setDefaultSystemProperty(properties, "atlassian.dev.mode", "true");
372         setDefaultSystemProperty(properties, "java.awt.headless", "true");
373         setDefaultSystemProperty(properties, "plugin.resource.directories", buildResourcesList());
374         setDefaultSystemProperty(properties, "plugin.root.directories", buildRootProperty());
375 
376         ctx.setSystemPropertyVariables(properties);
377         ctx.setBundledArtifacts(bundledArtifacts);
378         ctx.setLibArtifacts(libArtifacts);
379         ctx.setPluginArtifacts(pluginArtifacts);
380         ctx.setLog4jProperties(log4jProperties);
381         ctx.setHttpPort(httpPort);
382 
383         ctx.setVersion(productVersion);
384         ctx.setDataVersion(productDataVersion);
385         ctx.setDataPath(productDataPath);
386 
387         // continue to have these work for now
388         ctx.setRestVersion(restVersion);
389         ctx.setSalVersion(salVersion);
390         ctx.setPdkVersion(pdkVersion);
391         ctx.setWebConsoleVersion(webConsoleVersion);
392 
393         ctx.setEnableFastdev(enableFastdev);
394         ctx.setFastdevVersion(fastdevVersion);
395 
396         ctx.setEnableDevToolbox(enableDevToolbox);
397         ctx.setDevToolboxVersion(devToolboxVersion);
398 
399         ctx.setEnablePde(enablePde);
400         ctx.setPdeVersion(pdeVersion);
401 
402         ctx.setHttpPort(httpPort);
403         return ctx;
404     }
405 
406     /**
407      * @return a comma-separated list of resource directories.  If a test plugin is detected, the
408      * test resources directories are included as well.
409      */
410     private String buildResourcesList()
411     {
412         // collect all resource directories and make them available for
413         // on-the-fly reloading
414         StringBuilder resourceProp = new StringBuilder();
415         MavenProject mavenProject = getMavenContext().getProject();
416         @SuppressWarnings("unchecked") List<Resource> resList = mavenProject.getResources();
417         for (int i = 0; i < resList.size(); i++) {
418             resourceProp.append(resList.get(i).getDirectory());
419             if (i + 1 != resList.size()) {
420                 resourceProp.append(",");
421             }
422         }
423 
424         if (ProjectUtils.shouldDeployTestJar(getMavenContext()))
425         {
426             @SuppressWarnings("unchecked") List<Resource> testResList = mavenProject.getTestResources();
427             for (int i = 0; i < testResList.size(); i++) {
428                 if (i == 0 && resourceProp.length() > 0)
429                 {
430                     resourceProp.append(",");
431                 }
432                 resourceProp.append(testResList.get(i).getDirectory());
433                 if (i + 1 != testResList.size()) {
434                     resourceProp.append(",");
435                 }
436             }
437         }
438         return resourceProp.toString();
439     }
440 
441     /**
442      * @return the path of the project root, for the <tt>plugin.root.directories</tt> system property.
443      *
444      * @since 3.6
445      */
446     private String buildRootProperty()
447     {
448         MavenProject mavenProject = getMavenContext().getProject();
449         return mavenProject.getBasedir().getPath();
450     }
451 
452     private static void setDefaultSystemProperty(final Map<String,Object> props, final String key, final String value)
453     {
454         if (!props.containsKey(key))
455         {
456             props.put(key, System.getProperty(key, value));
457         }
458     }
459 
460     /**
461      * Set the default values for the product
462      * @param product the product
463      * @param handler the product handler associated to the product
464      */
465     protected void setDefaultValues(Product product, ProductHandler handler)
466     {
467         product.setInstanceId(getProductInstanceId(product));
468 
469         //Apply the common default values
470         String dversion = System.getProperty("product.data.version", product.getDataVersion());
471         String pversion = System.getProperty("product.version", product.getVersion());
472         String dpath = System.getProperty("product.data.path", product.getDataPath());
473 
474         // If it's a Studio product, some defaults are different (ex: context path for Confluence is /wiki)
475         if (!StudioProductHandler.setDefaultValues(getMavenContext(), product))
476         {
477             // hacky workaround for AMPS-738:  avoid applying the regular product defaults to a Studio sub-product;
478             // however, do apply them to the main Studio product if and only if we're explicitly running Studio (so
479             // we'll get the right result for command-line options like "-Dproduct=studio -Dproduct.version=108.3").
480             if (!STUDIO.equals(product.getId()) || STUDIO.equals(System.getProperty("product")))
481             {
482                 product.setVersion(pversion);
483 	            product.setDataVersion(dversion);
484                 product.setDataPath(dpath);
485             }
486         }
487 
488         product.setArtifactRetriever(new ArtifactRetriever(artifactResolver, artifactFactory, localRepository, repositories));
489 
490         if (product.getContainerId() == null)
491         {
492             product.setContainerId(DEFAULT_CONTAINER);
493         }
494 
495         if (product.getServer() == null)
496         {
497             product.setServer(DEFAULT_SERVER);
498         }
499 
500         if (product.getDataVersion() == null)
501         {
502             product.setDataVersion(DEFAULT_PRODUCT_DATA_VERSION);
503         }
504 
505         if (product.getPdkVersion() == null)
506         {
507             product.setPdkVersion(DEFAULT_PDK_VERSION);
508         }
509 
510         if (product.getWebConsoleVersion() == null)
511         {
512             product.setWebConsoleVersion(DEFAULT_WEB_CONSOLE_VERSION);
513         }
514 
515         if (product.isEnableFastdev() == null)
516         {
517             product.setEnableFastdev(true);
518         }
519 
520         if (product.getFastdevVersion() == null)
521         {
522             product.setFastdevVersion(DEFAULT_FASTDEV_VERSION);
523         }
524 
525         if (product.isEnableDevToolbox() == null)
526         {
527             product.setEnableDevToolbox(true);
528         }
529 
530         if (product.getDevToolboxVersion() == null)
531         {
532             product.setDevToolboxVersion(DEFAULT_DEV_TOOLBOX_VERSION);
533         }
534 
535         if (product.getPdeVersion() == null)
536         {
537             product.setPdeVersion(DEFAULT_PDE_VERSION);
538         }
539         
540         if (product.getOutput() == null)
541         {
542             product.setOutput(output);
543         }
544 
545         if (product.getStartupTimeout() <= 0)
546         {
547             product.setStartupTimeout(DEFAULT_PRODUCT_STARTUP_TIMEOUT);
548         }
549 
550         if (product.getShutdownTimeout() <= 0)
551         {
552             product.setShutdownTimeout(DEFAULT_PRODUCT_SHUTDOWN_TIMEOUT);
553         }
554 
555         if (product.getHttpPort() == 0)
556         {
557             product.setHttpPort(handler.getDefaultHttpPort());
558         }
559 
560         if (product.getVersion() == null)
561         {
562             product.setVersion("RELEASE");
563         }
564 
565         if (product.getContextPath() == null)
566         {
567             product.setContextPath("/" + handler.getId());
568         }
569     }
570 
571     private List<ProductArtifact> stringToArtifactList(String val, List<ProductArtifact> artifacts)
572     {
573         if (val == null || val.trim().length() == 0)
574         {
575             return artifacts;
576         }
577 
578         for (String ptn : val.split(","))
579         {
580             String[] items = ptn.split(":");
581             if (items.length < 2 || items.length > 3)
582             {
583                 throw new IllegalArgumentException("Invalid artifact pattern: " + ptn);
584             }
585             String groupId = items[0];
586             String artifactId = items[1];
587             String version = (items.length == 3 ? items[2] : "LATEST");
588             artifacts.add(new ProductArtifact(groupId, artifactId, version));
589         }
590         return artifacts;
591     }
592 
593     @Override
594     public final void execute() throws MojoExecutionException, MojoFailureException
595     {
596         stringToArtifactList(pluginArtifactsString, pluginArtifacts);
597         stringToArtifactList(libArtifactsString, libArtifacts);
598         stringToArtifactList(bundledArtifactsString, bundledArtifacts);
599         systemPropertyVariables.putAll((Map) systemProperties);
600 
601         detectDeprecatedVersionOverrides();
602         
603         doExecute();
604     }
605 
606     private void detectDeprecatedVersionOverrides()
607     {
608         Properties props = getMavenContext().getProject().getProperties();
609         for (String deprecatedProperty : new String[] {"sal.version", "rest.version", "web.console.version", "pdk.version"})
610         {
611             if (props.containsKey(deprecatedProperty))
612             {
613                 getLog().warn("The property '" + deprecatedProperty + "' is no longer usable to override the related bundled plugin." +
614                         "  Use <pluginArtifacts> or <libArtifacts> to explicitly override bundled plugins and libraries, respectively.");
615             }
616         }
617     }
618 
619     /**
620      * Builds the map {instanceId -> Product bean}, based on: <ul>
621      * <li>the {@literal <products>} tag</li>
622      * <li>the configuration values inherited from the {@literal <configuration>} tag
623      * </ul>
624      * @throws MojoExecutionException
625      */
626     Map<String, Product> createProductContexts() throws MojoExecutionException
627     {
628         Map<String, Product> productMap = Maps.newHashMap();
629         MavenContext mavenContext = getMavenContext();
630         MavenGoals goals = getMavenGoals();
631 
632         // Products in the <products> tag inherit from the upper settings, e.g. when there's a <httpPort> tag for all products
633         makeProductsInheritDefaultConfiguration(products, productMap);
634 
635         for (Product ctx : Lists.newArrayList(productMap.values()))
636         {
637             ProductHandler handler = ProductHandlerFactory.create(ctx.getId(), mavenContext, goals);
638             setDefaultValues(ctx, handler);
639 
640             // If it's a Studio product, check dependent instance are present
641             for (String instanceId : StudioProductHandler.getDependantInstances(ctx))
642             {
643                 if (!productMap.containsKey(instanceId))
644                 {
645                     ProductHandler dependantHandler = createProductHandler(instanceId);
646                     productMap.put(instanceId, createProductContext(instanceId, instanceId, dependantHandler));
647                 }
648             }
649         }
650 
651         // Submit the Studio products for configuration
652         StudioProductHandler studioProductHandler = (StudioProductHandler) ProductHandlerFactory.create(ProductHandlerFactory.STUDIO, mavenContext, goals);
653         studioProductHandler.configureStudioProducts(productMap);
654         
655         return productMap;
656     }
657 
658     /**
659      * Returns the map { instanceId -> Product } with initialized values.
660      */
661     protected Map<String, Product> getProductContexts() throws MojoExecutionException
662     {
663         if (productMap == null)
664         {
665             productMap = createProductContexts();
666         }
667         return productMap;
668     }
669 
670     /**
671      * Puts the list of {@literal <products>} in productMap:
672      * <ul>
673      * <li>The {@literal <product>} from the maven-amps-plugin configuration (if missing, RefApp is used)</li>
674      * <li>The {@literal <products>} from the maven-amps-plugin configuration</li>
675      * </ul>
676      */
677     void makeProductsInheritDefaultConfiguration(List<Product> products, Map<String, Product> productMap) throws MojoExecutionException
678     {
679         Product defaultProduct = createDefaultProductContext();
680         productMap.put(getProductId(), defaultProduct);
681         if (!products.isEmpty())
682         {
683             for (Product product : products)
684             {
685                 Product processedProduct = product.merge(defaultProduct);
686                 if (ProductHandlerFactory.STUDIO_CROWD.equals(processedProduct.getId()))
687                 {
688                     // This is a temporary fix for StudioCrowd - it requires atlassian.dev.mode=false - see AMPS-556
689                     processedProduct.getSystemPropertyVariables().put("atlassian.dev.mode", "false");
690                 }
691                 String instanceId = getProductInstanceId(processedProduct);
692                 productMap.put(instanceId, processedProduct);
693             }
694         }
695     }
696 
697     private String getProductInstanceId(Product processedProduct)
698     {
699         return processedProduct.getInstanceId() == null ? processedProduct.getId() : processedProduct.getInstanceId();
700     }
701 
702 
703     private Product createProductContext(String productNickname, String instanceId, ProductHandler handler) throws MojoExecutionException
704     {
705         getLog().info(String.format("Studio (instanceId=%s): No product with name %s is defined in the pom. Using a default product.", instanceId, productNickname));
706         Product product;
707         product = createDefaultProductContext();
708         product.setId(productNickname);
709         product.setInstanceId(instanceId);
710         setDefaultValues(product, handler);
711         if (ProductHandlerFactory.STUDIO_CROWD.equals(product.getId()))
712         {
713             // This is a temporary fix for StudioCrowd - it requires atlassian.dev.mode=false - see AMPS-556
714             product.getSystemPropertyVariables().put("atlassian.dev.mode", "false");
715         }
716         return product;
717     }
718 
719     /**
720      * Attempts to stop all products. Returns after the timeout or as soon as all products
721      * are shut down.
722      */
723     protected void stopProducts(List<ProductExecution> productExecutions) throws MojoExecutionException
724     {
725         ExecutorService executor = Executors.newFixedThreadPool(productExecutions.size());
726         try
727         {
728             long before = System.nanoTime();
729             for (final ProductExecution execution : Iterables.reverse(productExecutions))
730             {
731                 final Product product = execution.getProduct();
732                 final ProductHandler productHandler = execution.getProductHandler();
733 
734                 Future<?> task = executor.submit(new Runnable()
735                 {
736                     @Override
737                     public void run()
738                     {
739                         getLog().info(product.getInstanceId() + ": Shutting down");
740                         try
741                         {
742                             productHandler.stop(product);
743                         }
744                         catch (MojoExecutionException e)
745                         {
746                             getLog().error("Exception while trying to stop " + product.getInstanceId(), e);
747                         }
748                     }
749                 });
750 
751                 try
752                 {
753                     task.get(product.getShutdownTimeout(), TimeUnit.MILLISECONDS);
754                 }
755                 catch (TimeoutException e)
756                 {
757                     getLog().info(product.getInstanceId() + " shutdown: Didn't return in time");
758                     task.cancel(true);
759                 }
760             }
761             long after = System.nanoTime();
762             getLog().info("amps:stop in " + TimeUnit.NANOSECONDS.toSeconds(after - before) + "s");
763         }
764         catch (InterruptedException e1)
765         {
766             Thread.currentThread().interrupt();
767         }
768         catch (ExecutionException e)
769         {
770             throw new MojoExecutionException("Exception while stopping the products", e);
771         }
772 
773         // If products were launched in parallel, check they are stopped: CodeHaus Cargo returns before
774         // products are down.
775         if (parallel)
776         {
777             waitForProducts(productExecutions, false);
778         }
779     }
780 
781 
782     /**
783      * Waits until all products are running or stopped
784      * @param startingUp true if starting up the products, false if shutting down.
785      */
786     protected void waitForProducts(List<ProductExecution> productExecutions, boolean startingUp) throws MojoExecutionException
787     {
788         for (ProductExecution productExecution : productExecutions)
789         {
790             pingRepeatedly(productExecution.getProduct(), startingUp);
791         }
792     }
793 
794     /**
795      * Ping the product until it's up or stopped
796      * @param startingUp true if applications are expected to be up; false if applications are expected to be brought down
797      * @throws MojoExecutionException if the product didn't have the expected behaviour beofre the timeout
798      */
799     private void pingRepeatedly(Product product, boolean startingUp) throws MojoExecutionException
800     {
801         if (product.getHttpPort() != 0)
802         {
803             String url = "http://" + product.getServer() + ":" + product.getHttpPort();
804             if (StringUtils.isNotBlank(product.getContextPath()))
805             {
806                 url = url + product.getContextPath();
807             }
808 
809             int timeout = startingUp ? product.getStartupTimeout() : product.getShutdownTimeout();
810             final long end = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(timeout);
811             boolean interrupted = false;
812             boolean success = false;
813             String lastMessage = "";
814 
815             // keep retrieving from the url until a good response is returned, under a time limit.
816             while (!success && !interrupted && System.nanoTime() < end)
817             {
818                 HttpURLConnection connection = null;
819                 try
820                 {
821                     URL urlToPing = new URL(url);
822                     connection = (HttpURLConnection) urlToPing.openConnection();
823                     int response = connection.getResponseCode();
824                     // Tomcat returns 404 until the webapp is up
825                     lastMessage = "Last response code is " + response;
826                     if (startingUp)
827                     {
828                         success = response < 400;
829                     }
830                     else
831                     {
832                         success = response >= 400;
833                     }
834                 }
835                 catch (IOException e)
836                 {
837                     lastMessage = e.getMessage();
838                     success = !startingUp;
839                 }
840                 finally
841                 {
842                     if (connection != null)
843                     {
844                         try
845                         {
846                             connection.getInputStream().close();
847                         }
848                         catch (IOException e)
849                         {
850                             // Don't do anything
851                         }
852                     }
853                 }
854 
855                 if (!success)
856                 {
857                     getLog().info("Waiting for " + url + (startingUp ? "" : " to stop"));
858                     try
859                     {
860                         Thread.sleep(1000);
861                     }
862                     catch (InterruptedException e)
863                     {
864                         Thread.currentThread().interrupt();
865                         interrupted = true;
866                         break;
867                     }
868                 }
869             }
870 
871             if (!success)
872             {
873                 throw new MojoExecutionException(String.format("The product %s didn't %s after %ds at %s. %s",
874                         product.getInstanceId(), startingUp ? "start" : "stop", TimeUnit.MILLISECONDS.toSeconds(timeout), url, lastMessage));
875             }
876         }
877     }
878 
879     /**
880      * @return the list of instances for the product 'studio'
881      */
882     private Iterable<ProductExecution> getStudioExecutions(final List<ProductExecution> productExecutions)
883     {
884         return Iterables.filter(productExecutions, new Predicate<ProductExecution>(){
885 
886             @Override
887             public boolean apply(ProductExecution input)
888             {
889                 return input.getProductHandler() instanceof StudioProductHandler;
890             }});
891     }
892 
893 
894     /**
895      * If there is any Studio instance, returns a list with all products requested by this instance.
896      *
897      * Configures both the Studio instance and its dependent products.
898      *
899      * @param productExecutions the current list of products to run
900      * @param goals
901      * @return the complete list of products to run
902      * @throws MojoExecutionException
903      */
904     protected List<ProductExecution> includeStudioDependentProducts(final List<ProductExecution> productExecutions, final MavenGoals goals)
905             throws MojoExecutionException
906     {
907         // If one of the products is Studio, ask him/her which other products he/she wants to run
908         Iterable<ProductExecution> studioExecutions = getStudioExecutions(productExecutions);
909         if (Iterables.isEmpty(studioExecutions))
910         {
911             return productExecutions;
912         }
913 
914         // We have studio execution(s), so we need to add all products requested by Studio
915         List<ProductExecution> productExecutionsIncludingStudio = Lists.newArrayList(productExecutions);
916         Map<String, Product> allContexts = getProductContexts();
917         for(ProductExecution execution : studioExecutions)
918         {
919             for (String dependantProduct : StudioProductHandler.getDependantInstances(execution.getProduct()))
920             {
921                 Product product = allContexts.get(dependantProduct);
922                 productExecutionsIncludingStudio.add(toProductExecution(product));
923             }
924         }
925 
926         return productExecutionsIncludingStudio;
927     }
928     
929     protected ProductExecution toProductExecution(Product product)
930     {
931         return new ProductExecution(product, createProductHandler(product.getId()));
932     }
933 
934     protected abstract void doExecute() throws MojoExecutionException, MojoFailureException;
935 
936     protected void setParallelMode(List<ProductExecution> executions)
937     {
938         // Apply the configuration of the mojo to the products
939         for (ProductExecution execution : executions)
940         {
941             Product product = execution.getProduct();
942             if (parallel)
943             {
944                 if (product.getSynchronousStartup() == null)
945                 {
946                     product.setSynchronousStartup(Boolean.FALSE);
947                 }
948             }
949             else
950             {
951                 product.setSynchronousStartup(Boolean.TRUE);
952             }
953         }
954     }
955 }