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