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