1   package com.atlassian.maven.plugins.amps;
2   
3   import com.atlassian.maven.plugins.amps.product.ProductHandler;
4   import com.atlassian.maven.plugins.amps.util.GoogleAmpsTracker;
5   import com.google.common.base.Predicate;
6   import com.google.common.collect.Iterables;
7   import com.google.common.collect.Lists;
8   import org.apache.commons.io.IOUtils;
9   import org.apache.maven.plugin.MojoExecutionException;
10  import org.apache.maven.plugin.MojoFailureException;
11  import org.apache.maven.surefire.shade.org.apache.commons.lang.StringUtils;
12  import org.apache.maven.artifact.Artifact;
13  import org.jfrog.maven.annomojo.annotations.MojoExecute;
14  import org.jfrog.maven.annomojo.annotations.MojoGoal;
15  import org.jfrog.maven.annomojo.annotations.MojoParameter;
16  import org.jfrog.maven.annomojo.annotations.MojoRequiresDependencyResolution;
17  
18  import java.io.File;
19  import java.io.FileOutputStream;
20  import java.io.IOException;
21  import java.io.OutputStream;
22  import java.util.Collections;
23  import java.util.HashMap;
24  import java.util.List;
25  import java.util.Map;
26  import java.util.NoSuchElementException;
27  import java.util.Properties;
28  import java.util.concurrent.TimeUnit;
29  
30  import static org.apache.commons.lang.StringUtils.isBlank;
31  
32  /**
33   * Run the webapp
34   */
35  @MojoGoal("run")
36  @MojoExecute(phase = "package")
37  @MojoRequiresDependencyResolution
38  public class RunMojo extends AbstractTestGroupsHandlerMojo
39  {
40      @MojoParameter(expression = "${wait}", defaultValue = "true")
41      private boolean wait;
42  
43      /**
44       * Whether or not to write properties used by the plugin to amps.properties.
45       */
46      @MojoParameter(expression = "${amps.properties}", required = true, defaultValue = "false")
47      protected boolean writePropertiesToFile;
48  
49      /**
50       * Test group to run. If provided, used to determine the products to run.
51       */
52      @MojoParameter(expression = "${testGroup}")
53      protected String testGroup;
54  
55      /**
56       * Excluded instances from the execution. Only useful when Studio brings in all instances and you want to run only one.
57       * List of comma separated instanceIds, or {@literal *}/instanceId to exclude all but one product.
58       * <p>
59       * Examples:
60       * <ul>
61       * <li>mvn amps:run -DexcludeInstances=studio-crowd</li>
62       * <li>mvn amps:run -DexcludeInstances={@literal *}/studio-crowd to run only StudioCrowd</li>
63       * </ul>
64       */
65      @MojoParameter(expression = "${excludeInstances}")
66      protected String excludeInstances;
67  
68      /**
69       * The properties actually used by the mojo when running
70       */
71      protected final Map<String, String> properties = new HashMap<String, String>();
72  
73      protected void doExecute() throws MojoExecutionException, MojoFailureException
74      {
75          getGoogleTracker().track(GoogleAmpsTracker.RUN);
76  
77          final List<ProductExecution> productExecutions = getProductExecutions();
78  
79          startProducts(productExecutions);
80      }
81  
82      protected void startProducts(List<ProductExecution> productExecutions) throws MojoExecutionException
83      {
84          long globalStartTime = System.nanoTime();
85          setParallelMode(productExecutions);
86          List<StartupInformation> successMessages = Lists.newArrayList();
87          for (ProductExecution productExecution : productExecutions)
88          {
89              final ProductHandler productHandler = productExecution.getProductHandler();
90              final Product product = productExecution.getProduct();
91              if (product.isInstallPlugin() == null)
92              {
93                  product.setInstallPlugin(shouldInstallPlugin());
94              }
95  
96              // Leave a blank line and say what it's doing
97              getLog().info("");
98              if (StringUtils.isNotBlank(product.getOutput()))
99              {
100                 getLog().info(String.format("Starting %s... (see log at %s)", product.getInstanceId(), product.getOutput()));
101             }
102             else
103             {
104                 getLog().info(String.format("Starting %s...", product.getInstanceId()));
105             }
106 
107             // Actually start the product
108             long startTime = System.nanoTime();
109             int actualHttpPort = productHandler.start(product);
110             long durationSeconds = TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - startTime);
111 
112             // Log the success message
113             StartupInformation message = new StartupInformation(product, "started successfully", actualHttpPort, durationSeconds);
114             if (!parallel)
115             {
116                 getLog().info(message.toString());
117             }
118             successMessages.add(message);
119 
120             if (writePropertiesToFile)
121             {
122                 if (productExecutions.size() == 1)
123                 {
124                     properties.put("http.port", String.valueOf(actualHttpPort));
125                     properties.put("context.path", product.getContextPath());
126                 }
127 
128                 properties.put("http." + product.getInstanceId() + ".port", String.valueOf(actualHttpPort));
129                 properties.put("context." + product.getInstanceId() + ".path", product.getContextPath());
130             }
131         }
132 
133         if (writePropertiesToFile)
134         {
135             writePropertiesFile();
136         }
137 
138         if (parallel)
139         {
140             waitForProducts(productExecutions, true);
141         }
142         long globalDurationSeconds = TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - globalStartTime);
143 
144         // Give the messages once all applications are started
145         if (successMessages.size() > 1 || parallel)
146         {
147             getLog().info("");
148             getLog().info("=== Summary (total time " + globalDurationSeconds + "s):");
149             // First show the log files
150             for (StartupInformation message : successMessages)
151             {
152                 if (StringUtils.isNotBlank(message.getOutput()))
153                 {
154                     getLog().info("Log available at: " + message.getOutput());
155                 }
156             }
157             // Then show the applications
158             for (StartupInformation message : successMessages)
159             {
160                 getLog().info(message.toString());
161             }
162         }
163 
164         if (wait)
165         {
166             getLog().info("Type Ctrl-D to shutdown gracefully");
167             getLog().info("Type Ctrl-C to exit");
168             try
169             {
170                 while (System.in.read() != -1)
171                 {
172                 }
173             }
174             catch (final IOException e)
175             {
176                 // ignore
177             }
178 
179             // We don't stop products when -Dwait=false, because some projects rely on the
180             // application running after the end of the RunMojo goal. The SHITTY tests
181             // check this behaviour.
182             stopProducts(productExecutions);
183         }
184     }
185 
186     protected List<ProductExecution> getProductExecutions() throws MojoExecutionException
187     {
188         final List<ProductExecution> productExecutions;
189         final MavenGoals goals = getMavenGoals();
190         if (!isBlank(testGroup))
191         {
192             productExecutions = getTestGroupProductExecutions(testGroup);
193         }
194         else if (!isBlank(instanceId))
195         {
196             Product ctx = getProductContexts().get(instanceId);
197             if (ctx == null)
198             {
199                 throw new MojoExecutionException("No product with instance ID '" + instanceId + "'");
200             }
201             ProductHandler product = createProductHandler(ctx.getId());
202             productExecutions = Collections.singletonList(new ProductExecution(ctx, product));
203         }
204         else
205         {
206             Product ctx = getProductContexts().get(getProductId());
207             ProductHandler product = createProductHandler(ctx.getId());
208             productExecutions = Collections.singletonList(new ProductExecution(ctx, product));
209         }
210         return filterExcludedInstances(includeStudioDependentProducts(productExecutions, goals));
211     }
212 
213     private List<ProductExecution> filterExcludedInstances(List<ProductExecution> executions) throws MojoExecutionException
214     {
215         if (StringUtils.isBlank(excludeInstances))
216         {
217             return executions;
218         }
219         boolean inverted = excludeInstances.startsWith("*/");
220         String instanceIdList = inverted ? excludeInstances.substring(2) : excludeInstances;
221 
222         // Parse the list given by the user and find ProductExecutions
223         List<String> excludedInstanceIds = Lists.newArrayList(instanceIdList.split(","));
224         List<ProductExecution> excludedExecutions = Lists.newArrayList();
225         for (final String instanceId : excludedInstanceIds)
226         {
227             try
228             {
229                 excludedExecutions.add(Iterables.find(executions, new Predicate<ProductExecution>()
230                 {
231                     @Override
232                     public boolean apply(ProductExecution input)
233                     {
234                         return input.getProduct().getInstanceId().equals(instanceId);
235                     }
236                 }));
237             }
238             catch (NoSuchElementException nsee)
239             {
240                 throw new MojoExecutionException("You specified -Dexclude=" + excludeInstances + " but " + instanceId + " is not an existing instance id.");
241             }
242         }
243 
244         if (inverted)
245         {
246             return excludedExecutions;
247         }
248         else
249         {
250             executions.removeAll(excludedExecutions);
251             return executions;
252         }
253     }
254 
255     /**
256      * Only install a plugin if the installPlugin flag is true and the project is a jar. If the test plugin was built,
257      * it will be installed as well.
258      */
259     private boolean shouldInstallPlugin()
260     {
261         Artifact artifact = getMavenContext().getProject().getArtifact();
262         return installPlugin &&
263                 (artifact != null && !"pom".equalsIgnoreCase(artifact.getType()));
264     }
265 
266     private void writePropertiesFile() throws MojoExecutionException
267     {
268         final Properties props = new Properties();
269 
270         for (Map.Entry<String, String> entry : properties.entrySet())
271         {
272             props.setProperty(entry.getKey(), entry.getValue());
273         }
274 
275         final File ampsProperties = new File(getMavenContext().getProject().getBuild().getDirectory(), "amps.properties");
276         OutputStream out = null;
277         try
278         {
279             out = new FileOutputStream(ampsProperties);
280             props.store(out, "");
281         }
282         catch (IOException e)
283         {
284             throw new MojoExecutionException("Error writing " + ampsProperties.getAbsolutePath(), e);
285         }
286         finally
287         {
288             IOUtils.closeQuietly(out);
289         }
290     }
291 
292     /**
293      * Wraps information about the startup of a product
294      */
295     private static class StartupInformation
296     {
297         int actualHttpPort;
298         long durationSeconds;
299         Product product;
300         String event;
301 
302         public StartupInformation(Product product, String event, int actualHttpPort, long durationSeconds)
303         {
304             super();
305             this.actualHttpPort = actualHttpPort;
306             this.product = product;
307             this.event = event;
308             this.durationSeconds = durationSeconds;
309         }
310 
311         @Override
312         public String toString()
313         {
314             String message = String.format("%s %s in %ds", product.getInstanceId(), event
315                     + (Boolean.FALSE.equals(product.getSynchronousStartup()) ? " (asynchronously)" : ""), durationSeconds);
316             if (actualHttpPort != 0)
317             {
318                 message += " at http://" + product.getServer() + ":" + actualHttpPort + product.getContextPath();
319             }
320             return message;
321         }
322 
323         /**
324          * @return the output of the product
325          */
326         public String getOutput()
327         {
328             return product.getOutput();
329         }
330 
331     }
332 }