diff --git a/pom.xml b/pom.xml index 0686a37..e3c7aa0 100644 --- a/pom.xml +++ b/pom.xml @@ -51,7 +51,7 @@ com.inteligr8.alfresco aps-public-rest-api - 2.0.11 + 2.0.14 com.inteligr8.alfresco diff --git a/src/main/java/com/inteligr8/maven/aps/modeling/crawler/ApsTemplateCrawlable.java b/src/main/java/com/inteligr8/maven/aps/modeling/crawler/ApsTemplateCrawlable.java new file mode 100644 index 0000000..d5b101e --- /dev/null +++ b/src/main/java/com/inteligr8/maven/aps/modeling/crawler/ApsTemplateCrawlable.java @@ -0,0 +1,31 @@ +/* + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package com.inteligr8.maven.aps.modeling.crawler; + +/** + * An interface for APS template export transformation implementations. + * + * An APS template is a JSON file. + * + * @author brian@inteligr8.com + */ +public interface ApsTemplateCrawlable { + + /** + * @return A file transformer for APS template JSON files. + */ + ApsFileTransformer getTemplateJsonTransformer(); + +} diff --git a/src/main/java/com/inteligr8/maven/aps/modeling/crawler/ApsTemplateCrawler.java b/src/main/java/com/inteligr8/maven/aps/modeling/crawler/ApsTemplateCrawler.java new file mode 100644 index 0000000..8374c65 --- /dev/null +++ b/src/main/java/com/inteligr8/maven/aps/modeling/crawler/ApsTemplateCrawler.java @@ -0,0 +1,74 @@ +/* + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package com.inteligr8.maven.aps.modeling.crawler; + +import java.io.File; +import java.io.IOException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A class that implements a APS App export crawler. The crawler does not + * directly perform any transformations. Those are handled through a callback. + * + * @author brian@inteligr8.com + */ +public class ApsTemplateCrawler { + + private final Logger logger = LoggerFactory.getLogger(ApsTemplateCrawler.class); + private final File templateDirectory; + private final boolean failOnIntegrityViolation; + + /** + * @param apsAppName A name for the APS App. + * @param apsAppDirectory A directory to the unpacked APS App export. + * @param failOnIntegrityViolation true to fail on file integrity issues; false to log warnings. + */ + public ApsTemplateCrawler(File apsTemplateDirectory, boolean failOnIntegrityViolation) { + this.templateDirectory = apsTemplateDirectory; + this.failOnIntegrityViolation = failOnIntegrityViolation; + } + + /** + * @param crawlable A crawlable implementation; the callback for potential transformations. + * @throws IOException A file access exception occurred. + */ + public void execute(ApsTemplateCrawlable crawlable) throws IOException { + this.logger.info("Crawling APS templates ..."); + + this.crawlTemplates(crawlable.getTemplateJsonTransformer()); + } + + protected void crawlTemplates(ApsFileTransformer transformer) throws IOException { + for (File templateFile : this.templateDirectory.listFiles()) { + if (!templateFile.getName().endsWith(".json")) + continue; + + this.logger.trace("Transforming template: {}", templateFile); + + try { + transformer.transformFile(templateFile, null, null); + } catch (IllegalArgumentException | IllegalStateException ie) { + if (this.failOnIntegrityViolation) { + throw ie; + } else { + this.logger.warn(ie.getMessage()); + } + } + } + } + +} diff --git a/src/main/java/com/inteligr8/maven/aps/modeling/goal/ApsAddressibleGoal.java b/src/main/java/com/inteligr8/maven/aps/modeling/goal/ApsAddressibleGoal.java index dfac7bc..bb11769 100644 --- a/src/main/java/com/inteligr8/maven/aps/modeling/goal/ApsAddressibleGoal.java +++ b/src/main/java/com/inteligr8/maven/aps/modeling/goal/ApsAddressibleGoal.java @@ -14,6 +14,8 @@ */ package com.inteligr8.maven.aps.modeling.goal; +import java.util.List; + import org.apache.maven.execution.MavenSession; import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.settings.Server; @@ -21,6 +23,7 @@ import org.apache.maven.settings.Server; import com.inteligr8.alfresco.activiti.ApsClientJerseyConfiguration; import com.inteligr8.alfresco.activiti.ApsClientJerseyImpl; import com.inteligr8.alfresco.activiti.ApsPublicRestApiJerseyImpl; +import com.inteligr8.alfresco.activiti.model.Tenant; /** * This class adds APS addressbility to extending goals. @@ -128,5 +131,20 @@ public abstract class ApsAddressibleGoal extends DisablableGoal { return this.api; } + + /** + * This method makes the appropriate service calls to find the first APS + * tenant ID from the configured APS service. + * + * This method does not cache the result. + * + * @return An APS tenant ID; null only if there are no tenants. + */ + protected Long findTenantId() { + List tenants = this.getApsApi().getAdminApi().getTenants(); + if (tenants == null || tenants.isEmpty()) + return null; + return tenants.iterator().next().getId(); + } } diff --git a/src/main/java/com/inteligr8/maven/aps/modeling/goal/ApsAppAccessibleGoal.java b/src/main/java/com/inteligr8/maven/aps/modeling/goal/ApsAppAddressibleGoal.java similarity index 98% rename from src/main/java/com/inteligr8/maven/aps/modeling/goal/ApsAppAccessibleGoal.java rename to src/main/java/com/inteligr8/maven/aps/modeling/goal/ApsAppAddressibleGoal.java index b67fe2c..ae22f7d 100644 --- a/src/main/java/com/inteligr8/maven/aps/modeling/goal/ApsAppAccessibleGoal.java +++ b/src/main/java/com/inteligr8/maven/aps/modeling/goal/ApsAppAddressibleGoal.java @@ -36,7 +36,7 @@ import com.inteligr8.alfresco.activiti.model.Tenant; * * @author brian@inteligr8.com */ -public abstract class ApsAppAccessibleGoal extends ApsAddressibleGoal { +public abstract class ApsAppAddressibleGoal extends ApsAddressibleGoal { @Parameter( property = "aps-model.appName", required = true ) protected String apsAppName; diff --git a/src/main/java/com/inteligr8/maven/aps/modeling/goal/ApsTemplateAddressibleGoal.java b/src/main/java/com/inteligr8/maven/aps/modeling/goal/ApsTemplateAddressibleGoal.java new file mode 100644 index 0000000..a5cd759 --- /dev/null +++ b/src/main/java/com/inteligr8/maven/aps/modeling/goal/ApsTemplateAddressibleGoal.java @@ -0,0 +1,162 @@ +/* + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package com.inteligr8.maven.aps.modeling.goal; + +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +import org.apache.maven.plugins.annotations.Parameter; + +import com.inteligr8.activiti.model.ResultList; +import com.inteligr8.alfresco.activiti.model.BaseTemplateLight; +import com.inteligr8.alfresco.activiti.model.DocumentTemplateLight; +import com.inteligr8.alfresco.activiti.model.EmailTemplateLight; + +/** + * This class adds the APS App name to APS addressibility to extending goals. + * + * Only use this class if your goal needs both the APS name and an APS service + * client. You can use `ApsAppGoal` or `ApsAddressibleGoal` if you only need + * one of those capabilities. + * + * @author brian@inteligr8.com + */ +public abstract class ApsTemplateAddressibleGoal extends ApsAddressibleGoal { + + public enum TemplateType { + SystemEmail, + CustomEmail, + Document + } + + @Parameter( property = "aps-model.documentTemplateNames", defaultValue = ".*" ) + protected String apsDocumentTemplateNames; + @Parameter( property = "aps-model.systemEmailTemplateNames", defaultValue = ".*" ) + protected String apsSystemEmailTemplateNames; + @Parameter( property = "aps-model.customEmailTemplateNames", defaultValue = ".*" ) + protected String apsCustomEmailTemplateNames; + + protected Map> findTemplates() { + Long tenantId = this.findTenantId(); + return this.findTemplates(tenantId); + } + + protected Map> findTemplates(Long tenantId) { + Map> templates = new HashMap<>(32); + + Map systemEmailTemplateNames = this.findSystemEmailTemplates(tenantId); + if (systemEmailTemplateNames != null && !systemEmailTemplateNames.isEmpty()) + templates.put(TemplateType.SystemEmail, systemEmailTemplateNames); + + Map customEmailTemplateNames = this.findCustomEmailTemplates(tenantId); + if (customEmailTemplateNames != null && !customEmailTemplateNames.isEmpty()) + templates.put(TemplateType.CustomEmail, customEmailTemplateNames); + + Map docTemplateNames = this.findDocumentTemplates(tenantId); + if (docTemplateNames != null && !docTemplateNames.isEmpty()) + templates.put(TemplateType.Document, docTemplateNames); + + return templates; + } + + protected Map findSystemEmailTemplates(Long tenantId) { + this.getLog().debug("Searching for all APS System Email Templates"); + + List matchingPatterns = this.buildPatterns(this.apsSystemEmailTemplateNames); + Map map = new HashMap<>(); + + ResultList templates = this.getApsApi().getTemplatesApi().getSystemEmailTemplates(tenantId); + for (EmailTemplateLight template : templates.getData()) { + for (Pattern pattern : matchingPatterns) { + if (pattern.matcher(template.getName()).matches()) + map.put(template.getName(), template); + } + } + + this.getLog().debug("Found APS System Email Templates: " + map.size()); + + return map; + } + + protected Map findCustomEmailTemplates(Long tenantId) { + this.getLog().debug("Searching for all APS Custom Email Templates"); + + List matchingPatterns = this.buildPatterns(this.apsCustomEmailTemplateNames); + Map map = new HashMap<>(); + int pageSize = 50; + int page = 1; + + ResultList templates = this.getApsApi().getTemplatesApi().getCustomEmailTemplates(null, (page-1) * pageSize, pageSize, null, tenantId); + while (!templates.getData().isEmpty()) { + for (EmailTemplateLight template : templates.getData()) { + for (Pattern pattern : matchingPatterns) { + if (pattern.matcher(template.getName()).matches()) + map.put(template.getName(), template); + } + } + + page++; + templates = this.getApsApi().getTemplatesApi().getCustomEmailTemplates(null, (page-1) * pageSize, pageSize, null, tenantId); + } + + this.getLog().debug("Found APS Custom Email Templates: " + map.size()); + + return map; + } + + protected Map findDocumentTemplates(Long tenantId) { + List matchingPatterns = this.buildPatterns(this.apsDocumentTemplateNames); + Map map = new HashMap<>(); + int pageSize = 50; + int page = 1; + + ResultList templates = this.getApsApi().getTemplatesApi().getDocumentTemplates(null, (page-1) * pageSize, pageSize, null, tenantId); + while (!templates.getData().isEmpty()) { + for (DocumentTemplateLight template : templates.getData()) { + for (Pattern pattern : matchingPatterns) { + if (pattern.matcher(template.getName()).matches()) + map.put(template.getName(), template); + } + } + + page++; + templates = this.getApsApi().getTemplatesApi().getDocumentTemplates(null, (page-1) * pageSize, pageSize, null, tenantId); + } + + this.getLog().debug("Found APS Document Templates: " + map.size()); + + return map; + } + + private List buildPatterns(String regexes) { + if (regexes == null) + return Collections.emptyList(); + + List patterns = new LinkedList<>(); + + for (String regex : regexes.split(",")) { + regex = regex.trim(); + if (regex.length() > 0) + patterns.add(Pattern.compile(regex)); + } + + return patterns; + } + +} diff --git a/src/main/java/com/inteligr8/maven/aps/modeling/goal/DownloadAppGoal.java b/src/main/java/com/inteligr8/maven/aps/modeling/goal/DownloadAppGoal.java index d37f73b..3d1b174 100644 --- a/src/main/java/com/inteligr8/maven/aps/modeling/goal/DownloadAppGoal.java +++ b/src/main/java/com/inteligr8/maven/aps/modeling/goal/DownloadAppGoal.java @@ -37,7 +37,7 @@ import com.inteligr8.alfresco.activiti.ApsPublicRestApiJerseyImpl; */ @Mojo( name = "download-app", threadSafe = true ) @Component( role = org.apache.maven.plugin.Mojo.class ) -public class DownloadAppGoal extends ApsAppAccessibleGoal { +public class DownloadAppGoal extends ApsAppAddressibleGoal { @Parameter( property = "aps-model.download.directory", required = true, defaultValue = "${project.build.directory}/aps" ) protected File zipDirectory; @@ -65,7 +65,7 @@ public class DownloadAppGoal extends ApsAppAccessibleGoal { this.getLog().debug("Creating APS Apps directory: " + this.zipDirectory); this.zipDirectory.mkdirs(); } else if (!this.zipDirectory.isDirectory()) { - throw new IllegalStateException("The 'targetDirectory' refers to a file and not a directory"); + throw new IllegalStateException("The 'zipDirectory' refers to a file and not a directory"); } } diff --git a/src/main/java/com/inteligr8/maven/aps/modeling/goal/DownloadTemplateGoal.java b/src/main/java/com/inteligr8/maven/aps/modeling/goal/DownloadTemplateGoal.java new file mode 100644 index 0000000..d9be23c --- /dev/null +++ b/src/main/java/com/inteligr8/maven/aps/modeling/goal/DownloadTemplateGoal.java @@ -0,0 +1,127 @@ +/* + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package com.inteligr8.maven.aps.modeling.goal; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; +import java.util.Map.Entry; + +import javax.ws.rs.core.Response; + +import org.apache.commons.io.FileUtils; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.codehaus.plexus.component.annotations.Component; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.inteligr8.alfresco.activiti.model.BaseTemplateLight; +import com.inteligr8.alfresco.activiti.model.DocumentTemplateLight; +import com.inteligr8.alfresco.activiti.model.EmailTemplate; +import com.inteligr8.maven.aps.modeling.normalizer.ApsTemplateJsonNormalizer; +import com.inteligr8.maven.aps.modeling.util.ModelUtil; + +/** + * A class that implements an APS service download goal. + * + * This goal will simply download the APS templates with the specified names + * from the specified APS service. + * + * @author brian@inteligr8.com + */ +@Mojo( name = "download-template", threadSafe = true ) +@Component( role = org.apache.maven.plugin.Mojo.class ) +public class DownloadTemplateGoal extends ApsTemplateAddressibleGoal { + + @Parameter( property = "aps-model.download.directory", required = true, defaultValue = "${project.build.directory}/aps" ) + protected File templateDirectory; + + @Parameter( property = "aps-model.normalize" ) + protected boolean normalize; + + @Parameter( property = "aps-model.normalize.diffFriendly" ) + protected boolean diffFriendly; + + @Override + public void executeEnabled() throws MojoExecutionException, MojoFailureException { + this.validateTargetDirectory(); + + Long tenantId = this.findTenantId(); + + try { + Map> templates = this.findTemplates(tenantId); + for (TemplateType ttype : templates.keySet()) { + this.getLog().debug("Downloading templates: " + ttype); + + for (Entry template : templates.get(ttype).entrySet()) { + switch (ttype) { + case Document: + ObjectNode djson = ModelUtil.getInstance().readPojo(template.getValue()); + File dfile = new File(this.templateDirectory, template.getValue().getId() + ".doc-template.json"); + ModelUtil.getInstance().writeJson(djson, dfile, this.diffFriendly); + if (this.normalize) + new ApsTemplateJsonNormalizer(this.diffFriendly).normalizeFile(dfile, null); + + File dfilebin = new File(this.templateDirectory, template.getValue().getId() + ".doc-template.docx"); + + Response response = this.getApsApi().getTemplatesApi().getDocumentTemplate((DocumentTemplateLight) template.getValue()); + try { + InputStream istream = (InputStream) response.getEntity(); + try { + FileUtils.copyInputStreamToFile(istream, dfilebin); + } finally { + istream.close(); + } + } finally { + response.close(); + } + break; + case CustomEmail: + EmailTemplate etemplate = this.getApsApi().getTemplatesApi().getCustomEmailTemplate(template.getValue().getId(), tenantId); + ObjectNode ejson = ModelUtil.getInstance().readPojo(etemplate); + File efile = new File(this.templateDirectory, template.getValue().getId() + ".custom-email-template.json"); + ModelUtil.getInstance().writeJson(ejson, efile, this.diffFriendly); + if (this.normalize) + new ApsTemplateJsonNormalizer(this.diffFriendly).normalizeFile(efile, null); + + break; + case SystemEmail: + EmailTemplate stemplate = this.getApsApi().getTemplatesApi().getSystemEmailTemplate(template.getValue().getName(), tenantId); + ObjectNode sjson = ModelUtil.getInstance().readPojo(stemplate); + File sfile = new File(this.templateDirectory, template.getValue().getName() + ".system-email-template.json"); + ModelUtil.getInstance().writeJson(sjson, sfile, this.diffFriendly); + if (this.normalize) + new ApsTemplateJsonNormalizer(this.diffFriendly).normalizeFile(sfile, null); + } + } + } + } catch (IOException ie) { + throw new MojoExecutionException("The downloaded APS templates could not be saved", ie); + } + } + + protected void validateTargetDirectory() { + if (!this.templateDirectory.exists()) { + this.getLog().debug("Creating APS template directory: " + this.templateDirectory); + this.templateDirectory.mkdirs(); + } else if (!this.templateDirectory.isDirectory()) { + throw new IllegalStateException("The 'templateDirectory' refers to a file and not a directory"); + } + } + +} diff --git a/src/main/java/com/inteligr8/maven/aps/modeling/goal/PublishAppGoal.java b/src/main/java/com/inteligr8/maven/aps/modeling/goal/PublishAppGoal.java index 5541a17..7803c16 100644 --- a/src/main/java/com/inteligr8/maven/aps/modeling/goal/PublishAppGoal.java +++ b/src/main/java/com/inteligr8/maven/aps/modeling/goal/PublishAppGoal.java @@ -35,7 +35,7 @@ import com.inteligr8.alfresco.activiti.model.AppDefinitionPublishRepresentation; */ @Mojo( name = "publish-app", threadSafe = true ) @Component( role = org.apache.maven.plugin.Mojo.class ) -public class PublishAppGoal extends ApsAppAccessibleGoal { +public class PublishAppGoal extends ApsAppAddressibleGoal { @Parameter( property = "aps-model.publish.comment", required = true, defaultValue = "Automated by 'aps-model-maven-plugin'" ) protected String comment; diff --git a/src/main/java/com/inteligr8/maven/aps/modeling/goal/TranslateAppGoal.java b/src/main/java/com/inteligr8/maven/aps/modeling/goal/TranslateAppGoal.java index e305016..742377e 100644 --- a/src/main/java/com/inteligr8/maven/aps/modeling/goal/TranslateAppGoal.java +++ b/src/main/java/com/inteligr8/maven/aps/modeling/goal/TranslateAppGoal.java @@ -33,7 +33,7 @@ import com.inteligr8.maven.aps.modeling.translator.ApsAppTranslator; * This goal will translate all the JSON and XML files in an APS App to match * the environment referenced by the specified APS App. This relies on all APS * model elements (apps, processes, and forms) to have unique names. The names - * of those mdoel elements are used to remap IDs between environments. + * of those model elements are used to remap IDs between environments. * * APS does not enforce a unique name constraint. But it is good practice to * avoid using the same name anyhow. This plugin will just make you do it. It @@ -43,7 +43,7 @@ import com.inteligr8.maven.aps.modeling.translator.ApsAppTranslator; */ @Mojo( name = "translate-app", threadSafe = true ) @Component( role = org.apache.maven.plugin.Mojo.class ) -public class TranslateAppGoal extends ApsAppAccessibleGoal { +public class TranslateAppGoal extends ApsAppAddressibleGoal { @Parameter( property = "aps-model.app.directory", required = true, defaultValue = "${project.build.directory}/aps/app" ) protected File unzipDirectory; diff --git a/src/main/java/com/inteligr8/maven/aps/modeling/goal/TranslateTemplateGoal.java b/src/main/java/com/inteligr8/maven/aps/modeling/goal/TranslateTemplateGoal.java new file mode 100644 index 0000000..4b24f8b --- /dev/null +++ b/src/main/java/com/inteligr8/maven/aps/modeling/goal/TranslateTemplateGoal.java @@ -0,0 +1,95 @@ +/* + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package com.inteligr8.maven.aps.modeling.goal; + +import java.io.File; +import java.io.IOException; + +import org.apache.commons.io.FileUtils; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.codehaus.plexus.component.annotations.Component; + +import com.inteligr8.maven.aps.modeling.crawler.ApsTemplateCrawler; +import com.inteligr8.maven.aps.modeling.translator.ApsTemplateTranslator; + +/** + * A class that implements an APS template translation goal. + * + * This goal will translate all the JSON template files to match the + * environment referenced by the specified APS service. This relies on all APS + * templates having unique names. The names of those templates are used to + * remap IDs between environments. + * + * @author brian@inteligr8.com + */ +@Mojo( name = "translate-template", threadSafe = true ) +@Component( role = org.apache.maven.plugin.Mojo.class ) +public class TranslateTemplateGoal extends ApsAddressibleGoal { + + @Parameter( property = "aps-model.template.directory", required = true, defaultValue = "${project.build.directory}/aps" ) + protected File templateDirectory; + + @Parameter( property = "aps-model.translatedTemplate.directory", required = false, defaultValue = "${project.build.directory}/aps" ) + protected File finalDirectory; + + @Parameter( property = "aps-model.translate.overwrite", required = true, defaultValue = "true" ) + protected boolean overwrite = true; + + @Parameter( property = "aps-model.translate.charset", required = true, defaultValue = "utf-8" ) + protected String charsetName = "utf-8"; + + @Override + public void executeEnabled() throws MojoExecutionException, MojoFailureException { + this.validateApsTemplateDirectory(); + this.validateTargetDirectory(); + + try { + if (!this.templateDirectory.equals(this.finalDirectory)) { + FileUtils.copyDirectory(this.templateDirectory, this.finalDirectory); + } + + ApsTemplateTranslator translator = new ApsTemplateTranslator(this.getApsApi()); + translator.buildIndexes(); + + ApsTemplateCrawler crawler = new ApsTemplateCrawler(this.finalDirectory, true); + crawler.execute(translator); + } catch (IOException ie) { + throw new MojoExecutionException("An I/O issue occurred", ie); + } catch (IllegalArgumentException iae) { + throw new MojoExecutionException("The input is not supported", iae); + } catch (IllegalStateException ise) { + throw new MojoExecutionException("The state of system is not supported", ise); + } + } + + protected void validateApsTemplateDirectory() throws MojoExecutionException { + if (!this.templateDirectory.exists()) + throw new MojoExecutionException("The 'templateDirectory' does not exist: " + this.templateDirectory); + if (!this.templateDirectory.isDirectory()) + throw new MojoExecutionException("The 'templateDirectory' is not a directory: " + this.templateDirectory); + } + + protected void validateTargetDirectory() throws MojoExecutionException { + if (!this.finalDirectory.exists()) { + this.finalDirectory.mkdirs(); + } else if (!this.finalDirectory.isDirectory()) { + throw new MojoExecutionException("The 'finalDirectory' is not a directory: " + this.finalDirectory); + } + } + +} diff --git a/src/main/java/com/inteligr8/maven/aps/modeling/goal/UploadAppGoal.java b/src/main/java/com/inteligr8/maven/aps/modeling/goal/UploadAppGoal.java index 9396a6a..4dce552 100644 --- a/src/main/java/com/inteligr8/maven/aps/modeling/goal/UploadAppGoal.java +++ b/src/main/java/com/inteligr8/maven/aps/modeling/goal/UploadAppGoal.java @@ -42,7 +42,7 @@ import com.inteligr8.alfresco.activiti.model.FileMultipartJersey; */ @Mojo( name = "upload-app", threadSafe = true ) @Component( role = org.apache.maven.plugin.Mojo.class ) -public class UploadAppGoal extends ApsAppAccessibleGoal { +public class UploadAppGoal extends ApsAppAddressibleGoal { @Parameter( property = "aps-model.upload.directory", required = true, defaultValue = "${project.build.directory}/aps" ) protected File zipDirectory; diff --git a/src/main/java/com/inteligr8/maven/aps/modeling/goal/UploadTemplateGoal.java b/src/main/java/com/inteligr8/maven/aps/modeling/goal/UploadTemplateGoal.java new file mode 100644 index 0000000..5ffc08c --- /dev/null +++ b/src/main/java/com/inteligr8/maven/aps/modeling/goal/UploadTemplateGoal.java @@ -0,0 +1,118 @@ +/* + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package com.inteligr8.maven.aps.modeling.goal; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.text.ParseException; + +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.codehaus.plexus.component.annotations.Component; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.inteligr8.alfresco.activiti.model.DocumentTemplateLight; +import com.inteligr8.alfresco.activiti.model.EmailTemplate; +import com.inteligr8.alfresco.activiti.model.FileMultipartJersey; +import com.inteligr8.maven.aps.modeling.util.ModelUtil; + +/** + * A class that implements an APS service upload goal. + * + * This goal will simply upload the APS templates within the specified template + * directory to the specified APS service. Any IDs specified in the uploaded + * templates must match existing IDs for them to properly version. That is the + * main purpose of this plugin and can be achieved using the + * 'translate-template' goal. + * + * @author brian@inteligr8.com + */ +@Mojo( name = "upload-template", threadSafe = true ) +@Component( role = org.apache.maven.plugin.Mojo.class ) +public class UploadTemplateGoal extends ApsAddressibleGoal { + + @Parameter( property = "aps-model.upload.directory", required = true, defaultValue = "${project.build.directory}/aps" ) + protected File templateDirectory; + + protected final int bufferSize = 128 * 1024; + + @Override + public void executeEnabled() throws MojoExecutionException, MojoFailureException { + this.validateSourceDirectory(); + + Long tenantId = this.findTenantId(); + + for (File file : this.templateDirectory.listFiles()) { + if (!file.getName().endsWith(".json")) { + this.getLog().debug("Ignoring file: " + file.getName()); + continue; + } + + try { + if (file.getName().endsWith(".doc-template.json")) { + DocumentTemplateLight template = ModelUtil.getInstance().writePojo(ModelUtil.getInstance().readJsonAsMap(file), DocumentTemplateLight.class); + + File docxfile = new File(file.getParent(), file.getName().substring(0, file.getName().length() - ".json".length()) + ".docx"); + if (!docxfile.exists()) + throw new FileNotFoundException("The file, '" + docxfile.getName() + "' was expected and not found"); + + FileInputStream fistream = new FileInputStream(docxfile); + BufferedInputStream bistream = new BufferedInputStream(fistream, this.bufferSize); + try { + FileMultipartJersey multipart = FileMultipartJersey.from(docxfile.getName(), bistream); + if (template.getId() == null) { + this.getApsApi().getTemplatesJerseyApi().createDocumentTemplate(tenantId, multipart); + } else { + this.getApsApi().getTemplatesJerseyApi().updateDocumentTemplate(template.getId(), tenantId, multipart); + } + } finally { + bistream.close(); + fistream.close(); + } + } else if (file.getName().endsWith(".custom-email-template.json")) { + EmailTemplate template = ModelUtil.getInstance().writePojo(ModelUtil.getInstance().readJsonAsMap(file), EmailTemplate.class); + if (template.getId() == null) { + this.getApsApi().getTemplatesJerseyApi().createCustomEmailTemplate(template); + } else { + this.getApsApi().getTemplatesJerseyApi().updateCustomEmailTemplate(template.getId(), template); + } + } else if (file.getName().endsWith(".system-email-template.json")) { + EmailTemplate template = ModelUtil.getInstance().writePojo(ModelUtil.getInstance().readJsonAsMap(file), EmailTemplate.class); + this.getApsApi().getTemplatesJerseyApi().updateSystemEmailTemplate(template.getName(), template); + } + } catch (JsonProcessingException jpe) { + throw new MojoExecutionException("The APS templates JSON files could not be parsed", jpe); + } catch (IOException ie) { + throw new MojoExecutionException("The APS templates could not be uploaded", ie); + } catch (ParseException pe) { + throw new MojoFailureException("This should never happen", pe); + } + } + } + + protected void validateSourceDirectory() { + if (!this.templateDirectory.exists()) { + throw new IllegalStateException("The 'templateDirectory' does not exist: " + this.templateDirectory); + } else if (!this.templateDirectory.isDirectory()) { + throw new IllegalStateException("The 'templateDirectory' is not a directory: " + this.templateDirectory); + } + } + +} diff --git a/src/main/java/com/inteligr8/maven/aps/modeling/normalizer/ApsTemplateJsonNormalizer.java b/src/main/java/com/inteligr8/maven/aps/modeling/normalizer/ApsTemplateJsonNormalizer.java new file mode 100644 index 0000000..659fd18 --- /dev/null +++ b/src/main/java/com/inteligr8/maven/aps/modeling/normalizer/ApsTemplateJsonNormalizer.java @@ -0,0 +1,73 @@ +/* + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package com.inteligr8.maven.aps.modeling.normalizer; + +import java.io.File; +import java.io.IOException; +import java.util.Arrays; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.inteligr8.maven.aps.modeling.util.ModelUtil; + +/** + * This class implements an APS template JSON configuration file normalizer. + * + * This will remove the 'created' date of each defined template. + * + * @author brian@inteligr8.com + */ +public class ApsTemplateJsonNormalizer implements ApsFileNormalizer { + + private final Logger logger = LoggerFactory.getLogger(ApsTemplateJsonNormalizer.class); + + private final boolean enableSorting; + + /** + * This constructor initializes the default normalizer with or without + * sorting. + * + * The sorting feature is available for a better "diff" experience. If + * you intend to commit the APS App configurations to Git, you will want to + * enable sorting. + * + * @param enableSorting true to re-order JSON objects; false to keep as-is. + */ + public ApsTemplateJsonNormalizer(boolean enableSorting) { + this.enableSorting = enableSorting; + } + + @Override + public void normalizeFile(File file, String _unused) throws IOException { + this.logger.debug("Normalizing template JSON file: {}", file); + + ObjectNode templateJson = (ObjectNode) ModelUtil.getInstance().readJson(file); + boolean changed = this.transformModel(templateJson); + + if (changed) + ModelUtil.getInstance().writeJson(templateJson, file, this.enableSorting); + } + + private boolean transformModel(ObjectNode jsonModel) { + this.logger.trace("Removing excess template meta-data: {}", jsonModel.get("name")); + + int fields = jsonModel.size(); + jsonModel.remove(Arrays.asList("created", "createdBy")); + return jsonModel.size() < fields; + } + +} diff --git a/src/main/java/com/inteligr8/maven/aps/modeling/normalizer/ApsTemplateNormalizer.java b/src/main/java/com/inteligr8/maven/aps/modeling/normalizer/ApsTemplateNormalizer.java new file mode 100644 index 0000000..b839983 --- /dev/null +++ b/src/main/java/com/inteligr8/maven/aps/modeling/normalizer/ApsTemplateNormalizer.java @@ -0,0 +1,48 @@ +/* + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package com.inteligr8.maven.aps.modeling.normalizer; + +import com.inteligr8.maven.aps.modeling.crawler.ApsFileTransformer; +import com.inteligr8.maven.aps.modeling.crawler.ApsTemplateCrawlable; + +/** + * This class defines an APS template normalizer. + * + * @author brian@inteligr8.com + */ +public class ApsTemplateNormalizer implements ApsTemplateCrawlable { + + private final boolean enableSorting; + + /** + * This constructor initializes the default normalizer with or without + * sorting. + * + * The sorting feature is available for a better "diff" experience. If + * you intend to commit the APS template configurations to Git, you will + * want to enable sorting. + * + * @param enableSorting true to re-order JSON objects; false to keep as-is. + */ + public ApsTemplateNormalizer(boolean enableSorting) { + this.enableSorting = enableSorting; + } + + @Override + public ApsFileTransformer getTemplateJsonTransformer() { + return new ApsTemplateJsonNormalizer(this.enableSorting); + } + +} diff --git a/src/main/java/com/inteligr8/maven/aps/modeling/translator/ApsTemplateJsonTranslator.java b/src/main/java/com/inteligr8/maven/aps/modeling/translator/ApsTemplateJsonTranslator.java new file mode 100644 index 0000000..716ea0b --- /dev/null +++ b/src/main/java/com/inteligr8/maven/aps/modeling/translator/ApsTemplateJsonTranslator.java @@ -0,0 +1,90 @@ +/* + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package com.inteligr8.maven.aps.modeling.translator; + +import java.io.File; +import java.io.IOException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.inteligr8.maven.aps.modeling.util.Index; +import com.inteligr8.maven.aps.modeling.util.ModelUtil; + +/** + * This class implements an APS template JSON configuration file translator. + * + * @author brian@inteligr8.com + */ +public class ApsTemplateJsonTranslator implements ApsFileTranslator { + + private final Logger logger = LoggerFactory.getLogger(ApsTemplateJsonTranslator.class); + private final Index apsDocumentTemplateIndex; + private final Index apsCustomEmailTemplateIndex; + + /** + * This constructor initializes the default translator. + * + * @param api An APS API reference. + */ + public ApsTemplateJsonTranslator( + Index apsDocumentTemplateIndex, + Index apsCustomEmailTemplateIndex) { + this.apsDocumentTemplateIndex = apsDocumentTemplateIndex; + this.apsCustomEmailTemplateIndex = apsCustomEmailTemplateIndex; + } + + @Override + public void translateFile(File file, String _unsued1, Long _unused2) throws IOException { + this.logger.debug("Translating JSON file: {}", file); + + boolean changed = false; + + ObjectNode json = (ObjectNode) ModelUtil.getInstance().readJson(file); + + boolean isDocumentTemplate = json.get("mimeType") != null; + String templateName = json.get("name").asText(); + this.logger.trace("Found template name '{}' in APS template file: {}", templateName, file); + + Long oldTemplateId = null; + if (json.hasNonNull("id")) { + oldTemplateId = json.get("id").asLong(); + this.logger.trace("Found template ID '{}' in APS template file: {}", oldTemplateId, file); + } + + Long newTemplateId = null; + if (isDocumentTemplate) { + newTemplateId = this.apsDocumentTemplateIndex.getFirstKey(templateName); + } else { + newTemplateId = this.apsCustomEmailTemplateIndex.getFirstKey(templateName); + } + + if (newTemplateId == null) { + // new template; remove the key completely + json.remove("id"); + changed = true; + } else if (newTemplateId.equals(oldTemplateId)) { + // unchanged; nothing to do + } else { + json.put("id", newTemplateId); + changed = true; + } + + if (changed) + ModelUtil.getInstance().writeJson(json, file, false); + } + +} diff --git a/src/main/java/com/inteligr8/maven/aps/modeling/translator/ApsTemplateTranslator.java b/src/main/java/com/inteligr8/maven/aps/modeling/translator/ApsTemplateTranslator.java new file mode 100644 index 0000000..df5deb6 --- /dev/null +++ b/src/main/java/com/inteligr8/maven/aps/modeling/translator/ApsTemplateTranslator.java @@ -0,0 +1,149 @@ +/* + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package com.inteligr8.maven.aps.modeling.translator; + +import java.io.IOException; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.inteligr8.activiti.model.ResultList; +import com.inteligr8.alfresco.activiti.ApsPublicRestApi; +import com.inteligr8.alfresco.activiti.model.BaseTemplateLight; +import com.inteligr8.alfresco.activiti.model.Tenant; +import com.inteligr8.maven.aps.modeling.crawler.ApsFileTransformer; +import com.inteligr8.maven.aps.modeling.crawler.ApsTemplateCrawlable; +import com.inteligr8.maven.aps.modeling.util.Index; + +/** + * This class defines an APS App package translator. + * + * An APS App package is a ZIP file that contains multiple files in a + * predictable folder hierachy with predictable file names. It is effectively + * an APS App interchange format specification. + * + * A package must have at least a single configuration file at the root of the + * ZIP package in the JSON format. It must be named the APS App name. That + * file will then reference all the model elements contained in the ZIP. Any + * model elements not referenced will simply be ignored by APS and by this + * plugin. + * + * This class has methods that provide translator for all the various model + * elements in an APS App package. + * + * @author brian@inteligr8.com + */ +public class ApsTemplateTranslator implements ApsTemplateCrawlable { + + private final Logger logger = LoggerFactory.getLogger(ApsTemplateTranslator.class); + private final ApsPublicRestApi api; + + private boolean indexesBuilt = false; + private Index apsDocumentTemplateIndex; + private Index apsCustomEmailTemplateIndex; + + public ApsTemplateTranslator(ApsPublicRestApi api) { + this.api = api; + } + + /** + * This method initializes the data required from the APS Service for the + * function of this class. + * + * @throws IOException A network I/O related issue occurred. + */ + public synchronized void buildIndexes() throws IOException { + if (this.indexesBuilt) + return; + + this.logger.info("Building indexes ..."); + + long tenantId = this.findTenantId(); + this.logger.debug("APS tenant ID: {}", tenantId); + + this.apsDocumentTemplateIndex = this.buildApsDocumentTemplateIndex(tenantId); + this.logLarge("APS document templates: {}", this.apsDocumentTemplateIndex); + + this.apsCustomEmailTemplateIndex = this.buildApsCustomEmailTemplateIndex(tenantId); + this.logLarge("APS custom email templates: {}", this.apsCustomEmailTemplateIndex); + + this.indexesBuilt = true; + } + + private void logLarge(String message, Index index) { + if (this.logger.isTraceEnabled()) { + this.logger.trace(message, index); + } else { + this.logger.debug(message, index.valueSet()); + } + } + + @Override + public ApsFileTransformer getTemplateJsonTransformer() { + if (!this.indexesBuilt) + throw new IllegalStateException("The indexes are never built"); + + return new ApsTemplateJsonTranslator( + this.apsDocumentTemplateIndex, + this.apsCustomEmailTemplateIndex); + } + + protected Long findTenantId() { + List tenants = this.api.getAdminApi().getTenants(); + return (tenants == null || tenants.isEmpty()) ? null : tenants.iterator().next().getId(); + } + + protected Index buildApsDocumentTemplateIndex(Long tenantId) { + int perPage = 50; + int page = 1; + + ResultList templates = this.api.getTemplatesApi().getDocumentTemplates(null, (page-1)*perPage, perPage, null, tenantId); + Index index = new Index<>(templates.getTotal() / 2, false); + + while (!templates.getData().isEmpty()) { + this.logger.debug("APS document templates found: {}-{} out of {}", templates.getStart(), (templates.getStart() + templates.getSize()), templates.getTotal()); + + for (BaseTemplateLight template : templates.getData()) + index.put(template.getId(), template.getName()); + + page++; + templates = this.api.getTemplatesApi().getDocumentTemplates(null, (page-1)*perPage, perPage, null, tenantId); + } + + return index; + } + + protected Index buildApsCustomEmailTemplateIndex(Long tenantId) { + int perPage = 50; + int page = 1; + + ResultList templates = this.api.getTemplatesApi().getCustomEmailTemplates(null, (page-1)*perPage, perPage, null, tenantId); + Index index = new Index<>(templates.getTotal() / 2, false); + + while (!templates.getData().isEmpty()) { + this.logger.debug("APS document templates found: {}-{} out of {}", templates.getStart(), (templates.getStart() + templates.getSize()), templates.getTotal()); + + for (BaseTemplateLight template : templates.getData()) + index.put(template.getId(), template.getName()); + + page++; + templates = this.api.getTemplatesApi().getCustomEmailTemplates(null, (page-1)*perPage, perPage, null, tenantId); + } + + return index; + } + +} diff --git a/src/main/java/com/inteligr8/maven/aps/modeling/util/ModelUtil.java b/src/main/java/com/inteligr8/maven/aps/modeling/util/ModelUtil.java index 00ef5f0..0844eb5 100644 --- a/src/main/java/com/inteligr8/maven/aps/modeling/util/ModelUtil.java +++ b/src/main/java/com/inteligr8/maven/aps/modeling/util/ModelUtil.java @@ -52,6 +52,8 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.inteligr8.maven.aps.modeling.xml.DomNamespaceContext; /** @@ -73,8 +75,8 @@ public class ModelUtil { - private final ObjectMapper om = new ObjectMapper(); - private final ObjectMapper omsorted = new ObjectMapper(); + private final ObjectMapper om = new ObjectMapper().registerModule(new JavaTimeModule()); + private final ObjectMapper omsorted = new ObjectMapper().registerModule(new JavaTimeModule()); private final DocumentBuilderFactory dbfactory = DocumentBuilderFactory.newInstance(); private final DocumentBuilder docBuilder; private final XPathFactory xpathfactory = XPathFactory.newInstance(); @@ -296,6 +298,28 @@ public class ModelUtil { public Map readJsonAsMap(InputStream istream) throws IOException { return this.om.readValue(istream, Map.class); } + + /** + * This method reads/parses a Java POJO. + * + * @param o A Java POJO. + * @return A JSON node (array, object, or value). + * @throws IOException A stream I/O issue occurred. + */ + public ObjectNode readPojo(Object o) { + return this.om.convertValue(o, ObjectNode.class); + } + + /** + * This method reads/parses a Java POJO. + * + * @param o A Java POJO. + * @return A Java POJO as a map. + */ + @SuppressWarnings("unchecked") + public Map readPojoAsMap(Object o) { + return this.om.convertValue(o, Map.class); + } /** * This method formats/writes JSON to the specified file. @@ -384,6 +408,20 @@ public class ModelUtil { this.om.writeValue(ostream, map); } } + + /** + * This method formats/writes a Java POJO of the specified type using the + * specified map. + * + * @param The class of the type to create. + * @param map A Java map. + * @param type A Java class to create. + * @return A Java POJO instance. + * @throws IOException A file I/O issue occurred. + */ + public T writePojo(Map map, Class type) throws IOException { + return this.om.convertValue(map, type); + }