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                 String baseUrl = MavenGoals.getBaseUrl(product, actualHttpPort);
131                 properties.put("baseurl." + product.getInstanceId(), baseUrl);
132             }
133         }
134 
135         if (writePropertiesToFile)
136         {
137             writePropertiesFile();
138         }
139 
140         if (parallel)
141         {
142             waitForProducts(productExecutions, true);
143         }
144         long globalDurationSeconds = TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - globalStartTime);
145 
146         // Give the messages once all applications are started
147         if (successMessages.size() > 1 || parallel)
148         {
149             getLog().info("");
150             getLog().info("=== Summary (total time " + globalDurationSeconds + "s):");
151             // First show the log files
152             for (StartupInformation message : successMessages)
153             {
154                 if (StringUtils.isNotBlank(message.getOutput()))
155                 {
156                     getLog().info("Log available at: " + message.getOutput());
157                 }
158             }
159             // Then show the applications
160             for (StartupInformation message : successMessages)
161             {
162                 getLog().info(message.toString());
163             }
164         }
165 
166         if (wait)
167         {
168             getLog().info("Type Ctrl-D to shutdown gracefully");
169             getLog().info("Type Ctrl-C to exit");
170             try
171             {
172                 while (System.in.read() != -1)
173                 {
174                 }
175             }
176             catch (final IOException e)
177             {
178                 // ignore
179             }
180 
181             // We don't stop products when -Dwait=false, because some projects rely on the
182             // application running after the end of the RunMojo goal. The SHITTY tests
183             // check this behaviour.
184             stopProducts(productExecutions);
185         }
186     }
187 
188     protected List<ProductExecution> getProductExecutions() throws MojoExecutionException
189     {
190         final List<ProductExecution> productExecutions;
191         final MavenGoals goals = getMavenGoals();
192         if (!isBlank(testGroup))
193         {
194             productExecutions = getTestGroupProductExecutions(testGroup);
195         }
196         else if (!isBlank(instanceId))
197         {
198             Product ctx = getProductContexts().get(instanceId);
199             if (ctx == null)
200             {
201                 throw new MojoExecutionException("No product with instance ID '" + instanceId + "'");
202             }
203             ProductHandler product = createProductHandler(ctx.getId());
204             productExecutions = Collections.singletonList(new ProductExecution(ctx, product));
205         }
206         else
207         {
208             Product ctx = getProductContexts().get(getProductId());
209             ProductHandler product = createProductHandler(ctx.getId());
210             productExecutions = Collections.singletonList(new ProductExecution(ctx, product));
211         }
212         return filterExcludedInstances(includeStudioDependentProducts(productExecutions, goals));
213     }
214 
215     private List<ProductExecution> filterExcludedInstances(List<ProductExecution> executions) throws MojoExecutionException
216     {
217         if (StringUtils.isBlank(excludeInstances))
218         {
219             return executions;
220         }
221         boolean inverted = excludeInstances.startsWith("*/");
222         String instanceIdList = inverted ? excludeInstances.substring(2) : excludeInstances;
223 
224         // Parse the list given by the user and find ProductExecutions
225         List<String> excludedInstanceIds = Lists.newArrayList(instanceIdList.split(","));
226         List<ProductExecution> excludedExecutions = Lists.newArrayList();
227         for (final String instanceId : excludedInstanceIds)
228         {
229             try
230             {
231                 excludedExecutions.add(Iterables.find(executions, new Predicate<ProductExecution>()
232                 {
233                     @Override
234                     public boolean apply(ProductExecution input)
235                     {
236                         return input.getProduct().getInstanceId().equals(instanceId);
237                     }
238                 }));
239             }
240             catch (NoSuchElementException nsee)
241             {
242                 throw new MojoExecutionException("You specified -Dexclude=" + excludeInstances + " but " + instanceId + " is not an existing instance id.");
243             }
244         }
245 
246         if (inverted)
247         {
248             return excludedExecutions;
249         }
250         else
251         {
252             executions.removeAll(excludedExecutions);
253             return executions;
254         }
255     }
256 
257     /**
258      * Only install a plugin if the installPlugin flag is true and the project is a jar. If the test plugin was built,
259      * it will be installed as well.
260      */
261     private boolean shouldInstallPlugin()
262     {
263         Artifact artifact = getMavenContext().getProject().getArtifact();
264         return installPlugin &&
265                 (artifact != null && !"pom".equalsIgnoreCase(artifact.getType()));
266     }
267 
268     private void writePropertiesFile() throws MojoExecutionException
269     {
270         final Properties props = new Properties();
271 
272         for (Map.Entry<String, String> entry : properties.entrySet())
273         {
274             props.setProperty(entry.getKey(), entry.getValue());
275         }
276 
277         final File ampsProperties = new File(getMavenContext().getProject().getBuild().getDirectory(), "amps.properties");
278         OutputStream out = null;
279         try
280         {
281             out = new FileOutputStream(ampsProperties);
282             props.store(out, "");
283         }
284         catch (IOException e)
285         {
286             throw new MojoExecutionException("Error writing " + ampsProperties.getAbsolutePath(), e);
287         }
288         finally
289         {
290             IOUtils.closeQuietly(out);
291         }
292     }
293 
294     /**
295      * Wraps information about the startup of a product
296      */
297     private static class StartupInformation
298     {
299         int actualHttpPort;
300         long durationSeconds;
301         Product product;
302         String event;
303 
304         public StartupInformation(Product product, String event, int actualHttpPort, long durationSeconds)
305         {
306             super();
307             this.actualHttpPort = actualHttpPort;
308             this.product = product;
309             this.event = event;
310             this.durationSeconds = durationSeconds;
311         }
312 
313         @Override
314         public String toString()
315         {
316             String message = String.format("%s %s in %ds", product.getInstanceId(), event
317                     + (Boolean.FALSE.equals(product.getSynchronousStartup()) ? " (asynchronously)" : ""), durationSeconds);
318             if (actualHttpPort != 0)
319             {
320                 message += " at http://" + product.getServer() + ":" + actualHttpPort + product.getContextPath();
321             }
322             return message;
323         }
324 
325         /**
326          * @return the output of the product
327          */
328         public String getOutput()
329         {
330             return product.getOutput();
331         }
332 
333     }
334 }