1   package com.atlassian.maven.plugins.amps.product;
2   
3   import java.io.File;
4   
5   import java.io.IOException;
6   import java.util.ArrayList;
7   import java.util.Arrays;
8   import java.util.Collection;
9   import java.util.HashMap;
10  import java.util.Iterator;
11  import java.util.List;
12  import java.util.Map;
13  
14  import com.atlassian.maven.plugins.amps.AbstractProductHandlerMojo;
15  import com.atlassian.maven.plugins.amps.MavenContext;
16  import com.atlassian.maven.plugins.amps.MavenGoals;
17  import com.atlassian.maven.plugins.amps.Product;
18  import com.atlassian.maven.plugins.amps.ProductArtifact;
19  import com.atlassian.maven.plugins.amps.util.ConfigFileUtils;
20  
21  import com.atlassian.maven.plugins.amps.util.JvmArgsFix;
22  import com.google.common.base.Function;
23  import com.google.common.base.Joiner;
24  import com.google.common.collect.Iterables;
25  
26  import org.apache.commons.lang.StringUtils;
27  import org.apache.maven.plugin.MojoExecutionException;
28  
29  import static com.atlassian.maven.plugins.amps.util.FileUtils.doesFileNameMatchArtifact;
30  import static com.atlassian.maven.plugins.amps.util.ZipUtils.unzip;
31  import static org.apache.commons.io.FileUtils.copyDirectory;
32  import static org.apache.commons.io.FileUtils.copyFile;
33  import static org.apache.commons.io.FileUtils.iterateFiles;
34  import static org.apache.commons.io.FileUtils.moveDirectory;
35  import static org.apache.commons.io.FileUtils.readFileToString;
36  import static com.atlassian.maven.plugins.amps.util.ProjectUtils.createDirectory;
37  
38  public abstract class AbstractProductHandler extends AmpsProductHandler
39  {
40      private final PluginProvider pluginProvider;
41  
42      protected AbstractProductHandler(MavenContext context, MavenGoals goals, PluginProvider pluginProvider)
43      {
44          super(context, goals);
45          this.pluginProvider = pluginProvider;
46      }
47  
48      /**
49       * Extracts the product and its home, prepares both and starts the product
50       * @return the port
51       */
52      public final int start(final Product ctx) throws MojoExecutionException
53      {
54          final File homeDir = extractAndProcessHomeDirectory(ctx);
55  
56          final File extractedApp = extractApplication(ctx, homeDir);
57  
58          final File finalApp = addArtifactsAndOverrides(ctx, homeDir, extractedApp);
59  
60          // Ask for the system properties (from the ProductHandler and from the pom.xml)
61          Map<String, String> systemProperties = mergeSystemProperties(ctx);
62  
63          return startApplication(ctx, finalApp, homeDir, systemProperties);
64      }
65  
66      protected final File extractAndProcessHomeDirectory(final Product ctx) throws MojoExecutionException
67      {
68          final File homeDir = getHomeDirectory(ctx);
69  
70          // Check if home directory was provided by the user
71          if (StringUtils.isNotBlank(ctx.getDataHome()))
72          {
73              // Don't modify the home. Just use it.
74              return homeDir;
75          }
76  
77          // Create a home dir for the product in target
78          final File productHomeData = getProductHomeData(ctx);
79          if (productHomeData != null)
80          {
81  
82              // Only create the home dir if it doesn't exist
83              if (!homeDir.exists())
84              {
85                  extractProductHomeData(productHomeData, homeDir, ctx);
86  
87                  // just in case
88                  homeDir.mkdir();
89                  processHomeDirectory(ctx, homeDir);
90              }
91  
92              // Always override files regardless of home directory existing or not
93              overrideAndPatchHomeDir(homeDir, ctx);
94          }
95          return homeDir;
96      }
97  
98      protected void extractProductHomeData(File productHomeData, File homeDir, Product ctx)
99              throws MojoExecutionException
100     {
101         final File tmpDir = new File(getBaseDirectory(ctx), "tmp-resources");
102         tmpDir.mkdir();
103 
104         try
105         {
106             if (productHomeData.isFile())
107             {
108                 File tmp = new File(getBaseDirectory(ctx), ctx.getId() + "-home");
109 
110                 unzip(productHomeData, tmpDir.getPath());
111 
112                 File[] topLevelFiles = tmpDir.listFiles();
113                 if (topLevelFiles.length != 1)
114                 {
115                     Iterable<String> filenames = Iterables.transform(Arrays.asList(topLevelFiles), new Function<File, String>(){
116                         @Override
117                         public String apply(File from)
118                         {
119                             return from.getName();
120                         }
121                     });
122                     throw new MojoExecutionException("Expected a single top-level directory in test resources. Got: "
123                             + Joiner.on(", ").join(filenames));
124                 }
125 
126                 copyDirectory(topLevelFiles[0], getBaseDirectory(ctx), true);
127                 moveDirectory(tmp, homeDir);
128             }
129             else if (productHomeData.isDirectory())
130             {
131                 copyDirectory(productHomeData, homeDir);
132             }
133         }
134         catch (final IOException ex)
135         {
136             throw new MojoExecutionException("Unable to copy home directory", ex);
137         }
138     }
139 
140     /**
141      * Takes 'app' (the file of the application - either .war or the exploded directory),
142      * adds the artifacts, then returns the 'app'.
143      * @return if {@code app} was a dir, returns a dir; if {@code app} was a war, returns a war.
144      */
145     private final File addArtifactsAndOverrides(final Product ctx, final File homeDir, final File app) throws MojoExecutionException
146     {
147         try
148         {
149             final File appDir;
150             if (app.isFile())
151             {
152                 appDir = new File(getBaseDirectory(ctx), "webapp");
153                 if (!appDir.exists())
154                 {
155                     unzip(app, appDir.getAbsolutePath());
156                 }
157             }
158             else
159             {
160                 appDir = app;
161             }
162 
163             addArtifacts(ctx, homeDir, appDir);
164 
165             // override war files
166             try
167             {
168                 addOverrides(appDir, ctx);
169                 customiseInstance(ctx, homeDir, appDir);
170                 fixJvmArgs(ctx);
171             }
172             catch (IOException e)
173             {
174                 throw new MojoExecutionException("Unable to override WAR files using src/test/resources/" + ctx.getInstanceId() + "-app", e);
175             }
176 
177             if (app.isFile())
178             {
179                 final File warFile = new File(app.getParentFile(), getId() + ".war");
180                 com.atlassian.core.util.FileUtils.createZipFile(appDir, warFile);
181                 return warFile;
182             }
183             else
184             {
185                 return appDir;
186             }
187 
188         }
189         catch (final Exception e)
190         {
191             throw new MojoExecutionException(e.getMessage(), e);
192         }
193     }
194 
195     /**
196      * Each product handler can add specific operations on the application's home and war.
197      * By default no operation is performed in this hook.
198      *
199      * <p>Example: StudioXXXProductHandlers can change the webapp to be studio-ready.</p>
200      * @param ctx the product's details
201      * @param homeDir the home directory
202      * @param explodedWarDir the directory containing the exploded WAR of the application
203      * @throws MojoExecutionException
204      */
205     protected void customiseInstance(Product ctx, File homeDir, File explodedWarDir) throws MojoExecutionException
206     {
207         // No operation by default
208     }
209 
210     /**
211      * Fix jvmArgs, providing necessary defaults.
212      * @param ctx Product to fix jvmArgs for
213      */
214     protected void fixJvmArgs(Product ctx)
215     {
216         final String jvmArgs = JvmArgsFix.defaults()
217                 .apply(ctx.getJvmArgs());
218         ctx.setJvmArgs(jvmArgs);
219     }
220 
221     private void addArtifacts(final Product ctx, final File homeDir, final File appDir)
222             throws IOException, MojoExecutionException, Exception
223     {
224         File pluginsDir = getUserInstalledPluginsDirectory(appDir, homeDir);
225         final File bundledPluginsDir = new File(getBaseDirectory(ctx), "bundled-plugins");
226 
227         bundledPluginsDir.mkdir();
228         // add bundled plugins
229         final File bundledPluginsZip = new File(appDir, getBundledPluginPath(ctx));
230         if (bundledPluginsZip.exists())
231         {
232             unzip(bundledPluginsZip, bundledPluginsDir.getPath());
233         }
234 
235         if (isStaticPlugin())
236         {
237             if (!supportsStaticPlugins())
238             {
239                   throw new MojoExecutionException("According to your atlassian-plugin.xml file, this plugin is not " +
240                           "atlassian-plugins version 2. This app currently only supports atlassian-plugins " +
241                           "version 2.");
242             }
243             pluginsDir = new File(appDir, "WEB-INF/lib");
244         }
245 
246         if (pluginsDir == null)
247         {
248             pluginsDir = bundledPluginsDir;
249         }
250 
251         createDirectory(pluginsDir);
252 
253         // add this plugin itself if enabled
254         if (ctx.isInstallPlugin())
255         {
256             addThisPluginToDirectory(pluginsDir);
257             addTestPluginToDirectory(pluginsDir);
258         }
259 
260         // add plugins2 plugins if necessary
261         if (!isStaticPlugin())
262         {
263             addArtifactsToDirectory(pluginProvider.provide(ctx), pluginsDir);
264         }
265 
266         // add plugins1 plugins
267         List<ProductArtifact> artifacts = new ArrayList<ProductArtifact>();
268         artifacts.addAll(getDefaultLibPlugins());
269         artifacts.addAll(ctx.getLibArtifacts());
270         addArtifactsToDirectory(artifacts, new File(appDir, "WEB-INF/lib"));
271 
272         artifacts = new ArrayList<ProductArtifact>();
273         artifacts.addAll(getDefaultBundledPlugins());
274         artifacts.addAll(ctx.getBundledArtifacts());
275 
276         addArtifactsToDirectory(artifacts, bundledPluginsDir);
277 
278         if (bundledPluginsDir.list().length > 0)
279         {
280             com.atlassian.core.util.FileUtils.createZipFile(bundledPluginsDir, bundledPluginsZip);
281         }
282 
283         if (ctx.getLog4jProperties() != null && getLog4jPropertiesPath() != null)
284         {
285             copyFile(ctx.getLog4jProperties(), new File(appDir, getLog4jPropertiesPath()));
286         }
287     }
288 
289     /**
290      * Processes standard replacement of configuration placeholders in the home directory.
291      */
292     protected void processHomeDirectory(Product ctx, File snapshotDir) throws MojoExecutionException
293     {
294         ConfigFileUtils.replace(getConfigFiles(ctx, snapshotDir), getReplacements(ctx), false, log);
295     }
296 
297     abstract protected File extractApplication(Product ctx, File homeDir) throws MojoExecutionException;
298     abstract protected int startApplication(Product ctx, File app, File homeDir, Map<String, String> properties) throws MojoExecutionException;
299     abstract protected boolean supportsStaticPlugins();
300     abstract protected Collection<? extends ProductArtifact> getDefaultBundledPlugins();
301     abstract protected Collection<? extends ProductArtifact> getDefaultLibPlugins();
302     abstract protected String getBundledPluginPath(Product ctx);
303     abstract protected File getUserInstalledPluginsDirectory(File webappDir, File homeDir);
304 
305     protected String getLog4jPropertiesPath()
306     {
307         return null;
308     }
309 
310     protected boolean isStaticPlugin() throws IOException
311     {
312         final File atlassianPluginXml = new File(project.getBasedir(), "src/main/resources/atlassian-plugin.xml");
313         if (atlassianPluginXml.exists())
314         {
315             String text = readFileToString(atlassianPluginXml);
316             return !text.contains("pluginsVersion=\"2\"") && !text.contains("plugins-version=\"2\"");
317         }
318         else
319         {
320             // probably an osgi bundle
321             return false;
322         }
323     }
324 
325     protected final void addThisPluginToDirectory(final File targetDir) throws IOException
326     {
327         final File thisPlugin = getPluginFile();
328 
329         if (thisPlugin.exists())
330         {
331             // remove any existing version
332             for (final Iterator<?> iterateFiles = iterateFiles(targetDir, null, false); iterateFiles.hasNext();)
333             {
334                 final File file = (File) iterateFiles.next();
335                 if (doesFileNameMatchArtifact(file.getName(), project.getArtifactId()))
336                 {
337                     file.delete();
338                 }
339             }
340 
341             // add the plugin jar to the directory
342             copyFile(thisPlugin, new File(targetDir, thisPlugin.getName()));
343         }
344         else
345         {
346             log.info("No plugin in the current project - " + thisPlugin.getAbsolutePath());
347         }
348     }
349 
350     protected void addTestPluginToDirectory(final File targetDir) throws IOException
351     {
352         final File testPluginFile = getTestPluginFile();
353         if (testPluginFile.exists())
354         {
355             // add the test plugin jar to the directory
356             copyFile(testPluginFile, new File(targetDir, testPluginFile.getName()));
357         }
358 
359     }
360 
361     protected final File getPluginFile()
362     {
363         return new File(project.getBuild().getDirectory(), project.getBuild().getFinalName() + ".jar");
364     }
365 
366     protected File getTestPluginFile()
367     {
368         return new File(project.getBuild().getDirectory(), project.getBuild().getFinalName() + "-tests.jar");
369     }
370 
371     protected final void addArtifactsToDirectory(final List<ProductArtifact> artifacts, final File pluginsDir) throws MojoExecutionException
372     {
373         // copy the all the plugins we want in the webapp
374         if (!artifacts.isEmpty())
375         {
376             // first remove plugins from the webapp that we want to update
377             if (pluginsDir.isDirectory() && pluginsDir.exists())
378             {
379                 for (final Iterator<?> iterateFiles = iterateFiles(pluginsDir, null, false); iterateFiles.hasNext();)
380                 {
381                     final File file = (File) iterateFiles.next();
382                     for (final ProductArtifact webappArtifact : artifacts)
383                     {
384                         if (!file.isDirectory() && doesFileNameMatchArtifact(file.getName(), webappArtifact.getArtifactId()))
385                         {
386                             file.delete();
387                         }
388                     }
389                 }
390             }
391             goals.copyPlugins(pluginsDir, artifacts);
392         }
393     }
394 
395     protected final void addOverrides(File appDir, final Product ctx) throws IOException
396     {
397         final File srcDir = new File(project.getBasedir(), "src/test/resources/" + ctx.getInstanceId() + "-app");
398         if (srcDir.exists() && appDir.exists())
399         {
400             copyDirectory(srcDir, appDir);
401         }
402     }
403 
404     /**
405      * Merges the properties: pom.xml overrides {@link AbstractProductHandlerMojo#setDefaultValues} overrides the Product Handler.
406      * @param ctx the Product
407      * @return the complete list of system properties
408      */
409     protected final Map<String, String> mergeSystemProperties(Product ctx)
410     {
411         final Map<String, String> properties = new HashMap<String, String>();
412 
413         properties.putAll(getSystemProperties(ctx));
414         for (Map.Entry<String, Object> entry : ctx.getSystemPropertyVariables().entrySet())
415         {
416             properties.put(entry.getKey(), (String) entry.getValue());
417         }
418         return properties;
419     }
420 
421     /**
422      * System properties which are specific to the Product Handler
423      */
424     protected abstract Map<String, String> getSystemProperties(Product ctx);
425 
426     /**
427      * The artifact of the product (a war, a jar, a binary...)
428      */
429     protected abstract ProductArtifact getArtifact();
430 
431 }