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 }