1   package com.atlassian.plugins.codegen.modules;
2   
3   import java.io.StringReader;
4   import java.util.Map;
5   import java.util.Properties;
6   import java.util.TreeMap;
7   
8   import com.atlassian.plugins.codegen.ClassId;
9   import com.atlassian.plugins.codegen.I18nString;
10  import com.atlassian.plugins.codegen.PluginProjectChangeset;
11  import com.atlassian.plugins.codegen.modules.common.Resource;
12  import com.atlassian.plugins.codegen.util.CodeTemplateHelper;
13  
14  import com.google.common.collect.ImmutableMap;
15  
16  import org.apache.commons.io.FilenameUtils;
17  import org.apache.commons.io.IOUtils;
18  
19  import static com.atlassian.plugins.codegen.I18nString.i18nStrings;
20  import static com.atlassian.plugins.codegen.ModuleDescriptor.moduleDescriptor;
21  import static com.atlassian.plugins.codegen.PluginProjectChangeset.changeset;
22  import static com.atlassian.plugins.codegen.ResourceFile.resourceFile;
23  import static com.atlassian.plugins.codegen.SourceFile.sourceFile;
24  import static com.atlassian.plugins.codegen.SourceFile.SourceGroup.MAIN;
25  import static com.atlassian.plugins.codegen.SourceFile.SourceGroup.TESTS;
26  
27  /**
28   * Abstract base class for implementations of {@link PluginModuleCreator} that provides some
29   * helper methods for commonly used project modifications.
30   */
31  public abstract class AbstractPluginModuleCreator<T extends PluginModuleProperties> implements PluginModuleCreator<T>
32  {
33      public static final String DEFAULT_I18N_NAME = "atlassian-plugin";
34      public static final String FUNC_TEST_PACKAGE = "it";
35      public static final String TEST_SUFFIX = "Test";
36      public static final String FUNCT_TEST_SUFFIX = "FuncTest";
37      public static final String GENERIC_TEMPLATE_PREFIX = "templates/generic/";
38      public static final String GENERIC_TEST_TEMPLATE = GENERIC_TEMPLATE_PREFIX + "GenericTest.java.vtl";
39      public static final String TEMPLATES = "templates/";
40      
41      protected CodeTemplateHelper templateHelper;
42  
43      protected AbstractPluginModuleCreator()
44      {
45          this(new CodeTemplateHelper());
46      }
47  
48      protected AbstractPluginModuleCreator(CodeTemplateHelper templateHelper)
49      {
50          this.templateHelper = templateHelper;
51      }
52  
53      @Override
54      public abstract PluginProjectChangeset createModule(T props) throws Exception;
55  
56      /**
57       * Returns a changeset that will add a source file to the project, within the main source directory.
58       * @param props  property set whose {@link ClassBasedModuleProperties#getClassId()} method provides the class name;
59       *   other properties may be used in the template
60       * @param templateName  path to the template file for creating the source code
61       * @return  a {@link PluginProjectChangeset} that describes the new file
62       */
63      protected PluginProjectChangeset createClass(ClassBasedModuleProperties props, String templateName) throws Exception
64      {
65          return createClass(props, props.getClassId(), templateName);
66      }
67      
68      /**
69       * Returns a changeset that will add a source file to the project, within the main source directory.
70       * @param props  property set whose properties may be used in the template
71       * @param classId  describes the class and package name
72       * @param templateName  path to the template file for creating the source code
73       * @return  a {@link PluginProjectChangeset} that describes the new file
74       */
75      protected PluginProjectChangeset createClass(ClassBasedModuleProperties props, ClassId classId, String templateName) throws Exception
76      {
77          return changeset().with(sourceFile(classId, MAIN, fromTemplate(templateName, props)));
78      }
79  
80      /**
81       * Returns a changeset that will add a source file to the project, within the test source directory.
82       * @param props  property set whose properties may be used in the template
83       * @param classId  describes the class and package name
84       * @param templateName  path to the template file for creating the source code
85       * @return  a {@link PluginProjectChangeset} that describes the new file
86       */
87      protected PluginProjectChangeset createTestClass(ClassBasedModuleProperties props, ClassId classId, String templateName) throws Exception
88      {
89          return changeset().with(sourceFile(classId, TESTS, fromTemplate(templateName, props)));
90      }
91  
92      /**
93       * Returns a changeset that will add a source file and also its corresponding unit test.
94       * @param props  property set whose properties may be used in the template; the source file will be
95       *   for the class described by {@link ClassBasedModuleProperties#getClassId()} and the unit test
96       *   class will be determined by {@link #testClassFor(ClassId)}
97       * @param mainTemplate  path to the template file for creating the main source code
98       * @param unitTestTemplate  path to the template file for creating the unit test source code
99       * @return  a {@link PluginProjectChangeset} that describes the new files
100      */
101     protected PluginProjectChangeset createClassAndTests(ClassBasedModuleProperties props,
102                                                          String mainTemplate,
103                                                          String unitTestTemplate) throws Exception
104     {
105         return changeset()
106             .with(createClass(props, mainTemplate))
107             .with(createTestClass(props, testClassFor(props.getClassId()), unitTestTemplate));
108     }
109     
110     /**
111      * Returns a changeset that will add a source file and also its corresponding unit test and
112      * functional test.
113      * @param props  property set whose properties may be used in the template; the source file will be
114      *   for the class described by {@link ClassBasedModuleProperties#getClassId()}, the unit test
115      *   class will be determined by {@link #testClassFor(ClassId)}, and the functional test class
116      *   will be determined by {@link #funcTestClassFor(ClassId)}
117      * @param mainTemplate  path to the template file for creating the main source code
118      * @param unitTestTemplate  path to the template file for creating the unit test source code
119      * @param unitTestTemplate  path to the template file for creating the functional test source code
120      * @return  a {@link PluginProjectChangeset} that describes the new files
121      */
122     protected PluginProjectChangeset createClassAndTests(ClassBasedModuleProperties props,
123                                                          String mainTemplate,
124                                                          String unitTestTemplate,
125                                                          String funcTestTemplate) throws Exception
126     {
127         return createClassAndTests(props, mainTemplate, unitTestTemplate)
128             .with(createTestClass(props, funcTestClassFor(props.getClassId()), funcTestTemplate));
129     }
130     
131     /**
132      * Returns a changeset that will add a plugin module to the project's plugin XML file.  Also
133      * adds any i18n properties that are required for the module.
134      * @param props  property set whose properties may be used in the template; also may provide
135      *   i18n properties with {@link PluginModuleProperties#getI18nProperties()}
136      * @param templateName  path to the template file for the module's XML fragment
137      * @return  a {@link PluginProjectChangeset} that describes the new module and i18n changes
138      */
139     protected PluginProjectChangeset createModule(PluginModuleProperties props, String templateName) throws Exception
140     {
141         return changeset().with(moduleDescriptor(fromTemplate(templateName, props)))
142             .with(i18nStrings(props.getI18nProperties()));
143     }
144     
145     /**
146      * Returns a changeset that will add a resource file to the project, within the main resource
147      * directory or a subdirectory thereof.
148      * @param props  property set whose properties may be used in the template
149      * @param path  relative path within the resource directory, or "" for no subpath
150      * @param fileName  base name for the new file
151      * @param templateName  path to the template file for the resource content
152      * @return  a {@link PluginProjectChangeset} that describes the new file
153      */
154     protected PluginProjectChangeset createResource(Map<Object, Object> props, String path, String fileName, String templateName) throws Exception
155     {
156         return changeset().with(resourceFile(path, fileName, fromTemplate(templateName, props)));
157     }
158 
159     /**
160      * Returns a changeset that will add a resource file to the project, within a subdirectory of
161      * the main resource directory that is prefixed with "templates/".
162      * @param props  property set whose properties may be used in the template
163      * @param path  relative path within the resource directory, or "" for no subpath
164      * @param fileName  base name for the new file
165      * @param templateName  path to the template file for the resource content
166      * @return  a {@link PluginProjectChangeset} that describes the new file
167      */
168     protected PluginProjectChangeset createTemplateResource(Map<Object, Object> props, String path, String fileName, String templateName) throws Exception
169     {
170         path = path.equals("") ? TEMPLATES : (path.startsWith(TEMPLATES) ? path : (TEMPLATES + path));
171         return changeset().with(resourceFile(path, fileName, fromTemplate(templateName, props)));
172     }
173 
174     /**
175      * Wrapper for {@link #createTemplateResource(Map, Resource, String)} that derives the path
176      * and filename from a {@link Resource} object.  Also sets the property "CURRENT_VIEW" to the
177      * filename when generating the template.
178      * @param props  property set whose properties may be used in the template
179      * @param resource  a {@link Resource} describing the path and filename
180      * @param templateName  path to the template file for the resource content
181      * @return  a {@link PluginProjectChangeset} that describes the new file
182      */
183     protected PluginProjectChangeset createTemplateResource(Map<Object, Object> props, Resource resource, String templateName) throws Exception
184     {
185         String resourceFullPath = FilenameUtils.separatorsToSystem(resource.getLocation());
186         String path = FilenameUtils.getPath(resourceFullPath);
187         String fileName = FilenameUtils.getName(resourceFullPath);
188         Map<Object, Object> tempProps = ImmutableMap.builder().putAll(props).put("CURRENT_VIEW", fileName).build();
189         return createTemplateResource(tempProps, path, fileName, templateName);
190     }
191     
192     /**
193      * Returns a changeset that will add a set of I18n strings to the project, based on a properties file
194      * that can contain template variables.
195      * @param props  property set whose properties may be used in the template
196      * @param templateName  path to the template file for the property list
197      * @return  a {@link PluginProjectChangeset} that describes the new I18n strings
198      */
199     @SuppressWarnings("unchecked")
200     protected PluginProjectChangeset createI18nStrings(Map<Object, Object> props, String templateName) throws Exception
201     {
202         String propListString = fromTemplate(templateName, props);
203         Properties propList = new Properties();
204         propList.load(new StringReader(propListString));
205         return changeset().with(I18nString.i18nStrings(new TreeMap<String, String>((Map) propList)));
206     }
207     
208     /**
209      * Generates content using a template file.
210      * @param templatePath  path to the template file
211      * @param props  properties that may be used in the template
212      * @return  the generated content
213      * @throws Exception
214      */
215     protected String fromTemplate(String templatePath, Map<Object, Object> props) throws Exception
216     {
217         return templateHelper.getStringFromTemplate(templatePath, props);
218     }
219 
220     /**
221      * Reads a file as-is from the classpath, with no template substitution.
222      * @param filePath  path to the template file
223      * @return  the file content
224      * @throws Exception
225      */
226     protected String fromFile(String filePath) throws Exception
227     {
228         return IOUtils.toString(getClass().getClassLoader().getResourceAsStream(filePath));
229     }
230     
231     /**
232      * Returns the standard unit test class corresponding to the given class.  The test class
233      * is in the same package but has {@link #TEST_SUFFIX} appended to its name.
234      */
235     protected ClassId testClassFor(ClassId mainClass)
236     {
237         return mainClass.classNameSuffix(TEST_SUFFIX);
238     }
239     
240     /**
241      * Returns the standard functional test class corresponding to the given class.  The test
242      * class has {@link #FUNCT_TEST_SUFFIX} appended to its name and {@link #FUNC_TEST_PACKAGE}
243      * prepended to its package.
244      */
245     protected ClassId funcTestClassFor(ClassId mainClass)
246     {
247         return mainClass.packageNamePrefix(FUNC_TEST_PACKAGE).classNameSuffix(FUNCT_TEST_SUFFIX);
248     }
249 }