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