1   package com.atlassian.maven.plugins.amps.product;
2   
3   
4   import static org.apache.commons.io.FileUtils.copyDirectory;
5   import static org.apache.commons.lang.StringUtils.isNotBlank;
6   
7   import java.io.File;
8   import java.io.IOException;
9   import java.io.UnsupportedEncodingException;
10  import java.net.InetAddress;
11  import java.net.URLEncoder;
12  import java.net.UnknownHostException;
13  import java.util.Collections;
14  import java.util.Comparator;
15  import java.util.List;
16  
17  import org.apache.commons.io.FileUtils;
18  import org.apache.commons.lang.StringUtils;
19  import org.apache.maven.plugin.MojoExecutionException;
20  import org.apache.maven.plugin.logging.Log;
21  import org.apache.maven.project.MavenProject;
22  
23  import com.atlassian.maven.plugins.amps.MavenContext;
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.ConfigFileUtils;
28  import com.atlassian.maven.plugins.amps.util.ConfigFileUtils.Replacement;
29  import com.atlassian.maven.plugins.amps.util.ProjectUtils;
30  import com.atlassian.maven.plugins.amps.util.ZipUtils;
31  import com.google.common.collect.Lists;
32  
33  /**
34   * This abstract class is common to real applications (which inherit from AbstractProductHandler, like JIRA or Confluence)
35   * and the fake application Studio.
36   *
37   * This class handles common operations
38   *
39   * @since 3.6
40   */
41  public abstract class AmpsProductHandler implements ProductHandler
42  {
43  
44      protected final MavenGoals goals;
45      protected final MavenProject project;
46      protected final MavenContext context;
47      protected final Log log;
48  
49      protected AmpsProductHandler(MavenContext context, MavenGoals goals)
50      {
51          this.project = context.getProject();
52          this.context = context;
53          this.goals = goals;
54          this.log = context.getLog();
55      }
56  
57      /**
58       * Copies and creates a zip file of the previous run's home directory minus any installed plugins.
59       *
60       * @param homeDirectory
61       *            The path to the previous run's home directory.
62       * @param targetZip
63       *            The path to the final zip file.
64       * @param product
65       *            The product
66       *
67       * @since 3.1-m3
68       */
69      public void createHomeZip(final File homeDirectory, final File targetZip, final Product product) throws MojoExecutionException
70      {
71          if (homeDirectory == null || !homeDirectory.exists())
72          {
73              String homePath = "null";
74              if (homeDirectory != null)
75              {
76                  homePath = homeDirectory.getAbsolutePath();
77              }
78              context.getLog().info("home directory doesn't exist, skipping. [" + homePath + "]");
79              return;
80          }
81  
82  
83          try
84              {
85              /*
86               * The zip has /someRootFolder/{productId}-home/
87               */
88              final File appDir = getBaseDirectory(product);
89              final File tmpDir = new File(appDir, "tmp-resources");
90              final File homeSnapshot = new File(tmpDir, "generated-home");
91              final String entryBase = "generated-resources/" + product.getId() + "-home";
92  
93              if (homeSnapshot.exists())
94              {
95                  FileUtils.deleteDirectory(homeSnapshot);
96              }
97  
98              homeSnapshot.mkdirs();
99              FileUtils.copyDirectory(homeDirectory, homeSnapshot, true);
100 
101             cleanupProductHomeForZip(product, homeSnapshot);
102             ZipUtils.zipDir(targetZip, homeSnapshot, entryBase);
103         }
104         catch (IOException e)
105         {
106             throw new RuntimeException("Error zipping home directory", e);
107         }
108     }
109 
110     /**
111      * Prepares the home directory to snapshot:
112      * <ul>
113      * <li>Removes all unnecessary files</li>
114      * <li>Perform product-specific clean-up</li>
115      * <ul>
116      * This is a reference implementation. It is probable that each application has a different set of directories to delete.
117      * @param product the product details
118      * @param homeDirectory an image of the home which will be zipped. This is not the working home, so you're free to remove files and parametrise them.
119      * @throws IOException
120      */
121     public void cleanupProductHomeForZip(Product product, File snapshotDir) throws MojoExecutionException, IOException
122     {
123         try {
124             // we want to get rid of the plugins folders.
125             FileUtils.deleteDirectory(new File(snapshotDir, "plugins")); // Not used by: fisheye, confluence, studio - Used by: crowd, bamboo, jira
126             FileUtils.deleteDirectory(new File(snapshotDir, "bundled-plugins")); // Not used by: fisheye, jira - Used by: confluence, crowd, bamboo
127 
128             // Get rid of "studio-test-resources.zip", which is the homeZip that was used
129             // when we started Amps.
130             if (this.getTestResourcesArtifact() != null)
131             {
132                 String originalHomeZip = this.getTestResourcesArtifact().getArtifactId() + ".zip";
133                 FileUtils.deleteQuietly(new File(snapshotDir, originalHomeZip));
134             }
135 
136 
137             // Proceed to replacements
138             List<Replacement> replacements = getReplacements(product);
139             // Sort by longer values first, so that the right keys are used.
140             Collections.sort(replacements, new Comparator<Replacement>(){
141                 @Override
142                 public int compare(Replacement replacement1, Replacement replacement2)
143                 {
144                     // longest value < shortest value
145                     int length1 = replacement1.getValue().length();
146                     int length2 = replacement2.getValue().length();
147                     return length2 - length1;
148                 }
149             });
150             List<File> files = getConfigFiles(product, snapshotDir);
151 
152             ConfigFileUtils.replace(files, replacements, true, log);
153         }
154         catch (IOException ioe)
155         {
156             throw new MojoExecutionException("Could not delete home/plugins/ and /home/bundled-plugins/", ioe);
157         }
158     }
159 
160     abstract protected ProductArtifact getTestResourcesArtifact();
161 
162     protected File getProductHomeData(final Product ctx) throws MojoExecutionException
163     {
164         File productHomeZip = null;
165         String dataPath = ctx.getDataPath();
166 
167         //use custom zip if supplied
168         if (isNotBlank(dataPath))
169         {
170             File customHomeZip = new File(dataPath);
171 
172             if (customHomeZip.exists())
173             {
174                 return customHomeZip;
175             }
176             throw new MojoExecutionException("Unable to use custom test resources set by <dataPath>. File '" +
177                     customHomeZip.getAbsolutePath() + "' does not exist");
178         }
179 
180         //if we didn't find a custom zip, use the default
181         ProductArtifact testResourcesArtifact = getTestResourcesArtifact();
182         if (productHomeZip == null && testResourcesArtifact != null)
183         {
184             ProductArtifact artifact = new ProductArtifact(
185                 testResourcesArtifact.getGroupId(), testResourcesArtifact.getArtifactId(), ctx.getDataVersion());
186             productHomeZip = goals.copyHome(getBaseDirectory(ctx), artifact);
187         }
188 
189         return productHomeZip;
190     }
191 
192     protected void overrideAndPatchHomeDir(File homeDir, final Product ctx) throws MojoExecutionException
193     {
194         try
195         {
196             final File srcDir = new File(project.getBasedir(), "src/test/resources/" + ctx.getInstanceId() + "-home");
197             if (srcDir.exists() && homeDir.exists())
198             {
199                 copyDirectory(srcDir, homeDir);
200             }
201         }
202         catch (IOException e)
203         {
204             throw new MojoExecutionException("Unable to override files using src/test/resources", e);
205         }
206     }
207 
208 
209     /**
210      * Lists parameters which must be replaced in the configuration files of the home directory.
211      * <p/>
212      * Used reversely when reading / when creating a home zip.
213      */
214     public List<ConfigFileUtils.Replacement> getReplacements(Product product)
215     {
216         // Standard replacements:
217         List<Replacement> replacements = Lists.newArrayList();
218         String buildDirectory = project.getBuild().getDirectory();
219         String baseDirectory = getBaseDirectory(product).getAbsolutePath();
220         String homeDirectory = getHomeDirectory(product).getAbsolutePath();
221 
222         replacements.add(new Replacement("%PROJECT_BUILD_DIR%", buildDirectory));
223         replacements.add(new Replacement("%PRODUCT_BASE_DIR%", baseDirectory));
224         replacements.add(new Replacement("%PRODUCT_HOME_DIR%", homeDirectory));
225 
226         // These replacements are especially for Fecru, but there's no reason not to find them in other config files
227         try {
228             replacements.add(new Replacement("%PROJECT_BUILD_DIR_URL_ENCODED%", URLEncoder.encode(propertiesEncode(buildDirectory), "UTF-8")));
229             replacements.add(new Replacement("%PRODUCT_BASE_DIR_URL_ENCODED%", URLEncoder.encode(propertiesEncode(baseDirectory), "UTF-8")));
230             replacements.add(new Replacement("%PRODUCT_HOME_DIR_URL_ENCODED%", URLEncoder.encode(propertiesEncode(homeDirectory), "UTF-8")));
231         }
232         catch (UnsupportedEncodingException badJvm)
233         {
234             throw new RuntimeException("UTF-8 should be supported on any JVM", badJvm);
235         }
236 
237         replacements.add(Replacement.onlyWhenCreatingSnapshot("localhost", product.getServer()));
238 
239         try
240         {
241             String localHostName = InetAddress.getLocalHost().getHostName();
242             replacements.add(new Replacement("%LOCAL_HOST_NAME%", localHostName));
243         }
244         catch (UnknownHostException e)
245         {
246             // If we can't get the local computer's hostname, it's probable no product could,
247             // so we don't need to search-replace the value.
248         }
249 
250         return replacements;
251     }
252 
253     /**
254      * Encodes a String for a Properties file. Escapes : and =.
255      */
256     protected String propertiesEncode(String decoded)
257     {
258         if (decoded == null)
259         {
260             return null;
261         }
262         String replacement1 = decoded.replaceAll(":", "\\:");
263         String replacement2 = replacement1.replaceAll("=", "\\=");
264         return replacement2;
265     }
266 
267     @Override
268     public List<File> getConfigFiles(Product product, File snapshotDir)
269     {
270         return Lists.newArrayList();
271     }
272 
273     public File getBaseDirectory(Product ctx)
274     {
275         return ProjectUtils.createDirectory(new File(project.getBuild().getDirectory(), ctx.getInstanceId()));
276     }
277 
278     public File getHomeDirectory(Product ctx)
279     {
280         if (StringUtils.isNotBlank(ctx.getDataHome()))
281         {
282             return new File(ctx.getDataHome());
283         }
284         return new File(getBaseDirectory(ctx), "home");
285     }
286 
287     public File getSnapshotDirectory(Product product)
288     {
289         return getHomeDirectory(product);
290     }
291 
292     protected File createHomeDirectory(Product ctx)
293     {
294         return ProjectUtils.createDirectory(getHomeDirectory(ctx));
295     }
296 }