1   package com.atlassian.maven.plugins.amps.product.studio;
2   
3   import com.atlassian.maven.plugins.amps.MavenContext;
4   import com.atlassian.maven.plugins.amps.MavenGoals;
5   import com.atlassian.maven.plugins.amps.Product;
6   import com.atlassian.maven.plugins.amps.ProductArtifact;
7   import com.atlassian.maven.plugins.amps.product.AmpsProductHandler;
8   import com.atlassian.maven.plugins.amps.product.ProductHandler;
9   import com.atlassian.maven.plugins.amps.product.ProductHandlerFactory;
10  import com.atlassian.maven.plugins.amps.util.ConfigFileUtils;
11  import com.atlassian.maven.plugins.amps.util.ConfigFileUtils.Replacement;
12  import com.atlassian.maven.plugins.amps.util.ProjectUtils;
13  import com.google.common.annotations.VisibleForTesting;
14  import com.google.common.collect.ImmutableMap;
15  import com.google.common.collect.Lists;
16  import com.google.common.collect.Maps;
17  import org.apache.commons.io.FileUtils;
18  import org.apache.commons.io.IOUtils;
19  import org.apache.maven.model.Model;
20  import org.apache.maven.model.io.xpp3.MavenXpp3Reader;
21  import org.apache.maven.plugin.MojoExecutionException;
22  import org.apache.maven.plugin.logging.Log;
23  import org.apache.maven.surefire.shade.org.apache.commons.lang.StringUtils;
24  
25  import java.io.BufferedReader;
26  import java.io.File;
27  import java.io.FileReader;
28  import java.io.IOException;
29  import java.io.InputStreamReader;
30  import java.io.Reader;
31  import java.io.UnsupportedEncodingException;
32  import java.net.URLEncoder;
33  import java.util.ArrayList;
34  import java.util.Collections;
35  import java.util.HashMap;
36  import java.util.List;
37  import java.util.Locale;
38  import java.util.Map;
39  import java.util.Properties;
40  
41  import static com.atlassian.maven.plugins.amps.product.ProductHandlerFactory.STUDIO;
42  import static com.atlassian.maven.plugins.amps.product.ProductHandlerFactory.STUDIO_BAMBOO;
43  import static com.atlassian.maven.plugins.amps.product.ProductHandlerFactory.STUDIO_CONFLUENCE;
44  import static com.atlassian.maven.plugins.amps.product.ProductHandlerFactory.STUDIO_CROWD;
45  import static com.atlassian.maven.plugins.amps.product.ProductHandlerFactory.STUDIO_FECRU;
46  import static com.atlassian.maven.plugins.amps.product.ProductHandlerFactory.STUDIO_JIRA;
47  import static com.atlassian.maven.plugins.amps.util.ZipUtils.unzip;
48  import static org.apache.commons.io.FileUtils.copyDirectory;
49  
50  /**
51   * This product handler is a 'ghost'. It doesn't start a real product, but it prepares the environment
52   * for all studio-based products.
53   * @since 3.6
54   */
55  public class StudioProductHandler extends AmpsProductHandler
56  {
57      private static final String STUDIO_PROPERTIES = "home/studio.properties";
58      private static final String STUDIO_TEST_PROPERTIES = "studiotest.properties";
59      private static final String STUDIO_INITIAL_DATA_PROPERTIES = "home/studio-initial-data.properties";
60      private static final String DEVMODE_HAL_LICENSES_XML = "home/devmode-hal-licenses.xml";
61      private static final String STUDIO_INITIAL_DATA_XML = "home/studio-initial-data.xml";
62  
63      /** This token is used in product's <version> when they want to reuse the Studio product's version */
64      private static final String ONDEMAND_VERSION_TOKEN = "STUDIO-VERSION";
65      private static final String ONDEMAND_GROUP_ID = "com.atlassian.studio";
66  
67  
68      private static final String JIRA_VERSION_KEY = "jira.version";
69      private static final String CONFLUENCE_VERSION_KEY = "confluence.version";
70  
71      private static final Map<String, String> defaultContextPaths = new HashMap<String, String>()
72      {
73          {
74              put(STUDIO_BAMBOO, "/builds");
75              put(STUDIO_CONFLUENCE, "/wiki");
76              put(STUDIO_CROWD, "/crowd");
77              put(STUDIO_FECRU, "/");
78              put(STUDIO_JIRA, "/jira");
79          }
80      };
81  
82      private static final Map<String, Integer> defaultDebugPorts = new HashMap<String, Integer>()
83      {
84          {
85              put(STUDIO_BAMBOO, 5011);
86              put(STUDIO_CONFLUENCE, 5007);
87              put(STUDIO_CROWD, 5003);
88              put(STUDIO_FECRU, 5005);
89              put(STUDIO_JIRA, 5009);
90          }
91      };
92  
93      public StudioProductHandler(MavenContext context, MavenGoals goals)
94      {
95          super(context, goals);
96      }
97  
98  
99  
100     @Override
101     protected ProductArtifact getTestResourcesArtifact()
102     {
103         return new ProductArtifact("com.atlassian.studio", "studio-test-resources");
104     }
105 
106 
107 
108     @Override
109     public String getId()
110     {
111         return STUDIO;
112     }
113 
114     /**
115      * Returns the list of products that are configured in this studio instance, as defined in 'instanceIds'
116      *
117      * @param studioContext
118      *            the Studio product. If not a studio product, returns an empty list.
119      * @return a list of instance ids. Never null.
120      * @throws MojoExecutionException
121      *             if the Studio product is misconfigured
122      */
123     public static List<String> getDependantInstances(Product studioContext) throws MojoExecutionException
124     {
125         if (!STUDIO.equals(studioContext.getId()))
126         {
127             return Collections.emptyList();
128         }
129         StudioProperties studioProperties = getStudioProperties(studioContext);
130         studioProperties.setStudioProduct(studioContext);
131         List<String> instanceIds = studioContext.getInstanceIds();
132         if (instanceIds.isEmpty())
133         {
134             instanceIds.add(STUDIO_CROWD);
135             instanceIds.add(STUDIO_JIRA);
136             instanceIds.add(STUDIO_CONFLUENCE);
137             instanceIds.add(STUDIO_BAMBOO);
138             instanceIds.add(STUDIO_FECRU);
139         }
140         return instanceIds;
141     }
142 
143     /**
144      * Prepares the studio home. Does not start any application.
145      *
146      */
147     @Override
148     public int start(Product ctx) throws MojoExecutionException
149     {
150         // Sanity check
151         sanityCheck(ctx);
152 
153         // Launch the product
154         createStudioHome(ctx);
155 
156         // The symlink is pretty much constrained
157         // - must be in /target (the work dir for Bamboo)
158         // - must be 2 levels up from the studio home
159         File symlink = new File(context.getProject().getBuild().getDirectory(), "svn");
160         if (!symlink.exists())
161         {
162             // Create a symlink so that Bamboo can work
163             createSymlink(ctx.getStudioProperties().getSvnRoot(), symlink);
164         }
165 
166         return 0;
167     }
168 
169     /**
170      * Checks the configuration to throw exceptions early for the few most common problems
171      */
172     private void sanityCheck(Product studioProduct) throws MojoExecutionException
173     {
174         StudioProperties properties = studioProduct.getStudioProperties();
175         if (properties == null)
176         {
177             throw new MojoExecutionException(String.format("Something went wrong when starting %s. The 'studio' handler was not initialised propertly.",
178                     studioProduct.getInstanceId()));
179         }
180         if (properties.getCrowd() == null || properties.getCrowd().getStudioProperties() == null)
181         {
182             log.error(String.format(
183                     "You won't be able to run %s, Studio-Crowd was not configured properly.", studioProduct.getInstanceId()));
184         }
185         if (properties.isJiraEnabled() && (properties.getJira() == null || properties.getJira().getStudioProperties() == null))
186         {
187             log.error(String.format(
188                     "You won't be able to run %s, Studio-JIRA was not configured properly.", studioProduct.getInstanceId()));
189         }
190         if (properties.isConfluenceEnabled() && (properties.getConfluence() == null || properties.getConfluence().getStudioProperties() == null))
191         {
192             log.error(String.format(
193                     "You won't be able to run %s, Studio-Confluence was not configured properly.", studioProduct.getInstanceId()));
194         }
195         if (properties.isFisheyeEnabled() && (properties.getFisheye() == null || properties.getFisheye().getStudioProperties() == null))
196         {
197             log.error(String.format(
198                     "You won't be able to run %s, Studio-Fisheye was not configured properly.", studioProduct.getInstanceId()));
199         }
200         if (properties.isBambooEnabled() && (properties.getBamboo() == null || properties.getBamboo().getStudioProperties() == null))
201         {
202             log.error(String.format(
203                     "You won't be able to run %s, Studio-Bamboo was not configured properly.", studioProduct.getInstanceId()));
204         }
205     }
206 
207     @Override
208     public void stop(Product ctx) throws MojoExecutionException
209     {
210         // Delete the symlink so that the mvn clean:clean works properly
211         File symlink = new File(context.getProject().getBuild().getDirectory(), "svn");
212         symlink.deleteOnExit();
213 
214         // Nothing to stop
215     }
216 
217     @Override
218     public int getDefaultHttpPort()
219     {
220         // No default - this product can't be launched
221         return 0;
222     }
223 
224     /**
225      * Does nothing for non-studios products.
226      * For Studio products, defaults the studio-specific properties.
227      *
228      * @param product
229      *            a product. All products are accepted but not all of the will be
230      *            modified. The product must have an instanceId.
231      */
232     public static void setDefaultValues(MavenContext context, Product product)
233     {
234         // Set the default context path
235         // Amps requires '/' before and not after
236         String defaultContextPath = defaultContextPaths.get(product.getId());
237         if (defaultContextPath != null)
238         {
239             // It's a Studio product
240             if (product.getOutput() == null)
241             {
242                 product.setOutput(new File(context.getProject().getBuild().getDirectory(), product.getInstanceId() + ".log").getAbsolutePath());
243             }
244             if (product.getContextPath() == null)
245             {
246                 product.setContextPath(defaultContextPath);
247             }
248             if (product.getVersion() == null)
249             {
250                 // This value will be replaced with the version given by the studio product.
251                 // We can't leave it empty because the value will be defaulted to RELEASE.
252                 product.setVersion(ONDEMAND_VERSION_TOKEN);
253             }
254 
255             // Set the default debug port
256             if (product.getJvmDebugPort() == 0)
257             {
258                 product.setJvmDebugPort(defaultDebugPorts.get(product.getId()));
259             }
260 
261             // StudioFecru only
262             if (product.getShutdownEnabled() == null)
263             {
264                 product.setShutdownEnabled(Boolean.TRUE);
265             }
266         }
267     }
268 
269     /**
270      * Requests the Studio instance to configure its fellow products (home directory, ...)
271      *
272      * Not thread safe. The client should guarantee it calls this method once and only once for a productMap.
273      *
274      * @param productMap
275      *            The product map of { instanceId -> product }. 
276      * @throws MojoExecutionException
277      */
278     public void configureStudioProducts(Map<String, Product> productMap) throws MojoExecutionException
279     {
280         // Find the Studio product, if any
281         for (Product studioContext : productMap.values())
282         {
283             if (STUDIO.equals(studioContext.getId()))
284             {
285                 StudioProperties studioProperties = getStudioProperties(studioContext);
286 
287                 boolean confluenceStandalone = true;
288                 final OnDemandProductVersions versions = new OnDemandProductVersions(studioContext, studioProperties);
289 
290                 // Sets properties for each product
291                 for (String instanceId : getDependantInstances(studioContext))
292                 {
293                     Product product = productMap.get(instanceId);
294 
295                     // Each product provides some configuration info
296 
297                     // JIRA, Confluence and Bamboo support the parallel startup;
298                     // Crowd must be started synchronously because there's a race condition
299                     // and Fisheye doesn't support parallel startup.
300 
301                     if (STUDIO_CROWD.equals(product.getId()))
302                     {
303                         studioProperties.setCrowd(product);
304                         if (product.getSynchronousStartup() == null)
305                         {
306                             product.setSynchronousStartup(Boolean.TRUE);
307                         }
308                     }
309                     else if (STUDIO_CONFLUENCE.equals(product.getId()))
310                     {
311                         studioProperties.setConfluence(product);
312                         if (product.getSynchronousStartup() == null)
313                         {
314                             product.setSynchronousStartup(studioContext.getSynchronousStartup());
315                         }
316                     }
317                     else if (STUDIO_JIRA.equals(product.getId()))
318                     {
319                         studioProperties.setJira(product);
320                         confluenceStandalone = false;
321                         if (product.getSynchronousStartup() == null)
322                         {
323                             product.setSynchronousStartup(studioContext.getSynchronousStartup());
324                         }
325                     }
326                     else if (STUDIO_FECRU.equals(product.getId()))
327                     {
328                         studioProperties.setFisheye(product);
329                         confluenceStandalone = false;
330                     }
331                     else if (STUDIO_BAMBOO.equals(product.getId()))
332                     {
333                         studioProperties.setBamboo(product);
334                         confluenceStandalone = false;
335                         if (product.getSynchronousStartup() == null)
336                         {
337                             product.setSynchronousStartup(studioContext.getSynchronousStartup());
338                         }
339                     }
340                     else
341                     {
342                         throw new MojoExecutionException("A non-studio product was listed in a Studio instance: " + product.getInstanceId());
343                     }
344 
345                     studioProperties.setModeConfluenceStandalone(confluenceStandalone);
346 
347                     // And share the bean between all products
348                     product.setStudioProperties(studioProperties);
349 
350                     if (ONDEMAND_VERSION_TOKEN.equals(product.getVersion()))
351                     {
352                         product.setVersion(versions.getVersion(product));
353                     }
354                 }
355 
356                 // Sets the paths for non-products
357                 File studioHomeDir = getHomeDirectory(studioContext);
358                 File studioCommonsDir = studioHomeDir.getParentFile();
359                 File svnHomeDir = new File(studioCommonsDir, "svn-home");
360                 File webDavDir = new File(studioCommonsDir, "webdav-home");
361                 String svnPublicUrl;
362                 if (System.getProperty("os.name").toLowerCase(Locale.ENGLISH).contains("windows"))
363                 {
364                     svnPublicUrl = "file:///" + svnHomeDir.getAbsolutePath();
365                     log.warn("Studio is only designed to run on Linux systems.");
366                 }
367                 else
368                 {
369                     svnPublicUrl = "file://" + svnHomeDir.getAbsolutePath();
370                 }
371 
372                 studioProperties.setStudioHome(studioHomeDir.getAbsolutePath());
373                 studioProperties.setSvnRoot(svnHomeDir.getAbsolutePath());
374                 studioProperties.setSvnPublicUrl(svnPublicUrl);
375                 studioProperties.setWebDavHome(webDavDir.getAbsolutePath());
376             }
377         }
378     }
379 
380     /**
381      * Return the studio properties. If it doesn't exist, create the bean.
382      * Not thread safe.
383      *
384      * @param studioContext
385      *            the Studio product
386      * @return the properties, never null.
387      */
388     private static StudioProperties getStudioProperties(Product studioContext)
389     {
390         StudioProperties properties = studioContext.getStudioProperties();
391         if (properties == null)
392         {
393             properties = new StudioProperties(studioContext);
394             studioContext.setStudioProperties(properties);
395         }
396         return properties;
397     }
398 
399     /**
400      * Studio returns the parent of studio-home, to ship other application's homes:
401      * <ul>
402      * <li>studioInstance <b>(&lt;- the snapshot)</b></li>
403      * <li>studioInstance/confluence-home</li>
404      * <li>studioInstance/jira-home</li>
405      * <li>studioInstance/...</li>
406      * <li>studioInstance/home <b>(&lt;- the home)</b></li>
407      * <ul>
408      */
409     @Override
410     public File getSnapshotDirectory(Product studio)
411     {
412         return getHomeDirectory(studio).getParentFile();
413     }
414 
415 
416     /**
417      * Fills the properties with the studio configuration.
418      *
419      * If the studio1-home directory does not exist, creates it and fills it with the right contents.
420      * If this studio1-home exists, do not change the contents
421      *
422      * This method must be guaranteed to be called:
423      * <ul>
424      * <li>Exactly once for this StudioProperties bean.</li>
425      * <li>After {@link #configure(Product, List)}.</li>
426      * <li>Before any product's home is created or any product is started</li>
427      * </ul>
428      *
429      * <p>
430      * It also adds the svn home and the webdav home. The final tree is:
431      * <ul>
432      * <li>studioInstance1
433      * <ul>
434      * <li>home</li>
435      * <li>studio-confluence</li>
436      * <li>...</li>
437      * <li>svn-home</li>
438      * <li>webdav-home</li>
439      * </ul>
440      * </li>
441      * </ul>
442      * </p>
443      *
444      * @param studio
445      *            the Studio properties. Must not be null.
446      * @param buildirectory
447      *            the base directory (you can obtain it using ((MavenProject)project).getBuild().getDirectory())
448      * @throws MojoExecutionException
449      *
450      */
451     // This method reproduces PrepareStudioMojo.groovy
452     public void createStudioHome(Product studio) throws MojoExecutionException
453     {
454         StudioProperties properties = getStudioProperties(studio);
455 
456         // All homes are exported, including the studioInstanceId/home
457         File studioHomeDir = new File(properties.getStudioHome());
458         File studioCommonsDir = studioHomeDir.getParentFile();
459 
460         // Extracts the zip / copies the homes to studioInstanceId/
461         if (!studioHomeDir.exists())
462         {
463             extractHome(studioCommonsDir, studio);
464             if (!studioHomeDir.exists())
465             {
466                 throw new MojoExecutionException("The Studio home zip must contain a '*/*/home' folder");
467             }
468         }
469 
470         File svnHomeDir = new File(properties.getSvnRoot());
471         if (!svnHomeDir.exists())
472         {
473             throw new MojoExecutionException("The Studio home zip must contain a '*/*/svn-home' folder");
474         }
475 
476         File webDavDir = new File(properties.getWebDavHome());
477         if (!webDavDir.exists())
478         {
479             throw new MojoExecutionException("The Studio home zip must contain a '*/*/webdav-home' folder");
480         }
481 
482         // Parametrise the files
483         parameteriseFiles(studioCommonsDir, studio);
484 
485         // Set the system properties
486         properties.overrideSystemProperty("studio.home", studioHomeDir.getAbsolutePath());
487         properties.overrideSystemProperty("studio.initial.data.xml", new File(studioCommonsDir, STUDIO_INITIAL_DATA_XML).getAbsolutePath());
488         properties.overrideSystemProperty("studio.initial.data.properties", new File(studioCommonsDir, STUDIO_INITIAL_DATA_PROPERTIES).getAbsolutePath());
489         properties.overrideSystemProperty("studio.hal.instance.uri", new File(studioCommonsDir, DEVMODE_HAL_LICENSES_XML).getAbsolutePath());
490 
491         // Sets the home data for the products - we don't override productDataVersion because we
492         // won't ship separate studio versions of homes.
493         Product crowd = properties.getCrowd();
494         if (crowd.getDataPath() == null)
495         {
496             crowd.setDataPath(new File(studioCommonsDir, "crowd-home").getAbsolutePath());
497         }
498 
499         Product confluence = properties.getConfluence();
500         if (confluence != null && confluence.getDataPath() == null)
501         {
502             confluence.setDataPath(new File(studioCommonsDir, "confluence-home").getAbsolutePath());
503         }
504 
505         Product jira = properties.getJira();
506         if (jira != null && jira.getDataPath() == null)
507         {
508             jira.setDataPath(new File(studioCommonsDir, "jira-home").getAbsolutePath());
509         }
510 
511         Product bamboo = properties.getBamboo();
512         if (bamboo != null && bamboo.getDataPath() == null)
513         {
514             bamboo.setDataPath(new File(studioCommonsDir, "bamboo-home").getAbsolutePath());
515         }
516 
517         Product fecru = properties.getFisheye();
518         if (fecru != null && fecru.getDataPath() == null)
519         {
520             fecru.setDataPath(new File(studioCommonsDir, "fecru-home").getAbsolutePath());
521         }
522 
523         // Always override files regardless of home directory existing or not
524         overrideAndPatchHomeDir(studioCommonsDir, studio);
525     }
526 
527     private void parameteriseFiles(File studioSnapshotCopyDir, Product studio) throws MojoExecutionException
528     {
529         ConfigFileUtils.replace(getConfigFiles(studio, studioSnapshotCopyDir), getReplacements(studio), false, log);
530     }
531 
532     @Override
533     public List<File> getConfigFiles(Product studio, File studioSnapshotDir)
534     {
535         List<File> list = Lists.newArrayList();
536         list.add(new File(studioSnapshotDir, STUDIO_PROPERTIES));
537         list.add(new File(studioSnapshotDir, STUDIO_INITIAL_DATA_PROPERTIES));
538         list.add(new File(studioSnapshotDir, STUDIO_INITIAL_DATA_XML));
539         list.add(new File(studioSnapshotDir, DEVMODE_HAL_LICENSES_XML));
540         list.add(new File(project.getBuild().getTestOutputDirectory(), STUDIO_TEST_PROPERTIES));
541 
542         list.add(new File(studioSnapshotDir, "fecru-home/config.xml"));
543         list.add(new File(studioSnapshotDir, "confluence-home/database/confluencedb.log"));
544         list.add(new File(studioSnapshotDir, "confluence-home/database/confluencedb.script"));
545         list.add(new File(studioSnapshotDir, "jira-home/database.log"));
546         list.add(new File(studioSnapshotDir, "jira-home/database.script"));
547         list.add(new File(studioSnapshotDir, "bamboo-home/database.log"));
548         list.add(new File(studioSnapshotDir, "bamboo-home/database.script"));
549         return list;
550     }
551 
552     /**
553      * Both used to unzip and rezip the home
554      */
555     @Override
556     public List<Replacement> getReplacements(final Product studio)
557     {
558         List<Replacement> replacements = super.getReplacements(studio);
559         replacements.addAll(new ArrayList<Replacement>()
560         {
561             private void putIfNotNull(String key, String value)
562             {
563                 putIfNotNull(key, value, true);
564             }
565 
566             private void putIfNotNull(String key, String value, boolean reversible)
567             {
568                 if (reversible && StringUtils.isNotBlank(value))
569                 {
570                     add(new Replacement(key, value));
571                 }
572                 else if (value != null)
573                 {
574                     add(new Replacement(key, value, false));
575                 }
576             }
577 
578             // Static bloc for the anonymous subclass of ArrayList
579             {
580                 StudioProperties properties = studio.getStudioProperties();
581                 putIfNotNull("%GREENHOPPER-LICENSE%", "test-classes/greenhopper.license");
582 
583                 if (properties.isJiraEnabled())
584                 {
585                     File attachmentsFolder = new File(getHomeDirectory(properties.getJira()), "attachments");
586                     putIfNotNull("%JIRA-ATTACHMENTS%", attachmentsFolder.getAbsolutePath());
587                     putIfNotNull("%JIRA-BASE-URL%", properties.getJiraUrl());
588                     putIfNotNull("%JIRA-HOST-URL%", properties.getJiraHostUrl());
589                     putIfNotNull("%JIRA-CONTEXT%", properties.getJiraContextPath(), false);
590                 }
591 
592                 if (properties.isConfluenceEnabled())
593                 {
594                     putIfNotNull("%CONFLUENCE-BASE-URL%", properties.getConfluenceUrl());
595                     putIfNotNull("%CONFLUENCE-HOST-URL%", properties.getConfluenceHostUrl());
596                     putIfNotNull("%CONFLUENCE-CONTEXT%", properties.getConfluenceContextPath(), false);
597                 }
598 
599                 if (properties.isFisheyeEnabled())
600                 {
601                     putIfNotNull("%FISHEYE-BASE-URL%", properties.getFisheyeUrl());
602                     putIfNotNull("%FISHEYE-HOST-URL%", properties.getFisheyeHostUrl());
603                     putIfNotNull("%FISHEYE-CONTROL-PORT%", properties.getFisheyeControlPort());
604                     putIfNotNull("%FISHEYE-CONTEXT%", properties.getFisheyeContextPath(), false);
605                     putIfNotNull("%FISHEYE-SHUTDOWN-ENABLED%", String.valueOf(firstNotNull(properties.getFisheyeShutdownEnabled(), Boolean.TRUE)));
606                 }
607 
608                 if (properties.isBambooEnabled())
609                 {
610                     putIfNotNull("%BAMBOO-BASE-URL%", properties.getBambooUrl());
611                     putIfNotNull("%BAMBOO-HOST-URL%", properties.getBambooHostUrl());
612                     putIfNotNull("%BAMBOO-CONTEXT%", properties.getBambooContextPath(), false);
613                     putIfNotNull("%BAMBOO-ENABLED%", "true", false);
614                 }
615                 else
616                 {
617                     putIfNotNull("%BAMBOO-ENABLED%", "false", false);
618                 }
619 
620                 putIfNotNull("%CROWD-BASE-URL%", properties.getCrowdUrl());
621                 putIfNotNull("%CROWD-HOST-URL%", properties.getCrowdHostUrl());
622                 putIfNotNull("%CROWD-CONTEXT%", properties.getCrowdContextPath(), false);
623 
624                 putIfNotNull("%SVN-BASE-URL%", properties.getSvnRoot());
625                 putIfNotNull("%SVN-PUBLIC-URL%", properties.getSvnPublicUrl());
626                 putIfNotNull("%SVN-HOOKS%", properties.getSvnHooks());
627 
628                 putIfNotNull("%STUDIO-DATA-LOCATION%", "", false);
629                 putIfNotNull("%STUDIO-HOME%", properties.getStudioHome());
630                 putIfNotNull("%GAPPS-ENABLED%", Boolean.toString(properties.isGappsEnabled()), false);
631                 if (properties.isGappsEnabled())
632                 {
633                     putIfNotNull("%GAPPS-ENABLED%", Boolean.toString(true), false);
634                     putIfNotNull("%STUDIO-GAPPS-DOMAIN%", properties.getGappsDomain());
635                 }
636                 else
637                 {
638                     putIfNotNull("%GAPPS-ENABLED%", Boolean.toString(false), false);
639                 }
640                 putIfNotNull("%STUDIO-WEBDAV-DIRECTORY%", properties.getWebDavHome());
641                 putIfNotNull("%STUDIO-SVN-IMPORT-STAGING-DIRECTORY%", properties.getSvnImportStagingDirectory());
642 
643                 try
644                 {
645                     putIfNotNull("%SVN_HOME_URL_ENCODED%", URLEncoder.encode(propertiesEncode(properties.getSvnRoot()), "UTF-8"));
646                 }
647                 catch (UnsupportedEncodingException badJvm)
648                 {
649                     throw new RuntimeException("UTF-8 should be supported on any JVM", badJvm);
650                 }
651 
652             }
653         });
654         return replacements;
655     }
656 
657     private void createSymlink(String source, File target) throws MojoExecutionException
658     {
659 
660         if (System.getProperty("os.name").toLowerCase(Locale.ENGLISH).contains("windows"))
661         {
662             log.error("Studio is designed to run on Linux systems. As you can't create a symbolic link for SVN, you " +
663                     "will have problems using SVN, FishEye and Bamboo, and possibly the other products.");
664             return;
665         }
666 
667 
668         String[] systemCommand = {
669                 "ln",
670                 "-s",
671                 source,
672                 target.getAbsolutePath()
673         };
674         try
675         {
676 
677             Process symlinkCreation = Runtime.getRuntime().exec(systemCommand);
678 
679             // In case of errors, write the message and wait for the user to acknowledge
680             BufferedReader errorStream = new BufferedReader(
681                     new InputStreamReader(symlinkCreation.getErrorStream()));
682             String errorLine = null;
683             boolean hasErrors = false;
684             while ((errorLine = errorStream.readLine()) != null)
685             {
686                 if (!hasErrors)
687                 {
688                     System.err.println("Error while executing " + systemCommand + ": ");
689                     hasErrors = true;
690                 }
691                 System.err.println(errorLine);
692             }
693             if (hasErrors)
694             {
695                 System.out.println("Please execute this command in your command line and press a key to continue");
696                 System.in.read();
697             }
698         }
699         catch (IOException e)
700         {
701             throw new MojoExecutionException("Could not create the symlink: " + systemCommand, e);
702         }
703     }
704 
705     /**
706      * Copies/Extracts the data into parent/directoryName
707      * @throws MojoExecutionException
708      */
709     private void extractHome(File target, Product studio) throws MojoExecutionException
710     {
711         // Take whichever is provided by the user (dataPath or productDataVersion zip to download)
712         File testResourcesZip = getProductHomeData(studio);
713 
714         try
715         {
716             if (!testResourcesZip.exists())
717             {
718                 throw new MojoExecutionException(String.format("This source doesn't exist: %s", testResourcesZip.getAbsoluteFile()));
719             }
720             if (testResourcesZip.isDirectory())
721             {
722                 copyDirectory(testResourcesZip, target);
723             }
724             else
725             {
726                 unzip(testResourcesZip, target.getAbsolutePath(), 2);
727             }
728         }
729         catch (IOException ioe)
730         {
731             throw new MojoExecutionException(String.format("Unable to copy/unzip the studio home from %s to %s", testResourcesZip.getAbsolutePath(),
732                     target.getAbsolutePath()), ioe);
733         }
734     }
735 
736     /**
737      * Performs the necessary initialisation for Studio products' homes
738      */
739     static void processProductsHomeDirectory(Log log, Product ctx, File homeDir) throws MojoExecutionException
740     {
741         // Nothing to process in the home.
742         // Just check Studio has been configured.
743         if (ctx.getStudioProperties() == null)
744         {
745             throw new MojoExecutionException(String.format("%s product is dependant on Studio. You must include the Studio product in your execution.",
746                     ctx.getInstanceId()));
747         }
748     }
749 
750     /**
751      * Performs the necessary initialisation for Studio products
752      *
753      * @param log
754      * @param ctx
755      * @param homeDir
756      * @param explodedWarDir
757      * @param crowdPropertiesPath
758      *            the path from the explodedWarDir to the crowd.properties
759      * @throws MojoExecutionException
760      */
761     static void addProductHandlerOverrides(Log log, Product ctx, File homeDir, File explodedWarDir) throws MojoExecutionException
762     {
763         addProductHandlerOverrides(log, ctx, homeDir, explodedWarDir, "WEB-INF/classes/crowd.properties");
764     }
765 
766     /**
767      * Performs the necessary initialisation for Studio products
768      *
769      * @param log
770      * @param ctx
771      * @param homeDir
772      * @param explodedWarDir
773      * @param crowdPropertiesPath
774      *            the path from the explodedWarDir to the crowd.properties
775      * @throws MojoExecutionException
776      */
777     static void addProductHandlerOverrides(Log log, Product ctx, File homeDir, File explodedWarDir, String crowdPropertiesPath) throws MojoExecutionException
778     {
779         File crowdProperties = new File(explodedWarDir, crowdPropertiesPath);
780         if (checkFileExists(crowdProperties, log))
781         {
782             parametriseCrowdFile(crowdProperties, ctx.getStudioProperties().getCrowdUrl(), log);
783         }
784     }
785 
786     /**
787      * Replaces the crowd url in the the crowd.properties of the current application
788      *
789      * @param crowdProperties
790      *            the file "crowd.properties"
791      * @param crowdUrl
792      *            the Crowd url, example: "http://localhost:4990/crowd"
793      * @throws MojoExecutionException
794      *             if an error is encountered during the replacement
795      */
796     private static void parametriseCrowdFile(File crowdProperties, String crowdUrl, Log log) throws MojoExecutionException
797     {
798         List<Replacement> replacements = Lists.newArrayList();
799         replacements.add(new Replacement("%CROWD-INTERNAL-URL%", crowdUrl));
800         replacements.add(new Replacement("%CROWD-URL%", crowdUrl));
801 
802         ConfigFileUtils.replace(crowdProperties, replacements, false, log);
803     }
804 
805 
806     public void cleanupProductHomeForZip(Product studioProduct, File studioHome) throws MojoExecutionException
807     {
808         try
809         {
810             // Get products of this Studio instance
811             StudioProperties studioProperties = studioProduct.getStudioProperties();
812 
813             // The key of this map is the name of the home folder for this application
814             // Unused applications are "null", so they will not be seen in the map
815             Map<String, Product> products = Maps.newHashMap();
816             products.put("crowd-home", studioProperties.getCrowd());
817             products.put("confluence-home", studioProperties.getConfluence());
818             products.put("jira-home", studioProperties.getJira());
819             products.put("fecru-home", studioProperties.getFisheye());
820             products.put("bamboo-home", studioProperties.getBamboo());
821 
822             // Make each product's home
823             for (String productHomeName : products.keySet())
824             {
825                 Product product = products.get(productHomeName);
826                 if (product != null)
827                 {
828                     File productDestinationDirectory = new File(studioHome, productHomeName);
829                     File productHomeDirectory = getHomeDirectory(product);
830 
831                     // Delete studio1/{product}-home and replace it with the current product's home
832                     if (productDestinationDirectory.exists())
833                     {
834                         FileUtils.deleteDirectory(productDestinationDirectory);
835                     }
836                     ProjectUtils.createDirectory(productDestinationDirectory);
837                     copyDirectory(productHomeDirectory, productDestinationDirectory);
838 
839                 }
840             }
841 
842             // Un-parametrise the files
843             super.cleanupProductHomeForZip(studioProduct, studioHome);
844 
845             // Request the products to clean their own files
846             // Do it after Studio cleanup, because Studio will handle "svn-home" in Fecru, which Fecru can't do
847             // (Fecru is not aware of Studio).
848             for (String productHomeName : products.keySet())
849             {
850                 Product product = products.get(productHomeName);
851                 if (product != null)
852                 {
853                     File productDestinationDirectory = new File(studioHome, productHomeName);
854                     ProductHandler handler = ProductHandlerFactory.create(product.getId(), context, goals);
855                     handler.cleanupProductHomeForZip(product, productDestinationDirectory);
856                 }
857             }
858         }
859         catch (IOException ioe)
860         {
861             throw new MojoExecutionException("Could not copy a product home directory.", ioe);
862         }
863     }
864 
865     static boolean checkFileExists(File file, Log log)
866     {
867         if (!file.exists())
868         {
869             log.warn(String.format("%s does not exist. Will skip customisation", file.getAbsolutePath()));
870             return false;
871         }
872         return true;
873     }
874 
875     /**
876      * Returns the first value which is not null. Useful to set default values
877      *
878      * @param <T>
879      * @return the first non-null value, or null if all values are null
880      */
881     public static <T> T firstNotNull(T... values)
882     {
883         for (T t : values)
884         {
885             if (t != null)
886             {
887                 return t;
888             }
889         }
890         return null;
891     }
892 
893     @VisibleForTesting
894     protected Model getOnDemandPomModel(Product ondemand, StudioProperties properties) throws MojoExecutionException
895     {
896         File pomFile = getOnDemandPom(ondemand, properties);
897         return readModel(pomFile);
898     }
899 
900     private Model readModel(File pomFile) throws MojoExecutionException
901     {
902         Reader filePomReader = null;
903         try
904         {
905             filePomReader = new BufferedReader(new FileReader(pomFile));
906             MavenXpp3Reader pomReader = new MavenXpp3Reader();
907             return pomReader.read(filePomReader);
908         }
909         catch (Exception e)
910         {
911             throw new MojoExecutionException("Failed to read ondemand-fireball pom", e);
912         }
913         finally
914         {
915             IOUtils.closeQuietly(filePomReader);
916         }
917     }
918 
919     private File getOnDemandPom(Product ondemand, StudioProperties properties) throws MojoExecutionException
920     {
921         final File baseDir = getBaseDirectory(ondemand);
922         File ondemandPom = new File(baseDir, "ondemand.pom");
923         if (!ondemandPom.exists())
924         {
925             ondemandPom = goals.copyArtifact("ondemand.pom", baseDir, getPomArtifact(properties), "pom");
926         }
927         return ondemandPom;
928     }
929 
930     private ProductArtifact getPomArtifact(StudioProperties properties)
931     {
932         final String version = properties.getVersion();
933         if (isPostOnDemandSplit(version))
934         {
935             return new ProductArtifact(ONDEMAND_GROUP_ID, "ondemand-fireball", version);
936         }
937         else
938         {
939             return new ProductArtifact(ONDEMAND_GROUP_ID, "studio-parent", version);
940         }
941 
942     }
943 
944     private boolean isPostOnDemandSplit(String version)
945     {
946         // first post-split version was 133, but 133-partial-1 is before! :)
947         return getVersionNumber(version) >= 133 && !version.equals("133-partial-1");
948     }
949 
950     private int getVersionNumber(String version)
951     {
952         final int digitsCount = getDigitsCount(version);
953         if (digitsCount == 0)
954         {
955             return 0;
956         }
957         else
958         {
959             return Integer.parseInt(version.substring(0, digitsCount));
960         }
961     }
962 
963     private int getDigitsCount(String version)
964     {
965         for (int i=0; i<version.length(); i++)
966         {
967             if (!Character.isDigit(version.charAt(i)))
968             {
969                 return i;
970             }
971         }
972         return version.length();
973     }
974 
975     /**
976      * Retrieves default product versions from ondemand parent pom.
977      *
978      */
979     private class OnDemandProductVersions
980     {
981         private final Map<String,String> productVersionMappings;
982         private final StudioProperties properties;
983 
984         OnDemandProductVersions(Product ondemand, StudioProperties properties) throws MojoExecutionException
985         {
986             this.properties = properties;
987             this.productVersionMappings = initMappings(ondemand);
988         }
989 
990         String getVersion(Product product)
991         {
992             if (productVersionMappings.containsKey(product.getId()))
993             {
994                 return productVersionMappings.get(product.getId());
995             }
996             else
997             {
998                 // default version is OnDemand version
999                 return properties.getVersion();
1000             }
1001         }
1002 
1003         private Map<String, String> initMappings(Product ondemand) throws MojoExecutionException
1004         {
1005             Model model = getOnDemandPomModel(ondemand, properties);
1006             validate(model);
1007             ImmutableMap.Builder<String,String> builder = ImmutableMap.builder();
1008             builder.put(STUDIO_JIRA, getVersionProperty(JIRA_VERSION_KEY, model.getProperties()));
1009             builder.put(STUDIO_CONFLUENCE, getVersionProperty(CONFLUENCE_VERSION_KEY, model.getProperties()));
1010             return builder.build();
1011         }
1012 
1013 
1014 
1015         private void validate(Model model) throws MojoExecutionException
1016         {
1017             Properties props = model.getProperties();
1018             validatePropertyExists(JIRA_VERSION_KEY, props);
1019             validatePropertyExists(CONFLUENCE_VERSION_KEY, props);
1020         }
1021 
1022         private String getVersionProperty(String key, Properties props)
1023         {
1024             if (props.containsKey(key))
1025             {
1026                 return props.getProperty(key);
1027             }
1028             else
1029             {
1030                 return properties.getVersion();
1031             }
1032         }
1033 
1034         private void validatePropertyExists(String key, Properties props)
1035         {
1036             if (!props.containsKey(key))
1037             {
1038                 context.getLog().warn("Expected property '" + key + "' in the OnDemand fireball POM (version "
1039                         + properties.getVersion() + ") not found. OnDemand version will be used instead");
1040             }
1041         }
1042 
1043 
1044     }
1045 
1046 }