1   package com.atlassian.maven.plugins.amps.product;
2   
3   import static com.atlassian.core.util.FileUtils.createZipFile;
4   import static com.atlassian.maven.plugins.amps.util.ConfigFileUtils.replace;
5   import static com.atlassian.maven.plugins.amps.util.ZipUtils.unzip;
6   
7   import java.io.File;
8   import java.io.IOException;
9   import java.net.Socket;
10  import java.util.ArrayList;
11  import java.util.Arrays;
12  import java.util.Collection;
13  import java.util.Collections;
14  import java.util.List;
15  import java.util.Map;
16  
17  import org.apache.commons.io.FileUtils;
18  import org.apache.maven.plugin.MojoExecutionException;
19  import org.apache.maven.plugin.logging.Log;
20  import org.apache.maven.project.MavenProject;
21  import org.apache.tools.ant.taskdefs.Java;
22  import org.apache.tools.ant.types.Path;
23  
24  import com.atlassian.maven.plugins.amps.MavenGoals;
25  import com.atlassian.maven.plugins.amps.Product;
26  import com.atlassian.maven.plugins.amps.ProductArtifact;
27  import com.atlassian.maven.plugins.amps.util.ant.AntJavaExecutorThread;
28  import com.atlassian.maven.plugins.amps.util.ant.JavaTaskFactory;
29  
30  import static com.atlassian.maven.plugins.amps.util.ant.JavaTaskFactory.*;
31  
32  public class FeCruProductHandler extends AbstractProductHandler
33  {
34      private static final int STARTUP_CHECK_DELAY = 1000;
35      private static final int STARTUP_CHECK_MAX = 1000 * 60 * 3; //todo is 3 mins enough?
36      private final PluginProvider pluginProvider = new FeCruPluginProvider();
37      private final Log log;
38      private final JavaTaskFactory javaTaskFactory;
39      
40      public FeCruProductHandler(MavenProject project, MavenGoals goals, Log log)
41      {
42          super(project, goals);
43          this.log = log;
44          this.javaTaskFactory = new JavaTaskFactory(log);
45      }
46  
47      public String getId()
48      {
49          return ProductHandlerFactory.FECRU;
50      }
51  
52      public int getDefaultHttpPort()
53      {
54          return 3990;
55      }
56  
57      public int start(Product ctx) throws MojoExecutionException
58      {
59          if (ctx.getJvmArgs() == null)
60          {
61              ctx.setJvmArgs("-Xmx512m -XX:MaxPermSize=160m");
62          }
63  
64          extractAndProcessHomeDirectory(ctx);
65          addArtifacts(ctx);
66  
67          // add application overrides
68          try
69          {
70              addOverrides(ctx);
71          }
72          catch (IOException e)
73          {
74              throw new MojoExecutionException("Unable to override app files using src/test/resources/" + ctx.getInstanceId() + "-app", e);
75          }
76  
77          log.info("Starting " + ctx.getInstanceId() + " on ports "
78                  + ctx.getHttpPort() + " (http) and " + controlPort(ctx.getHttpPort()) + " (control)");
79  
80          AntJavaExecutorThread thread;
81          try
82          {
83              thread = execFishEyeCmd("run", ctx);
84          }
85          catch (Exception e)
86          {
87              throw new MojoExecutionException("Error starting fisheye.", e);
88          }
89  
90          waitForFishEyeToStart(ctx, thread);
91          
92          return ctx.getHttpPort();
93      }
94  
95      private void addOverrides(Product ctx) throws IOException
96      {
97          final File srcDir = new File(project.getBasedir(), "src/test/resources/" + ctx.getInstanceId() + "-app");
98          if (srcDir.exists() && getHomeDirectory(ctx).exists())
99          {
100             FileUtils.copyDirectory(srcDir, getHomeDirectory(ctx));
101         }
102     }
103 
104     private void waitForFishEyeToStart(Product ctx, AntJavaExecutorThread thread) throws MojoExecutionException
105     {
106         boolean connected = false;
107         int waited = 0;
108         while (!connected)
109         {
110             try
111             {
112                 Thread.sleep(STARTUP_CHECK_DELAY);
113             }
114             catch (InterruptedException e)
115             {
116                 // ignore
117             }
118             try
119             {
120                 new Socket("localhost", ctx.getHttpPort()).close();
121                 connected = true;
122             }
123             catch (IOException e)
124             {
125                 // ignore
126             }
127 
128             if (thread.isFinished())
129             {
130                 throw new MojoExecutionException("Fisheye failed to start.", thread.getBuildException());
131             }
132             
133             if (waited++ * STARTUP_CHECK_DELAY > STARTUP_CHECK_MAX)
134             {
135                 throw new MojoExecutionException("FishEye took longer than " + STARTUP_CHECK_MAX + "ms to start!");
136             }
137         }
138     }
139 
140     public void stop(Product ctx) throws MojoExecutionException
141     {
142         log.info("Stopping " + ctx.getInstanceId() + " on ports "
143                 + ctx.getHttpPort() + " (http) and " + controlPort(ctx.getHttpPort()) + " (control)");
144         try
145         {
146             execFishEyeCmd("stop", ctx);
147         }
148         catch (Exception e)
149         {
150             throw new MojoExecutionException("Failed to stop FishEye/Crucible instance at " + ctx.getServer() + ":" + ctx.getHttpPort());
151         }
152         
153         waitForFishEyeToStop(ctx);
154     }
155 
156     private void waitForFishEyeToStop(Product ctx) throws MojoExecutionException
157     {
158         boolean connected = true;
159         int waited = 0;
160         while (connected)
161         {
162             try
163             {
164                 Thread.sleep(STARTUP_CHECK_DELAY);
165             }
166             catch (InterruptedException e)
167             {
168                 // ignore
169             }
170             try
171             {
172                 new Socket("localhost", ctx.getHttpPort()).close();
173             }
174             catch (IOException e)
175             {
176                 connected = false;
177             }
178 
179             if (waited++ * STARTUP_CHECK_DELAY > STARTUP_CHECK_MAX)
180             {
181                 throw new MojoExecutionException("FishEye took longer than " + STARTUP_CHECK_MAX + "ms to stop!");
182             }
183         }
184     }
185 
186     private AntJavaExecutorThread execFishEyeCmd(String bootCommand, Product ctx) throws MojoExecutionException
187     {
188         final Map<String, String> properties = mergeSystemProperties(ctx);
189 
190         Java java = javaTaskFactory.newJavaTask(output(ctx.getOutput()).systemProperties(properties).jvmArgs(ctx.getJvmArgs()));
191 
192         Path classpath = java.createClasspath();
193         classpath.createPathElement().setLocation(new File(getHomeDirectory(ctx), "fisheyeboot.jar"));
194         
195         java.setClassname("com.cenqua.fisheye.FishEyeCtl");
196         
197         java.createArg().setValue(bootCommand);
198         
199         AntJavaExecutorThread javaThread = new AntJavaExecutorThread(java);
200         javaThread.start();
201 
202         return javaThread;
203     }
204 
205     private File getBuildDirectory()
206     {
207         return new File(project.getBuild().getDirectory());
208     }
209 
210     /**
211      * Unpack the application and its sample home diectory from the two artifact zip files.
212      * If the application already exists this isn't repeated, so you can keep your data until atlas-clean is run.
213      *
214      * @param ctx
215      * @throws MojoExecutionException
216      */
217     private void extractAndProcessHomeDirectory(Product ctx) throws MojoExecutionException
218     {
219         final File homeDir = getHomeDirectory(ctx);
220         final File varDirectory = new File(homeDir, "var");
221         if (!varDirectory.exists()) {
222             final File cruDistZip = goals.copyDist(getBuildDirectory(),
223                     new ProductArtifact(
224                             "com.atlassian.crucible",
225                             "atlassian-crucible",
226                             ctx.getVersion()));
227     
228             final File ampsDistZip = goals.copyHome(getBuildDirectory(),
229                     new ProductArtifact(
230                             "com.atlassian.fecru",
231                             "amps-fecru",
232                             ctx.getDataVersion()));
233     
234             createDirectory(homeDir);
235             try
236             {
237                 unzip(cruDistZip, homeDir.getPath(), 1);
238                 unzip(ampsDistZip, homeDir.getPath());
239             }
240             catch (final IOException ex)
241             {
242                 throw new MojoExecutionException("Unable to extract ZIP artifacts into home directory", ex);
243             }
244 
245             //setup config.xml, ports, test repos, whatever
246             final File configXml = new File(homeDir, "config.xml");
247             replace(configXml, "@CONTROL_BIND@", String.valueOf(controlPort(ctx.getHttpPort())));
248             replace(configXml, "@HTTP_BIND@", String.valueOf(ctx.getHttpPort()));
249             replace(configXml, "@HTTP_CONTEXT@", String.valueOf(ctx.getContextPath()));
250             replace(configXml, "@HOME_DIR@", String.valueOf(homeDir.getAbsolutePath()));
251             replace(configXml, "@SITE_URL@", String.valueOf(siteUrl(ctx)));
252         } else {
253             log.info("Using existing FishEye/Crucible application and instance data.");
254         }
255     }
256 
257     private String siteUrl(Product ctx)
258     {
259         return "http://" + ctx.getServer() + ":" + ctx.getHttpPort() + ctx.getContextPath();
260     }
261 
262     private List<ProductArtifact> getPluginsArtifacts(final Product ctx)
263     {
264         final List<ProductArtifact> artifacts = new ArrayList<ProductArtifact>();
265         //artifacts.addAll(getDefaultPlugins());
266         artifacts.addAll(ctx.getPluginArtifacts());
267 
268         if (ctx.getSalVersion() != null)
269         {
270             artifacts.add(new ProductArtifact("com.atlassian.sal", "sal-api", ctx.getSalVersion()));
271             artifacts.add(new ProductArtifact("com.atlassian.sal", "sal-fisheye-plugin", ctx.getSalVersion()));
272         }
273 
274         if (ctx.getPdkVersion() != null)
275         {
276             artifacts.add(new ProductArtifact("com.atlassian.pdkinstall", "pdkinstall-plugin", ctx.getPdkVersion()));
277         }
278 
279         if (ctx.getRestVersion() != null)
280         {
281             artifacts.add(new ProductArtifact("com.atlassian.plugins.rest", "atlassian-rest-module", ctx.getRestVersion()));
282         }
283 
284         return artifacts;
285     }
286 
287     private void createDirectory(File dir) throws MojoExecutionException
288     {
289         if (!dir.exists() && !dir.mkdirs()) {
290                 throw new MojoExecutionException("Failed to create directory " + dir.getAbsolutePath());
291         }
292     }
293 
294     //todo has alot in common with the AbstractWebappProductHandler
295     private void addArtifacts(final Product ctx) throws MojoExecutionException
296     {
297         try
298         {
299             File homeDirectory = getHomeDirectory(ctx);
300             final File pluginsDir = new File(homeDirectory, "var/plugins");
301             createDirectory(pluginsDir);
302             final File bundledPluginsDir = new File(pluginsDir, "bundled");
303             createDirectory(bundledPluginsDir);
304             final File userPluginsDir = new File(pluginsDir, "user");
305             createDirectory(userPluginsDir);
306 
307             // add bundled plugins (todo these are already part of the dist.. this step is possibly unnecessary?)
308             final File bundledPluginsZip = new File(homeDirectory, "plugins/bundled-plugins.zip");
309             if (bundledPluginsZip.exists())
310             {
311                 unzip(bundledPluginsZip, bundledPluginsDir.getPath());
312             }
313 
314             if (isStaticPlugin())
315             {
316                 throw new MojoExecutionException("According to your atlassian-plugin.xml file, this plugin is not " +
317                         "atlassian-plugins version 2. FishEye / Crucible currently only supports atlassian-plugins " +
318                         "version 2.");
319             }
320 
321             // add this plugin itself if enabled
322 
323             if (ctx.isInstallPlugin())
324             {
325                 addThisPluginToDirectory(userPluginsDir);
326                 addTestPluginToDirectory(userPluginsDir);
327             }
328 
329             // add plugins2 plugins
330             addArtifactsToDirectory(pluginProvider.provide(ctx), userPluginsDir);
331             addArtifactsToDirectory(getPluginsArtifacts(ctx), userPluginsDir);
332 
333             List<ProductArtifact> artifacts = new ArrayList<ProductArtifact>();
334             //artifacts.addAll(getDefaultLibPlugins()); -- we don't support plugins 1
335             artifacts.addAll(ctx.getLibArtifacts());
336             addArtifactsToDirectory(artifacts, new File(homeDirectory, "lib"));
337 
338             artifacts = new ArrayList<ProductArtifact>();
339             //artifacts.addAll(getDefaultBundledPlugins()); -- todo default bundled plugins?
340             artifacts.addAll(ctx.getBundledArtifacts());
341 
342             addArtifactsToDirectory(artifacts, bundledPluginsDir);
343 
344             if (bundledPluginsDir.list().length > 0)
345             {
346                 createZipFile(bundledPluginsDir, bundledPluginsZip);
347             }
348 
349             // todo add log4j.properties file if specified
350 //            if (ctx.getLog4jProperties() != null)
351 //            {
352 //                FileUtils.copyFile(ctx.getLog4jProperties(), new File(webappDir, "WEB-INF/classes/log4j.properties"));
353 //            }
354 
355         }
356         catch (final Exception e)
357         {
358             throw new MojoExecutionException("Failed to add plugin artifacts", e);
359         }
360     }
361 
362     /**
363      * The control port is the httpPort with a "1" appended to it //todo doc this
364      */
365     private int controlPort(int httpPort)
366     {
367         return httpPort * 10 + 1;
368     }
369 
370     protected Map<String, String> getSystemProperties(Product ctx)
371     {
372         return Collections.emptyMap();
373     }
374 
375     private static class FeCruPluginProvider extends AbstractPluginProvider
376     {
377 
378         @Override
379         protected Collection<ProductArtifact> getSalArtifacts(String salVersion)
380         {
381             return Arrays.asList(
382                     new ProductArtifact("com.atlassian.sal", "sal-api", salVersion),
383                     new ProductArtifact("com.atlassian.sal", "sal-fisheye-plugin", salVersion)
384             );
385         }
386     }
387 
388 }