1   package com.atlassian.maven.plugins.amps.product.studio;
2   
3   import static com.atlassian.maven.plugins.amps.product.ProductHandlerFactory.STUDIO;
4   import static com.atlassian.maven.plugins.amps.product.ProductHandlerFactory.STUDIO_BAMBOO;
5   import static com.atlassian.maven.plugins.amps.product.ProductHandlerFactory.STUDIO_CONFLUENCE;
6   import static com.atlassian.maven.plugins.amps.product.ProductHandlerFactory.STUDIO_CROWD;
7   import static com.atlassian.maven.plugins.amps.product.ProductHandlerFactory.STUDIO_FECRU;
8   import static com.atlassian.maven.plugins.amps.product.ProductHandlerFactory.STUDIO_JIRA;
9   import static com.atlassian.maven.plugins.amps.util.ZipUtils.unzip;
10  import static org.apache.commons.io.FileUtils.copyDirectory;
11  
12  import java.io.BufferedReader;
13  import java.io.File;
14  import java.io.IOException;
15  import java.io.InputStreamReader;
16  import java.util.ArrayList;
17  import java.util.Arrays;
18  import java.util.HashMap;
19  import java.util.Iterator;
20  import java.util.List;
21  import java.util.Locale;
22  import java.util.Map;
23  import java.util.Set;
24  
25  import org.apache.commons.io.FileUtils;
26  import org.apache.maven.plugin.MojoExecutionException;
27  import org.apache.maven.plugin.logging.Log;
28  
29  import com.atlassian.maven.plugins.amps.MavenContext;
30  import com.atlassian.maven.plugins.amps.MavenGoals;
31  import com.atlassian.maven.plugins.amps.Product;
32  import com.atlassian.maven.plugins.amps.ProductArtifact;
33  import com.atlassian.maven.plugins.amps.ProductExecution;
34  import com.atlassian.maven.plugins.amps.product.AmpsProductHandler;
35  import com.atlassian.maven.plugins.amps.product.ProductHandler;
36  import com.atlassian.maven.plugins.amps.product.ProductHandlerFactory;
37  import com.atlassian.maven.plugins.amps.util.ConfigFileUtils;
38  import com.atlassian.maven.plugins.amps.util.ConfigFileUtils.Replacement;
39  import com.atlassian.maven.plugins.amps.util.ProjectUtils;
40  import com.google.common.collect.Lists;
41  import com.google.common.collect.Maps;
42  import com.google.common.collect.Sets;
43  
44  /**
45   * This product handler is a 'ghost'. It doesn't start a product, but it prepares the environment
46   * for all studio-based products.
47   */
48  final public class StudioProductHandler extends AmpsProductHandler
49  {
50  
51      private static final String STUDIO_PROPERTIES = "home/studio.properties";
52      private static final String STUDIO_INITIAL_DATA_PROPERTIES = "home/studio-initial-data.properties";
53      private static final String DEVMODE_HAL_LICENSES_XML = "home/devmode-hal-licenses.xml";
54      private static final String STUDIO_INITIAL_DATA_XML = "home/studio-initial-data.xml";
55  
56      /** This token is used in product's <version> when they want to reuse the Studio product's version */
57      private static final String STUDIO_VERSION_TOKEN = "STUDIO-VERSION";
58  
59      private final static String LAUNCH_INSTANCES_SYSTEM_PROPERTY = "studio.instanceIds";
60  
61      private final static Map<String, String> defaultContextPaths = new HashMap<String, String>()
62      {
63          {
64              put(STUDIO_BAMBOO, "/builds");
65              put(STUDIO_CONFLUENCE, "/wiki");
66              put(STUDIO_CROWD, "/crowd");
67              put(STUDIO_FECRU, "/");
68              put(STUDIO_JIRA, "/jira");
69          }
70      };
71  
72      public StudioProductHandler(MavenContext context, MavenGoals goals)
73      {
74          super(context, goals);
75      }
76  
77  
78  
79      @Override
80      protected ProductArtifact getTestResourcesArtifact()
81      {
82          return new ProductArtifact("com.atlassian.studio", "studio-test-resources");
83      }
84  
85  
86  
87      @Override
88      public String getId()
89      {
90          return STUDIO;
91      }
92  
93      /**
94       * Returns the list of products that are configured in this studio instance, as defined in 'instanceIds'
95       *
96       * @param studioContext
97       *            the Studio product
98       * @return a list of instance ids. Never null.
99       * @throws MojoExecutionException
100      *             if the Studio product is misconfigured
101      */
102     public List<String> getDependantInstances(Product studioContext) throws MojoExecutionException
103     {
104         StudioProperties studioProperties = getStudioProperties(studioContext);
105         studioProperties.setStudioProduct(studioContext);
106         List<String> instanceIds = studioContext.getInstanceIds();
107         if (instanceIds.isEmpty())
108         {
109             instanceIds.add(STUDIO_CROWD);
110             instanceIds.add(STUDIO_JIRA);
111             instanceIds.add(STUDIO_CONFLUENCE);
112             instanceIds.add(STUDIO_FECRU);
113             instanceIds.add(STUDIO_BAMBOO);
114         }
115         return instanceIds;
116     }
117 
118     /**
119      * System property 'studio.instanceIds': If defined, only runs those applications.
120      *
121      * They must be comma-separated, eg: {@code -Dstudio.instanceids=studio-crowd,studio-jira}.
122      * The studio configuration will be built using the pom.xml configuration.
123      */
124     public Set<String> getExcludedInstances(Product studioContext)
125     {
126         String restriction = System.getProperty(LAUNCH_INSTANCES_SYSTEM_PROPERTY);
127         if (restriction == null)
128         {
129             return null;
130         }
131         String[] restrictionList = restriction.split(",");
132         log.info(String.format("Excluding %s from the %s instance.", Arrays.toString(restrictionList), studioContext.getInstanceId()));
133         return Sets.newHashSet(restrictionList);
134     }
135 
136     /**
137      * Prepares the studio home. Does not start any application.
138      *
139      */
140     @Override
141     public int start(Product ctx) throws MojoExecutionException
142     {
143         // Sanity check
144         sanityCheck(ctx);
145 
146         // Launch the product
147         createStudioHome(ctx);
148 
149         // The symlink is pretty much constrained
150         // - must be in /target (the work dir for Bamboo)
151         // - must be 2 levels up from the studio home
152         File symlink = new File(context.getProject().getBuild().getDirectory(), "svn");
153         if (!symlink.exists())
154         {
155             // Create a symlink so that Bamboo can work
156             createSymlink(ctx.getStudioProperties().getSvnRoot(), symlink);
157         }
158 
159         return 0;
160     }
161 
162     /**
163      * Checks the configuration to throw exceptions early for the few most common problems
164      */
165     private void sanityCheck(Product studioProduct) throws MojoExecutionException
166     {
167         StudioProperties properties = studioProduct.getStudioProperties();
168         if (properties == null)
169         {
170             throw new MojoExecutionException(String.format("Something went wrong when starting %s. The 'studio' handler was not initialised propertly.",
171                     studioProduct.getInstanceId()));
172         }
173         if (properties.getCrowd() == null || properties.getCrowd().getStudioProperties() == null)
174         {
175             log.error(String.format(
176                     "You won't be able to run %s, Studio-Crowd was not configured properly.", studioProduct.getInstanceId()));
177         }
178         if (properties.isJiraEnabled() && (properties.getJira() == null || properties.getJira().getStudioProperties() == null))
179         {
180             log.error(String.format(
181                     "You won't be able to run %s, Studio-JIRA was not configured properly.", studioProduct.getInstanceId()));
182         }
183         if (properties.isConfluenceEnabled() && (properties.getConfluence() == null || properties.getConfluence().getStudioProperties() == null))
184         {
185             log.error(String.format(
186                     "You won't be able to run %s, Studio-Confluence was not configured properly.", studioProduct.getInstanceId()));
187         }
188         if (properties.isFisheyeEnabled() && (properties.getFisheye() == null || properties.getFisheye().getStudioProperties() == null))
189         {
190             log.error(String.format(
191                     "You won't be able to run %s, Studio-Fisheye was not configured properly.", studioProduct.getInstanceId()));
192         }
193         if (properties.isBambooEnabled() && (properties.getBamboo() == null || properties.getBamboo().getStudioProperties() == null))
194         {
195             log.error(String.format(
196                     "You won't be able to run %s, Studio-Bamboo was not configured properly.", studioProduct.getInstanceId()));
197         }
198     }
199 
200     @Override
201     public void stop(Product ctx) throws MojoExecutionException
202     {
203         // Delete the symlink so that the mvn clean:clean works properly
204         File symlink = new File(context.getProject().getBuild().getDirectory(), "svn");
205         symlink.deleteOnExit();
206 
207         // Nothing to stop
208     }
209 
210     @Override
211     public int getDefaultHttpPort()
212     {
213         // No default - this product can't be launched
214         return 0;
215     }
216 
217     /**
218      * Does nothing for non-studios products.
219      * For Studio products, defaults the studio-specific properties.
220      *
221      * @param product
222      *            a product. All products are accepted but not all of the will be
223      *            modified. The product must have an instanceId.
224      */
225     public static void setDefaultValues(Product product)
226     {
227         String defaultContextPath = defaultContextPaths.get(product.getId());
228         if (defaultContextPath != null)
229         {
230             // It's a Studio product
231             if (product.getOutput() == null)
232             {
233                 product.setOutput("target/" + product.getInstanceId() + ".log");
234             }
235             if (product.getContextPath() == null)
236             {
237                 product.setContextPath(defaultContextPath);
238             }
239             if (product.getVersion() == null)
240             {
241                 // This value will be replaced with the version given by the studio product.
242                 // We can't leave it empty because the value will be defaulted to RELEASE.
243                 product.setVersion(STUDIO_VERSION_TOKEN);
244             }
245         }
246     }
247 
248     /**
249      * Requests the Studio instance to configure its fellow products (home directory, ...)
250      *
251      * Not thread safe.
252      *
253      * @param studioContext
254      *            the studio instance. There may be several Studio instances, so the products should be configured
255      *            with this instance in mind.
256      *
257      * @param dependantProducts
258      *            the list of products running 'in' this instance of studio (same home & applinked).
259      *            The client should guarantee it calls this method once and only once for all the product on one
260      *            Studio instance.
261      * @throws MojoExecutionException
262      */
263     public void configure(Product studioContext, List<ProductExecution> dependantProducts) throws MojoExecutionException
264     {
265         StudioProperties studioProperties = getStudioProperties(studioContext);
266 
267         boolean confluenceStandalone = true;
268 
269         // Sets properties for each product
270         for (ProductExecution execution : dependantProducts)
271         {
272             // Each product provides some configuration info
273             Product product = execution.getProduct();
274             if (STUDIO_CROWD.equals(product.getId()))
275             {
276                 studioProperties.setCrowd(product);
277             }
278             else if (STUDIO_CONFLUENCE.equals(product.getId()))
279             {
280                 studioProperties.setConfluence(product);
281             }
282             else if (STUDIO_JIRA.equals(product.getId()))
283             {
284                 studioProperties.setJira(product);
285                 confluenceStandalone = false;
286             }
287             else if (STUDIO_FECRU.equals(product.getId()))
288             {
289                 studioProperties.setFisheye(product);
290                 confluenceStandalone = false;
291             }
292             else if (STUDIO_BAMBOO.equals(product.getId()))
293             {
294                 studioProperties.setBamboo(product);
295                 confluenceStandalone = false;
296             }
297             else
298             {
299                 throw new MojoExecutionException("A non-studio product was listed in a Studio instance: " + product.getInstanceId());
300             }
301 
302             studioProperties.setModeConfluenceStandalone(confluenceStandalone);
303 
304             // And share the bean between all products
305             product.setStudioProperties(studioProperties);
306 
307             // Set the common Studio version
308             if (STUDIO_VERSION_TOKEN.equals(product.getVersion()))
309             {
310                 product.setVersion(studioProperties.getVersion());
311             }
312         }
313     }
314 
315     /**
316      * Return the studio properties. If it doesn't exist, create the bean.
317      * Not thread safe.
318      *
319      * @param studioContext
320      *            the Studio product
321      * @return the properties, never null.
322      */
323     private static StudioProperties getStudioProperties(Product studioContext)
324     {
325         StudioProperties properties = studioContext.getStudioProperties();
326         if (properties == null)
327         {
328             properties = new StudioProperties(studioContext);
329             studioContext.setStudioProperties(properties);
330         }
331         return properties;
332     }
333 
334     /**
335      * Studio returns the parent of studio-home, to ship other application's homes:
336      * <ul>
337      * <li>studioInstance <b>(&lt;- the snapshot)</b></li>
338      * <li>studioInstance/confluence-home</li>
339      * <li>studioInstance/jira-home</li>
340      * <li>studioInstance/...</li>
341      * <li>studioInstance/home <b>(&lt;- the home)</b></li>
342      * <ul>
343      */
344     @Override
345     public File getSnapshotDirectory(Product studio)
346     {
347         return getHomeDirectory(studio).getParentFile();
348     }
349 
350 
351     /**
352      * Fills the properties with the studio configuration.
353      *
354      * If the studio1-home directory does not exist, creates it and fills it with the right contents.
355      * If this studio1-home exists, do not change the contents
356      *
357      * This method must be guaranteed to be called:
358      * <ul>
359      * <li>Exactly once for this StudioProperties bean.</li>
360      * <li>After {@link #configure(Product, List)}.</li>
361      * <li>Before any product's home is created or any product is started</li>
362      * </ul>
363      *
364      * <p>
365      * It also adds the svn home and the webdav home. The final tree is:
366      * <ul>
367      * <li>studioInstance1
368      * <ul>
369      * <li>home</li>
370      * <li>svn-home</li>
371      * <li>webdav-home</li>
372      * </ul>
373      * </li>
374      * </ul>
375      * </p>
376      *
377      * @param studio
378      *            the Studio properties. Must not be null.
379      * @param buildirectory
380      *            the base directory (you can obtain it using ((MavenProject)project).getBuild().getDirectory())
381      * @throws MojoExecutionException
382      *
383      */
384     // This method reproduces PrepareStudioMojo.groovy
385     public void createStudioHome(Product studio) throws MojoExecutionException
386     {
387         StudioProperties properties = getStudioProperties(studio);
388 
389         // All homes are exported, including the studioInstanceId/home
390         File studioHomeDir = getHomeDirectory(studio);
391         File studioCommonsDir = studioHomeDir.getParentFile();
392         String studioProductDataOrigin = studioCommonsDir.getAbsolutePath();
393 
394         // Extracts the zip / copies the homes to studioInstanceId/
395         if (!studioHomeDir.exists())
396         {
397             extractHome(studioCommonsDir, studio);
398             if (!studioHomeDir.exists())
399             {
400                 throw new MojoExecutionException(studioProductDataOrigin + "studio-test-resources.zip must contain a 'xx/xx/home' folder");
401             }
402         }
403 
404         File svnHomeDir = new File(studioCommonsDir, "svn-home");
405         if (!svnHomeDir.exists())
406         {
407             throw new MojoExecutionException(studioProductDataOrigin + " must contain a 'xx/xx/svn-home' folder");
408         }
409 
410         File webDavDir = new File(studioCommonsDir, "webdav-home");
411         if (!webDavDir.exists())
412         {
413             throw new MojoExecutionException(studioProductDataOrigin + " must contain a 'xx/xx/webdav-home' folder");
414         }
415 
416         String svnPublicUrl;
417         if (System.getProperty("os.name").toLowerCase(Locale.ENGLISH).contains("windows"))
418         {
419             svnPublicUrl = "file:///" + svnHomeDir.getAbsolutePath();
420         }
421         else
422         {
423             svnPublicUrl = "file://" + svnHomeDir.getAbsolutePath();
424         }
425 
426         properties.setStudioHome(studioHomeDir.getAbsolutePath());
427         properties.setSvnRoot(svnHomeDir.getAbsolutePath());
428         properties.setSvnPublicUrl(svnPublicUrl);
429         properties.setWebDavHome(webDavDir.getAbsolutePath());
430 
431         // Parametrise the files
432         parameteriseFiles(studioCommonsDir, studio);
433 
434         // Set the system properties
435         properties.overrideSystemProperty("studio.home", studioHomeDir.getAbsolutePath());
436         properties.overrideSystemProperty("studio.initial.data.xml", new File(studioCommonsDir, STUDIO_INITIAL_DATA_XML).getAbsolutePath());
437         properties.overrideSystemProperty("studio.initial.data.properties", new File(studioCommonsDir, STUDIO_INITIAL_DATA_PROPERTIES).getAbsolutePath());
438         properties.overrideSystemProperty("studio.hal.instance.uri", new File(studioCommonsDir, DEVMODE_HAL_LICENSES_XML).getAbsolutePath());
439 
440         // Sets the home data for the products - we don't override productDataVersion because we
441         // won't ship separate studio versions of homes.
442         Product crowd = properties.getCrowd();
443         if (crowd.getDataPath() == null)
444         {
445             crowd.setDataPath(new File(studioCommonsDir, "crowd-home").getAbsolutePath());
446         }
447 
448         Product confluence = properties.getConfluence();
449         if (confluence != null && confluence.getDataPath() == null)
450         {
451             confluence.setDataPath(new File(studioCommonsDir, "confluence-home").getAbsolutePath());
452         }
453 
454         Product jira = properties.getJira();
455         if (jira != null && jira.getDataPath() == null)
456         {
457             jira.setDataPath(new File(studioCommonsDir, "jira-home").getAbsolutePath());
458         }
459 
460         Product bamboo = properties.getBamboo();
461         if (bamboo != null && bamboo.getDataPath() == null)
462         {
463             bamboo.setDataPath(new File(studioCommonsDir, "bamboo-home").getAbsolutePath());
464         }
465 
466         Product fecru = properties.getFisheye();
467         if (fecru != null && fecru.getDataPath() == null)
468         {
469             fecru.setDataPath(new File(studioCommonsDir, "fecru-home").getAbsolutePath());
470         }
471     }
472 
473     private void parameteriseFiles(File studioSnapshotCopyDir, Product studio) throws MojoExecutionException
474     {
475         ConfigFileUtils.replace(getConfigFiles(studio, studioSnapshotCopyDir), getReplacements(studio), false, log);
476     }
477 
478     @Override
479     public List<File> getConfigFiles(Product studio, File studioSnapshotDir)
480     {
481         List<File> list = Lists.newArrayList();
482         list.add(new File(studioSnapshotDir, STUDIO_PROPERTIES));
483         list.add(new File(studioSnapshotDir, STUDIO_INITIAL_DATA_PROPERTIES));
484         list.add(new File(studioSnapshotDir, STUDIO_INITIAL_DATA_XML));
485         list.add(new File(studioSnapshotDir, DEVMODE_HAL_LICENSES_XML));
486         return list;
487     }
488 
489     /**
490      * Both used to unzip and rezip the home
491      */
492     @Override
493     public List<Replacement> getReplacements(final Product studio)
494     {
495         List<Replacement> replacements = super.getReplacements(studio);
496         replacements.addAll(new ArrayList<Replacement>()
497         {
498             private void putNonReversibleIfNotNull(String key, String value)
499             {
500                 if (value != null)
501                 {
502                     add(new Replacement(key, value, false));
503                 }
504             }
505 
506             private void putIfNotNull(String key, String value)
507             {
508                 if (value != null)
509                 {
510                     add(new Replacement(key, value));
511                 }
512             }
513 
514             // Static bloc for the anonymous subclass of ArrayList
515             {
516                 StudioProperties properties = studio.getStudioProperties();
517                 putIfNotNull("%GREENHOPPER-LICENSE%", "test-classes/greenhopper.license");
518 
519                 if (properties.isJiraEnabled())
520                 {
521                     File attachmentsFolder = new File(getHomeDirectory(properties.getJira()), "attachments");
522                     putIfNotNull("%JIRA-ATTACHMENTS%", attachmentsFolder.getAbsolutePath());
523                     putIfNotNull("%JIRA-BASE-URL%", properties.getJiraUrl());
524                     putIfNotNull("%JIRA-HOST-URL%", properties.getJiraHostUrl());
525                     putIfNotNull("%JIRA-CONTEXT%", properties.getJiraContextPath());
526                 }
527 
528                 if (properties.isConfluenceEnabled())
529                 {
530                     putIfNotNull("%CONFLUENCE-BASE-URL%", properties.getConfluenceUrl());
531                     putIfNotNull("%CONFLUENCE-HOST-URL%", properties.getConfluenceHostUrl());
532                     putIfNotNull("%CONFLUENCE-CONTEXT%", properties.getConfluenceContextPath());
533                 }
534 
535                 if (properties.isFisheyeEnabled())
536                 {
537                     putIfNotNull("%FISHEYE-BASE-URL%", properties.getFisheyeUrl());
538                     putIfNotNull("%FISHEYE-HOST-URL%", properties.getFisheyeHostUrl());
539                     putIfNotNull("%FISHEYE-CONTROL-PORT%", properties.getFisheyeControlPort());
540                     putNonReversibleIfNotNull("%FISHEYE-CONTEXT%", properties.getFisheyeContextPath());
541                     putIfNotNull("%FISHEYE-SHUTDOWN-ENABLED%", properties.getFisheyeShutdownEnabled());
542                 }
543 
544                 if (properties.isBambooEnabled())
545                 {
546                     putIfNotNull("%BAMBOO-BASE-URL%", properties.getBambooUrl());
547                     putIfNotNull("%BAMBOO-HOST-URL%", properties.getBambooHostUrl());
548                     putIfNotNull("%BAMBOO-CONTEXT%", properties.getBambooContextPath());
549                     putNonReversibleIfNotNull("%BAMBOO-ENABLED%", "true");
550                 }
551                 else
552                 {
553                     putNonReversibleIfNotNull("%BAMBOO-ENABLED%", "false");
554                 }
555 
556                 putIfNotNull("%CROWD-BASE-URL%", properties.getCrowdUrl());
557                 putIfNotNull("%CROWD-HOST-URL%", properties.getCrowdHostUrl());
558                 putIfNotNull("%CROWD-CONTEXT%", properties.getCrowdContextPath());
559 
560                 putIfNotNull("%SVN-BASE-URL%", properties.getSvnRoot());
561                 putIfNotNull("%SVN-PUBLIC-URL%", properties.getSvnPublicUrl());
562                 putIfNotNull("%SVN-HOOKS%", properties.getSvnHooks());
563 
564                 putNonReversibleIfNotNull("%STUDIO-DATA-LOCATION%", "");
565                 putIfNotNull("%STUDIO-HOME%", properties.getStudioHome());
566                 putNonReversibleIfNotNull("%GAPPS-ENABLED%", Boolean.toString(properties.isGappsEnabled()));
567                 if (properties.isGappsEnabled())
568                 {
569                     putNonReversibleIfNotNull("%GAPPS-ENABLED%", Boolean.toString(true));
570                     putIfNotNull("%STUDIO-GAPPS-DOMAIN%", properties.getGappsDomain());
571                 }
572                 else
573                 {
574                     putNonReversibleIfNotNull("%GAPPS-ENABLED%", Boolean.toString(false));
575                 }
576                 putIfNotNull("%STUDIO-WEBDAV-DIRECTORY%", properties.getWebDavHome());
577                 putIfNotNull("%STUDIO-SVN-IMPORT-STAGING-DIRECTORY%", properties.getSvnImportStagingDirectory());
578             }
579         });
580         return replacements;
581     }
582 
583     private void createSymlink(String source, File target) throws MojoExecutionException
584     {
585         String[] systemCommand = {
586                 "ln",
587                 "-s",
588                 source,
589                 target.getAbsolutePath()
590         };
591         try
592         {
593 
594             Process symlinkCreation = Runtime.getRuntime().exec(systemCommand);
595 
596             // In case of errors, write the message and wait for the user to acknowledge
597             BufferedReader errorStream = new BufferedReader(
598                     new InputStreamReader(symlinkCreation.getErrorStream()));
599             String errorLine = null;
600             boolean hasErrors = false;
601             while ((errorLine = errorStream.readLine()) != null)
602             {
603                 if (!hasErrors)
604                 {
605                     System.err.println("Error while executing " + systemCommand + ": ");
606                     hasErrors = true;
607                 }
608                 System.err.println(errorLine);
609             }
610             if (hasErrors)
611             {
612                 System.out.println("Please execute this command in your command line and press a key to continue");
613                 System.in.read();
614             }
615         }
616         catch (IOException e)
617         {
618             throw new MojoExecutionException("Could not create the symlink: " + systemCommand, e);
619         }
620     }
621 
622     /**
623      * Copies/Extracts the data into parent/directoryName
624      * @throws MojoExecutionException
625      */
626     private void extractHome(File target, Product studio) throws MojoExecutionException
627     {
628         // Take whichever is provided by the user (dataPath or productDataVersion zip to download)
629         File testResourcesZip = getProductHomeData(studio);
630 
631         try
632         {
633             if (!testResourcesZip.exists())
634             {
635                 throw new MojoExecutionException(String.format("This source doesn't exist: %s", testResourcesZip.getAbsoluteFile()));
636             }
637             if (testResourcesZip.isDirectory())
638             {
639                 copyDirectory(testResourcesZip, target);
640             }
641             else
642             {
643                 unzip(testResourcesZip, target.getAbsolutePath(), 2);
644             }
645         }
646         catch (IOException ioe)
647         {
648             throw new MojoExecutionException(String.format("Unable to copy/unzip the studio home from %s to %s", testResourcesZip.getAbsolutePath(),
649                     target.getAbsolutePath()), ioe);
650         }
651     }
652 
653     /**
654      * Performs the necessary initialisation for Studio products' homes
655      */
656     static void processProductsHomeDirectory(Log log, Product ctx, File homeDir) throws MojoExecutionException
657     {
658         // Nothing to process in the home.
659         // Just check Studio has been configured.
660         if (ctx.getStudioProperties() == null)
661         {
662             throw new MojoExecutionException(String.format("%s product is dependant on Studio. You must include the Studio product in your execution.",
663                     ctx.getInstanceId()));
664         }
665     }
666 
667     /**
668      * Performs the necessary initialisation for Studio products
669      *
670      * @param log
671      * @param ctx
672      * @param homeDir
673      * @param explodedWarDir
674      * @param crowdPropertiesPath
675      *            the path from the explodedWarDir to the crowd.properties
676      * @throws MojoExecutionException
677      */
678     static void addProductHandlerOverrides(Log log, Product ctx, File homeDir, File explodedWarDir) throws MojoExecutionException
679     {
680         addProductHandlerOverrides(log, ctx, homeDir, explodedWarDir, "WEB-INF/classes/crowd.properties");
681     }
682 
683     /**
684      * Performs the necessary initialisation for Studio products
685      *
686      * @param log
687      * @param ctx
688      * @param homeDir
689      * @param explodedWarDir
690      * @param crowdPropertiesPath
691      *            the path from the explodedWarDir to the crowd.properties
692      * @throws MojoExecutionException
693      */
694     static void addProductHandlerOverrides(Log log, Product ctx, File homeDir, File explodedWarDir, String crowdPropertiesPath) throws MojoExecutionException
695     {
696         File crowdProperties = new File(explodedWarDir, crowdPropertiesPath);
697         if (checkFileExists(crowdProperties, log))
698         {
699             parametriseCrowdFile(crowdProperties, ctx.getStudioProperties().getCrowdUrl(), log);
700         }
701     }
702 
703     /**
704      * Replaces the crowd url in the the crowd.properties of the current application
705      *
706      * @param crowdProperties
707      *            the file "crowd.properties"
708      * @param crowdUrl
709      *            the Crowd url, example: "http://localhost:4990/crowd"
710      * @throws MojoExecutionException
711      *             if an error is encountered during the replacement
712      */
713     private static void parametriseCrowdFile(File crowdProperties, String crowdUrl, Log log) throws MojoExecutionException
714     {
715         List<Replacement> replacements = Lists.newArrayList();
716         replacements.add(new Replacement("%CROWD-INTERNAL-URL%", crowdUrl));
717         replacements.add(new Replacement("%CROWD-URL%", crowdUrl));
718 
719         ConfigFileUtils.replace(crowdProperties, replacements, false, log);
720     }
721 
722 
723     public void cleanupProductHomeForZip(Product studioProduct, File studioHome) throws MojoExecutionException
724     {
725         try
726         {
727             // Get products of this Studio instance
728             StudioProperties studioProperties = studioProduct.getStudioProperties();
729 
730             // The key of this map is the name of the home folder for this application
731             // Unused applications are "null", so they will not be seen in the map
732             Map<String, Product> products = Maps.newHashMap();
733             products.put("crowd-home", studioProperties.getCrowd());
734             products.put("confluence-home", studioProperties.getConfluence());
735             products.put("jira-home", studioProperties.getJira());
736             products.put("fecru-home", studioProperties.getFisheye());
737             products.put("bamboo-home", studioProperties.getBamboo());
738 
739             // Make each product's home
740             Iterator<String> productKeys = products.keySet().iterator();
741             while (productKeys.hasNext())
742             {
743                 String productHomeName = productKeys.next();
744                 Product product = products.get(productHomeName);
745                 if (product != null)
746                 {
747                     File productDestinationDirectory = new File(studioHome, productHomeName);
748                     copyAndCleanProductHome(product, productDestinationDirectory);
749                 }
750             }
751 
752 
753             // Un-parametrise the files
754             super.cleanupProductHomeForZip(studioProduct, studioHome);
755         }
756         catch (IOException ioe)
757         {
758             throw new MojoExecutionException("Could not copy a product home directory.", ioe);
759         }
760 
761     }
762 
763     private void copyAndCleanProductHome(Product product, File productDestinationDirectory) throws IOException, MojoExecutionException
764     {
765         ProductHandler handler = ProductHandlerFactory.create(product.getId(), context, goals);
766         File productHomeDirectory = getHomeDirectory(product);
767 
768         // Delete studio1/{product}-home and replace it with the current product's home
769         if (productDestinationDirectory.exists())
770         {
771             FileUtils.deleteDirectory(productDestinationDirectory);
772         }
773         ProjectUtils.createDirectory(productDestinationDirectory);
774         copyDirectory(productHomeDirectory, productDestinationDirectory);
775 
776         // Request the product to clean up
777         handler.cleanupProductHomeForZip(product, productDestinationDirectory);
778     }
779 
780     static String fixWindowsSlashes(final String path)
781     {
782         return path.replaceAll("\\\\", "/");
783     }
784 
785     static boolean checkFileExists(File file, Log log)
786     {
787         if (!file.exists())
788         {
789             log.warn(String.format("%s does not exist. Will skip customisation", file.getAbsolutePath()));
790             return false;
791         }
792         return true;
793     }
794 
795     /**
796      * Returns the first value which is not null. Useful to set default values
797      *
798      * @param t
799      * @return the first non-null value, or null if all values are null
800      */
801     public static <T> T firstNotNull(T... values)
802     {
803         for (T t : values)
804         {
805             if (t != null)
806             {
807                 return t;
808             }
809         }
810         return null;
811     }
812 }