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