use of com.codename1.ant.SortedProperties in project CodenameOne by codenameone.
the class AbstractCN1Mojo method getProjectProperties.
/**
* The project's codenameone_settings.properties
*
* @return
* @throws IOException
*/
protected SortedProperties getProjectProperties() throws IOException {
if (projectProperties == null) {
projectProperties = new SortedProperties();
File propertiesFile = getProjectPropertiesFile();
if (propertiesFile.exists()) {
try (FileInputStream fis = new FileInputStream(propertiesFile)) {
projectProperties.load(fis);
}
}
}
return projectProperties;
}
use of com.codename1.ant.SortedProperties in project CodenameOne by codenameone.
the class CN1BuildMojo method createAntProject.
private void createAntProject() throws IOException, LibraryPropertiesException, MojoExecutionException {
File cn1dir = new File(project.getBuild().getDirectory() + File.separator + "codenameone");
File antProject = new File(cn1dir, "antProject");
antProject.mkdirs();
File codenameOneSettings = new File(getCN1ProjectDir(), "codenameone_settings.properties");
File icon = new File(getCN1ProjectDir(), "icon.png");
if (icon.exists()) {
FileUtils.copyFile(icon, new File(antProject, "icon.png"));
} else {
FileUtils.copyInputStreamToFile(getClass().getResourceAsStream("codenameone-icon.png"), new File(antProject, "icon.png"));
}
File codenameOneSettingsCopy = new File(antProject, codenameOneSettings.getName());
FileUtils.copyFile(codenameOneSettings, codenameOneSettingsCopy);
FileUtils.copyInputStreamToFile(getClass().getResourceAsStream("buildxml-template.xml"), new File(antProject, "build.xml"));
File distDir = new File(antProject, "dist");
distDir.mkdirs();
// Build a jar with all dependencies that we will send to the build server.
File jarWithDependencies = new File(path(project.getBuild().getDirectory(), project.getBuild().getFinalName() + "-" + buildTarget + "-jar-with-dependencies.jar"));
List<String> cpElements;
try {
// getLog().info("Classpath Elements: "+ project.getCompileClasspathElements());
cpElements = project.getCompileClasspathElements();
} catch (Exception ex) {
throw new MojoExecutionException("Failed to get classpath elements", ex);
}
File appExtensionsJar = getAppExtensionsJar();
if (appExtensionsJar != null) {
cpElements.add(appExtensionsJar.getAbsolutePath());
}
File stringsJar = getStringsJar();
if (stringsJar != null) {
cpElements.add(stringsJar.getAbsolutePath());
}
getLog().debug("Classpath Elements: " + cpElements);
if (jarWithDependencies.exists()) {
getLog().debug("Found jar file with dependencies at " + jarWithDependencies + ". Will use that one unless it is out of date.");
for (String artifact : cpElements) {
File jar = new File(artifact);
if (jar.isDirectory()) {
if (jarWithDependencies.lastModified() < lastModifiedRecursive(jar)) {
getLog().debug("Jar file out of date. Dependencies have changed. " + jarWithDependencies + ". Deleting");
jarWithDependencies.delete();
break;
}
} else if (jar.exists() && jar.lastModified() > jarWithDependencies.lastModified()) {
// One of the dependency jar files is newer... so we delete the dependencies jar file
// and will generate a new one.
getLog().debug("Jar file out of date. Dependencies have changed. " + jarWithDependencies + ". Deleting");
jarWithDependencies.delete();
break;
}
}
}
if (!jarWithDependencies.exists()) {
getLog().info(jarWithDependencies + " not found. Generating jar with dependencies now");
// Jars that should be stripped out and not sent to the server
List<String> blackListJars = new ArrayList<String>();
getLog().info("Project artifacts: " + project.getArtifacts());
for (Artifact artifact : project.getArtifacts()) {
boolean addToBlacklist = false;
if (artifact.getGroupId().equals("com.codenameone") && contains(artifact.getArtifactId(), BUNDLE_ARTIFACT_ID_BLACKLIST)) {
addToBlacklist = true;
}
if (!addToBlacklist && !isLocalBuildTarget(buildTarget)) {
// for local builds, it's easier to just include it.
if (artifact.getGroupId().equals("org.jetbrains.kotlin") && artifact.getArtifactId().equals("kotlin-stdlib")) {
addToBlacklist = true;
serverMustProvideKotlinVersion = artifact.getVersion();
getLog().debug("Adding kotlin-stdlib to blacklist. Server will provide this:" + artifact);
}
}
if (!addToBlacklist && !"compile".equals(artifact.getScope())) {
addToBlacklist = true;
}
if (addToBlacklist) {
File jar = getJar(artifact);
if (jar != null) {
blackListJars.add(jar.getAbsolutePath());
blackListJars.add(jar.getPath());
try {
blackListJars.add(jar.getCanonicalPath());
getLog().debug("Added " + jar + " to blacklist");
} catch (Exception ex) {
}
}
}
}
getLog().debug("Merging compile classpath elements into jar with dependencies: " + cpElements);
List<File> jarsToMerge = new ArrayList<File>();
for (String element : cpElements) {
String canonicalEl = element;
try {
canonicalEl = new File(canonicalEl).getCanonicalPath();
} catch (Exception ex) {
}
if (blackListJars.contains(element) || blackListJars.contains(canonicalEl)) {
getLog().debug("NOT adding jar " + element + " because it is on the blacklist");
continue;
}
if (!new File(element).exists()) {
continue;
}
getLog().debug("Adding jar " + element + " to " + jarWithDependencies + " Jar file=" + element);
jarsToMerge.add(new File(element));
}
mergeJars(jarWithDependencies, jarsToMerge.toArray(new File[jarsToMerge.size()]));
}
try {
updateCodenameOne(false);
} catch (MojoExecutionException ex) {
getLog().error("Failed to update Codename One");
throw new IOException("Failed to update Codename One", ex);
}
File antDistDir = new File(antProject, "dist");
File antDistJar = new File(antDistDir, project.getBuild().getFinalName() + "-" + buildTarget + "-jar-with-dependencies.jar");
antDistDir.mkdirs();
FileUtils.copyFile(jarWithDependencies, antDistJar);
Properties p = new Properties();
p.setProperty("codenameone_settings.properties", codenameOneSettingsCopy.getAbsolutePath());
p.setProperty("CodeNameOneBuildClient.jar", path(System.getProperty("user.home"), ".codenameone", "CodeNameOneBuildClient.jar"));
p.setProperty("dist.jar", antDistJar.getAbsolutePath());
if (automated) {
p.setProperty("automated", "true");
}
getLog().info("Running ANT build target " + buildTarget);
String logPasskey = UUID.randomUUID().toString();
Properties cn1SettingsProps = new Properties();
try (FileInputStream fis = new FileInputStream(codenameOneSettingsCopy)) {
cn1SettingsProps.load(fis);
}
if (serverMustProvideKotlinVersion != null) {
cn1SettingsProps.setProperty("codename1.arg.requireKotlinStdlib", serverMustProvideKotlinVersion);
}
FileSystemManager fsManager = VFS.getManager();
FileObject jarFile = fsManager.resolveFile("jar:" + jarWithDependencies.getAbsolutePath() + "!/META-INF/codenameone");
if (jarFile != null) {
FileObject[] appendedPropsFiles = jarFile.findFiles(new PatternFileSelector(".*\\/codenameone_library_appended.properties"));
if (appendedPropsFiles != null) {
for (FileObject appendedPropsFile : appendedPropsFiles) {
SortedProperties appendedProps = new SortedProperties();
try (InputStream appendedPropsIn = appendedPropsFile.getContent().getInputStream()) {
appendedProps.load(appendedPropsIn);
}
for (String propName : appendedProps.stringPropertyNames()) {
String propVal = appendedProps.getProperty(propName);
if (!cn1SettingsProps.containsKey(propName)) {
cn1SettingsProps.put(propName, propVal);
} else {
String existing = cn1SettingsProps.getProperty(propName);
if (!existing.contains(propVal)) {
cn1SettingsProps.setProperty(propName, existing + propVal);
}
}
}
}
}
FileObject[] requiredPropsFiles = jarFile.findFiles(new PatternFileSelector(".*\\/codenameone_library_required.properties"));
if (requiredPropsFiles != null) {
for (FileObject requiredPropsFile : requiredPropsFiles) {
SortedProperties requiredProps = new SortedProperties();
try (InputStream appendedPropsIn = requiredPropsFile.getContent().getInputStream()) {
requiredProps.load(appendedPropsIn);
}
String artifactId = requiredPropsFile.getParent().getName().getBaseName();
String groupId = requiredPropsFile.getParent().getParent().getName().getBaseName();
String libraryName = groupId + ":" + artifactId;
cn1SettingsProps = mergeRequiredProperties(libraryName, requiredProps, cn1SettingsProps);
}
}
}
cn1SettingsProps.setProperty("codename1.arg.hyp.beamId", logPasskey);
cn1SettingsProps.setProperty("codename1.arg.maven.codenameone-core.version", cn1MavenVersion);
cn1SettingsProps.setProperty("codename1.arg.maven.codenameone-maven-plugin", cn1MavenPluginVersion);
try (FileOutputStream fos = new FileOutputStream(codenameOneSettingsCopy)) {
cn1SettingsProps.store(fos, "");
}
final Process[] proc = new Process[1];
final boolean[] closingHypLog = new boolean[1];
Thread hyperBeamThread = new Thread(() -> {
ProcessBuilder pb = new ProcessBuilder("hyp", "beam", logPasskey);
pb.redirectErrorStream(true);
try {
proc[0] = pb.start();
InputStream out = proc[0].getInputStream();
byte[] buffer = new byte[4000];
while (isAlive(proc[0])) {
int no = out.available();
if (no > 0) {
int n = out.read(buffer, 0, Math.min(no, buffer.length));
getLog().info(new String(buffer, 0, n));
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
}
}
} catch (Exception ex) {
if (!closingHypLog[0]) {
getLog().warn("Failed to start hyperlog. The build log will not stream to your console. If the build fails, you can download the error log at https://cloud.codenameone.com/secure/index.html");
getLog().debug(ex);
}
}
});
try {
if (isLocalBuildTarget(buildTarget)) {
automated = false;
if (buildTarget.contains("android") || BUILD_TARGET_ANDROID_PROJECT.equals(buildTarget)) {
doAndroidLocalBuild(antProject, cn1SettingsProps, antDistJar);
} else if (buildTarget.contains("ios") || BUILD_TARGET_XCODE_PROJECT.equals(buildTarget)) {
doIOSLocalBuild(antProject, cn1SettingsProps, antDistJar);
} else {
throw new MojoExecutionException("Build target not supported " + buildTarget);
}
} else {
if (automated) {
getLog().debug("Attempting to start hyper beam stream the build log to the console");
hyperBeamThread.start();
}
AntExecutor.executeAntTask(new File(antProject, "build.xml").getAbsolutePath(), buildTarget, p);
}
} finally {
if (automated) {
try {
closingHypLog[0] = true;
proc[0].destroyForcibly();
} catch (Exception ex) {
}
}
}
if (automated) {
getLog().info("Extracting server result");
File result = new File(antDistDir, "result.zip");
if (!result.exists()) {
throw new IOException("Failed to find result.zip after automated build");
}
Expand unzip = (Expand) this.antProject.createTask("unzip");
unzip.setSrc(result);
File resultDir = new File(antDistDir, "result");
resultDir.mkdir();
unzip.setDest(resultDir);
unzip.execute();
for (File child : resultDir.listFiles()) {
String name = child.getName();
int dotpos = name.lastIndexOf(".");
if (dotpos < 0) {
continue;
}
String extension = name.substring(dotpos);
String base = name.substring(0, dotpos);
File copyTo = new File(project.getBuild().getDirectory() + File.separator + project.getBuild().getFinalName() + extension);
FileUtils.copyFile(child, copyTo);
if (".war".equals(extension)) {
projectHelper.attachArtifact(project, "war", copyTo);
} else if (".zip".equals(extension) && "javascript".equals(buildTarget)) {
projectHelper.attachArtifact(project, "zip", "webapp", copyTo);
} else if (".dmg".equals(extension) && "mac-os-x-desktop".equals(buildTarget)) {
projectHelper.attachArtifact(project, "dmg", "mac-app", copyTo);
} else if (".pkg".equals(extension) && "mac-os-x-desktop".equals(buildTarget)) {
projectHelper.attachArtifact(project, "pkg", "mac-app-installer", copyTo);
}
}
FileUtils.deleteDirectory(resultDir);
result.delete();
afterBuild();
}
}
use of com.codename1.ant.SortedProperties in project CodenameOne by codenameone.
the class CN1BuildMojo method doAndroidLocalBuild.
private void doAndroidLocalBuild(File tmpProjectDir, Properties props, File distJar) throws MojoExecutionException {
if (BUILD_TARGET_ANDROID_PROJECT.equals(buildTarget)) {
File generatedProject = getGeneratedAndroidProjectSourceDirectory();
getLog().info("Generating android gradle Project to " + generatedProject + "...");
try {
if (generatedProject.exists()) {
getLog().info("Android gradle project already exists. Checking to see if it needs updating...");
if (getSourcesModificationTime() <= lastModifiedRecursive(generatedProject)) {
getLog().info("Sources have not changed. Skipping android gradle project generation");
if (open) {
openAndroidStudioProject(generatedProject);
}
return;
}
}
} catch (IOException ex) {
throw new MojoExecutionException("Failed to find last modification time of " + generatedProject);
}
}
File codenameOneJar = getJar("com.codenameone", "codenameone-core");
AndroidGradleBuilder e = new AndroidGradleBuilder();
e.setBuildTarget(buildTarget);
e.setLogger(getLog());
File buildDirectory = new File(tmpProjectDir, "dist" + File.separator + "android-build");
e.setBuildDirectory(buildDirectory);
e.setCodenameOneJar(codenameOneJar);
e.setPlatform("android");
BuildRequest r = new BuildRequest();
r.setDisplayName(props.getProperty("codename1.displayName"));
r.setPackageName(props.getProperty("codename1.packageName"));
r.setMainClass(props.getProperty("codename1.mainName"));
r.setVersion(props.getProperty("codename1.version"));
String iconPath = props.getProperty("codename1.icon");
File iconFile = new File(iconPath);
if (!iconFile.isAbsolute()) {
iconFile = new File(getCN1ProjectDir(), iconPath);
}
try {
BufferedImage bi = ImageIO.read(iconFile);
if (bi.getWidth() != 512 || bi.getHeight() != 512) {
throw new MojoExecutionException("The icon must be a 512x512 pixel PNG image. It will be scaled to the proper sizes for devices");
}
r.setIcon(iconFile.getAbsolutePath());
} catch (IOException ex) {
throw new MojoExecutionException("Error reading the icon: the icon must be a 512x512 pixel PNG image. It will be scaled to the proper sizes for devices");
}
r.setVendor(props.getProperty("codename1.vendor"));
r.setSubTitle(props.getProperty("codename1.secondaryTitle"));
r.setType("android");
r.setKeystoreAlias(props.getProperty("codename1.android.keystoreAlias"));
String keystorePath = props.getProperty("codename1.android.keystore");
if (keystorePath != null) {
File keystoreFile = new File(keystorePath);
if (!keystoreFile.isAbsolute()) {
keystoreFile = new File(getCN1ProjectDir(), keystorePath);
}
if (keystoreFile.exists() && keystoreFile.isFile()) {
try {
r.setCertificate(keystoreFile.getAbsolutePath());
} catch (IOException ex) {
throw new MojoExecutionException("Failed to load keystore file. ", ex);
}
} else {
File androidCerts = new File(getCN1ProjectDir(), "androidCerts");
androidCerts.mkdirs();
keystoreFile = new File(androidCerts, "KeyChain.ks");
if (!keystoreFile.exists()) {
try {
String alias = r.getKeystoreAlias();
if (alias == null || alias.isEmpty()) {
alias = "androidKey";
r.setKeystoreAlias(alias);
props.setProperty("codename1.android.keystoreAlias", alias);
}
String password = props.getProperty("codename1.android.keystorePassword");
if (password == null || password.isEmpty()) {
password = "password";
props.setProperty("codename1.android.keystorePassword", password);
}
getLog().info("No Keystore found. Generating one now");
String keyPath = generateCertificate(password, alias, r.getVendor(), "", r.getVendor(), "Vancouver", "BC", "CA", false);
FileUtils.copyFile(new File(keyPath), keystoreFile);
r.setCertificate(keystoreFile.getAbsolutePath());
getLog().info("Generated keystore with password 'password' at " + keystoreFile + ". alias=androidKey");
new File(keyPath).delete();
SortedProperties sp = new SortedProperties();
try (FileInputStream fis = new FileInputStream(new File(getCN1ProjectDir(), "codenameone_settings.properties"))) {
sp.load(fis);
}
sp.setProperty("codename1.android.keystore", keystoreFile.getAbsolutePath());
sp.setProperty("codename1.android.keystorePassword", password);
sp.setProperty("codename1.android.keystoreAlias", alias);
try (FileOutputStream fos = new FileOutputStream(new File(getCN1ProjectDir(), "codenameone_settings.properties"))) {
sp.store(fos, "Updated keystore");
}
} catch (Exception ex) {
getLog().error("Failed to generate keystore", ex);
throw new MojoExecutionException("Failed to generate keystore", ex);
}
}
}
}
r.setCertificatePassword(props.getProperty("codename1.android.keystorePassword"));
for (Object k : props.keySet()) {
String key = (String) k;
if (key.startsWith("codename1.arg.")) {
String value = props.getProperty(key);
String currentKey = key.substring(14);
if (currentKey.indexOf(' ') > -1) {
throw new MojoExecutionException("The build argument contains a space in the key: '" + currentKey + "'");
}
r.putArgument(currentKey, value);
}
}
BuildRequest request = r;
request.setIncludeSource(true);
String testBuild = request.getArg("build.unitTest", null);
if (testBuild != null && testBuild.equals("1")) {
e.setUnitTestMode(true);
}
try {
getLog().info("Starting android project builder...");
boolean result = e.build(distJar, request);
getLog().info("Android project builder completed with result " + result);
if (!result) {
getLog().error("Received false return value from build()");
throw new MojoExecutionException("Android build failed. Received false return value for build");
}
if (BUILD_TARGET_ANDROID_PROJECT.equals(buildTarget) && e.getGradleProjectDirectory() != null) {
File gradleProject = e.getGradleProjectDirectory();
File output = getGeneratedAndroidProjectSourceDirectory();
output.getParentFile().mkdirs();
try {
getLog().info("Copying Gradle Project to " + output);
FileUtils.copyDirectory(gradleProject, output);
} catch (IOException ex) {
throw new MojoExecutionException("Failed to copy gradle project at " + gradleProject + " to " + output, ex);
}
}
if (open) {
openAndroidStudioProject(getGeneratedAndroidProjectSourceDirectory());
}
} catch (BuildException ex) {
getLog().error("Failed to build Android project with error: " + ex.getMessage(), ex);
getLog().error(e.getErrorMessage());
throw new MojoExecutionException("Failed to build android app", ex);
} finally {
e.cleanup();
}
}
use of com.codename1.ant.SortedProperties in project CodenameOne by codenameone.
the class GenerateAppProjectMojo method copySourceFiles.
private void copySourceFiles() {
{
Copy copy = (Copy) antProject().createTask("copy");
copy.setTodir(targetSrcDir("java"));
copy.setOverwrite(true);
FileSet files = new FileSet();
files.setProject(antProject());
files.setDir(sourceSrcDir());
files.setIncludes("**/*.java, *.java");
copy.addFileset(files);
copy.execute();
}
{
Copy copy = (Copy) antProject().createTask("copy");
copy.setTodir(targetSrcDir("resources"));
copy.setOverwrite(true);
FileSet files = new FileSet();
files.setProject(antProject());
files.setDir(sourceSrcDir());
files.setExcludes("**/*.kt, **/*.java, **/*.mirah, *.kt, *.java, *.mirah");
copy.addFileset(files);
copy.execute();
File cn1PropertiesFile = new File(sourceProject, "codenameone_settings.properties");
if (cn1PropertiesFile.exists()) {
Properties cn1Properties = new SortedProperties();
try (FileInputStream input = new FileInputStream(cn1PropertiesFile)) {
cn1Properties.load(input);
} catch (IOException ex) {
getLog().error("Failed to open " + cn1Properties + " while checking or cssTheme property", ex);
}
if ("true".equals(cn1Properties.getProperty("codename1.cssTheme", "false"))) {
// If we're using a CSS theme, then we need to delete the theme.res file
File themeRes = new File(targetSrcDir("resources"), "theme.res");
if (themeRes.exists()) {
getLog().debug("Deleting " + themeRes + " because this project uses CSS themes. In maven the theme.res is generated at build time, and is never saved in the source directory.");
themeRes.delete();
}
}
}
}
if (hasFilesWithSuffix(sourceSrcDir(), ".kt")) {
targetSrcDir("kotlin").mkdirs();
Copy copy = (Copy) antProject().createTask("copy");
copy.setTodir(targetSrcDir("kotlin"));
copy.setOverwrite(true);
FileSet files = new FileSet();
files.setProject(antProject());
files.setDir(sourceSrcDir());
files.setIncludes("**/*.kt, *.kt");
copy.addFileset(files);
copy.execute();
}
if (hasFilesWithSuffix(sourceSrcDir(), ".mirah")) {
targetSrcDir("mirah").mkdirs();
Copy copy = (Copy) antProject().createTask("copy");
copy.setTodir(targetSrcDir("mirah"));
copy.setOverwrite(true);
FileSet files = new FileSet();
files.setProject(antProject());
files.setDir(sourceSrcDir());
files.setIncludes("**/*.mirah, *.mirah");
copy.addFileset(files);
copy.execute();
}
}
use of com.codename1.ant.SortedProperties in project CodenameOne by codenameone.
the class GenerateArchetypeFromTemplateMojo method processString.
/**
* Processes a string with template instructions. This string may have [dependencies], [css], or [properties]
* sections with content that will be injected.
*
* WARNING: This will make changes to the existing pom.xml file, and the src/main/css/theme.css file.
*
* @param contents String contents to be processed.
* @throws MojoExecutionException
*/
private void processString(String contents, File projectDir) throws MojoExecutionException {
File archetypeResourcesDir = new File(projectDir, path("src", "main", "resources", "archetype-resources"));
try {
File commonProjectDir = new File(archetypeResourcesDir, "common");
File pomFile = new File(commonProjectDir, "pom.xml");
File codenameoneSettingsProperties = new File(commonProjectDir, "codenameone_settings.properties");
File themeCss = new File(commonProjectDir, "src" + File.separator + "main" + File.separator + "css");
String pomContents = FileUtils.readFileToString(pomFile, "UTF-8");
final String origPomContents = pomContents;
String dependencies = extractDependencies(contents);
if (!dependencies.isEmpty()) {
getLog().info("Injecting dependencies:\n" + dependencies + " \ninto " + pomFile);
String marker = "<!-- INJECT DEPENDENCIES -->";
pomContents = pomContents.replace(marker, dependencies + "\n" + marker);
}
String properties = extractProperties(contents);
if (!properties.isEmpty()) {
SortedProperties props = new SortedProperties();
props.load(new StringReader(properties));
if (codenameoneSettingsProperties == null || !codenameoneSettingsProperties.exists()) {
throw new MojoExecutionException("Cannot find codenameone_settings.properties");
}
SortedProperties cn1Props = new SortedProperties();
cn1Props.load(new FileReader(codenameoneSettingsProperties));
cn1Props.putAll(props);
getLog().info("Injecting properties:\n" + props + "\n into " + codenameoneSettingsProperties);
cn1Props.store(new FileWriter(codenameoneSettingsProperties), "Injected properties from template");
}
String css = extractCSS(contents);
if (!css.isEmpty()) {
if (!themeCss.exists()) {
themeCss.getParentFile().mkdirs();
}
getLog().info("Adding CSS to " + themeCss);
FileUtils.writeStringToFile(themeCss, css, "UTF-8");
}
// We change the codename1.template property to codename1.template.installed so that
// this mojo won't operate on this project again. (Notice the check at the beginning
// of execImpl() to return if it doesn't find the codename1.template property).
pomContents = pomContents.replace("<codename1.template>", "<codename1.templated.installed>").replace("</codename1.template>", "</codename1.template.installed>");
if (!pomContents.equals(origPomContents)) {
getLog().info("Writing changes to " + pomFile);
FileUtils.writeStringToFile(pomFile, pomContents, "UTF-8");
}
for (FileContent file : extractFiles(contents)) {
File f = new File(commonProjectDir, file.path);
f.getParentFile().mkdirs();
FileUtils.writeStringToFile(f, file.content, "UTF-8");
}
} catch (TemplateParseException ex) {
throw new MojoExecutionException("Syntax error in template file", ex);
} catch (IOException ex) {
throw new MojoExecutionException("Failed to process template file", ex);
}
}
Aggregations