001package org.nasdanika.html.model.app.gen.maven; 002 003import java.io.File; 004import java.io.IOException; 005import java.net.URL; 006import java.util.ArrayList; 007import java.util.Collection; 008import java.util.HashMap; 009import java.util.List; 010import java.util.Map; 011import java.util.Map.Entry; 012import java.util.function.Function; 013import java.util.Objects; 014import java.util.stream.Collectors; 015 016import org.apache.maven.plugins.annotations.LifecyclePhase; 017import org.apache.maven.plugins.annotations.Mojo; 018import org.apache.maven.plugins.annotations.Parameter; 019import org.eclipse.emf.common.util.DiagnosticException; 020import org.eclipse.emf.common.util.URI; 021import org.eclipse.emf.ecore.EObject; 022import org.eclipse.emf.ecore.resource.ResourceSet; 023import org.nasdanika.common.Context; 024import org.nasdanika.common.DiagramGenerator; 025import org.nasdanika.common.DiagramGeneratorImpl; 026import org.nasdanika.common.NasdanikaException; 027import org.nasdanika.common.ProgressMonitor; 028import org.nasdanika.common.Util; 029import org.nasdanika.drawio.Document; 030import org.nasdanika.html.emf.RepresentationProcessor; 031import org.nasdanika.html.model.app.Action; 032import org.nasdanika.html.model.app.Link; 033import org.nasdanika.html.model.app.gen.ActionSiteGenerator; 034import org.nasdanika.html.model.app.gen.SiteGeneratorContributor; 035import org.nasdanika.html.model.app.util.AppDrawioResourceFactory; 036import org.nasdanika.maven.AbstractCommandMojo; 037import org.nasdanika.ncore.util.SemanticInfo; 038import org.nasdanika.ncore.util.SemanticRegistry; 039 040import io.github.azagniotov.matcher.AntPathMatcher; 041import io.github.azagniotov.matcher.AntPathMatcher.Builder; 042 043/** 044 * Generates action site. 045 */ 046@Mojo(name = "generate-action-site", defaultPhase = LifecyclePhase.SITE) 047public class ActionSiteGeneratorMojo extends AbstractCommandMojo { 048 049 /** 050 * Directory to output generated site 051 */ 052 @Parameter(defaultValue = "target/action-site") 053 private File outputDirectory; 054 055 /** 056 * Working directory for storing intermediate files 057 */ 058 @Parameter(defaultValue = "target/action-site-work-dir") 059 private File workDirectory; 060 061 /** 062 * If true the working directory is cleaned before generation 063 */ 064 @Parameter 065 private boolean cleanWorkDir; 066 067 /** 068 * URI of the action from which to generate a site. Resolved relative to the project base directory 069 */ 070 @Parameter(required = true) 071 private String action; 072 073 /** 074 * URI of the page template. Resolved relative to the project base directory 075 */ 076 @Parameter(required = true) 077 private String pageTemplate; 078 079 /** 080 * Site map domain/URL to output to sitemap.xml. 081 * Site map is not generated if this parameter is not set. 082 */ 083 @Parameter 084 private String siteMapDomain; 085 086 /** 087 * URL of the Drawio viewer script for rendering Drawio diagrams. 088 * Set if you are hosting your own Drawio site. 089 */ 090 @Parameter 091 private String drawioViewer; 092 093 /** 094 * Number of known/expected errors. E.g. some blank pages or broken links. Build fails if the number of actual errors reported is different from this parameter. 095 */ 096 @Parameter(required = false) 097 private int errors; 098 099 /** 100 * Pluggable diagram generators. 101 */ 102 @Parameter 103 private List<DiagramGenerator> diagramGenerators; 104 105 /** 106 * Contributors to site generation. 107 */ 108 @Parameter 109 private List<SiteGeneratorContributor> contributors; 110 111 /** 112 * Files to keep in the output directory. 113 */ 114 @Parameter 115 private List<String> outputCleanExcludes; 116 117 /** 118 * URL's of JSON resources with information about external semantic elements. Such JSON resources are created as part of site generation. 119 * They are named semantic-info.json 120 * Semantic infos are used to link model elements to externally defined element by URI. It is similar to how Java modules require other modules and then 121 * classes in that module may reference elements from the required modules by their fully qualified names. 122 * 123 * Semantic info may be created programmatically using {@link SemanticInfo} and {@link SemanticRegistry} classes. 124 */ 125 @Parameter 126 private List<String> semanticInfos; 127 128 /** 129 * If true, generation is performed in parallel threads. 130 */ 131 @Parameter(required = false) 132 protected boolean parallel; 133 134 @Override 135 protected void execute(Context context, ProgressMonitor progressMonitor) { 136 ActionSiteGenerator actionSiteGenerator = createActionSiteGenerator(context, progressMonitor); 137 138 File baseDir = project.getBasedir(); 139 URI baseDirURI = URI.createFileURI(baseDir.getAbsolutePath()).appendSegment(""); 140 141 URI actionURI = URI.createURI(action).resolve(baseDirURI); 142 URI pageTemplateURI = URI.createURI(pageTemplate).resolve(baseDirURI); 143 144 try { 145 Map<String, Collection<String>> errors = actionSiteGenerator.generate( 146 actionURI, 147 pageTemplateURI, 148 siteMapDomain, 149 outputDirectory, 150 workDirectory, 151 cleanWorkDir); 152 153 int errorCount = 0; 154 for (Entry<String, Collection<String>> ee: errors.entrySet()) { 155 getLog().error(ee.getKey()); 156 for (String error: ee.getValue()) { 157 ++errorCount; 158 getLog().error("\t" + error); 159 } 160 } 161 if (errorCount != this.errors) { 162 String message = "There are " + errorCount + " site errors"; 163 getLog().error(message); 164 throw new NasdanikaException(message); 165 } 166 } catch (IOException | DiagnosticException ex) { 167 throw new NasdanikaException(ex); 168 } 169 } 170 171 protected boolean isDeleteOutputPath(String path) { 172 if (outputCleanExcludes != null) { 173 Builder builder = new AntPathMatcher.Builder(); 174 AntPathMatcher matcher = builder.build(); 175 for (String exclude: outputCleanExcludes) { 176 if (matcher.isMatch(exclude, path)) { 177 return false; 178 } 179 } 180 } 181 182 return true; 183 } 184 185 protected ActionSiteGenerator createActionSiteGenerator(Context context, ProgressMonitor progressMonitor) { 186 File baseDir = project.getBasedir(); 187 URI baseDirURI = URI.createFileURI(baseDir.getAbsolutePath()).appendSegment(""); 188 SemanticRegistry semanticRegistry = new SemanticRegistry(); 189 if (semanticInfos != null) { 190 for (String smLocation: semanticInfos) { 191 URI semanticInfoURI = URI.createURI(smLocation).resolve(baseDirURI); 192 try { 193 semanticRegistry.load(new URL(semanticInfoURI.toString())); 194 } catch (IOException e) { 195 String message = "Could not load semantic info from " + semanticInfoURI; 196 getLog().error(message, e); 197 throw new NasdanikaException(message, e); 198 } 199 } 200 } 201 202 return new ActionSiteGenerator() { 203 204 @Override 205 protected boolean isDeleteOutputPath(String path) { 206 return ActionSiteGeneratorMojo.this.isDeleteOutputPath(path); 207 } 208 209 { 210 this.parallel = ActionSiteGeneratorMojo.this.parallel; 211 } 212 213 @Override 214 protected Iterable<SemanticInfo> getSemanticInfos() { 215 return semanticRegistry 216 .stream() 217 .filter(SemanticInfo.class::isInstance) 218 .map(SemanticInfo.class::cast) 219 .toList(); 220 } 221 222 @Override 223 protected boolean isSemanticInfoLink(Link link) { 224 if (link == null || Util.isBlank(link.getLocation())) { 225 return false; 226 } 227 String linkLocation = link.getLocation(); 228 return semanticRegistry 229 .stream() 230 .filter(SemanticInfo.class::isInstance) 231 .map(SemanticInfo.class::cast) 232 .map(SemanticInfo::getLocation) 233 .filter(Objects::nonNull) 234 .map(Object::toString) 235 .filter(linkLocation::equals) 236 .findFirst() 237 .isPresent(); 238 } 239 240 @Override 241 protected Context createContext(ProgressMonitor progressMonitor) { 242 DiagramGenerator diagramGenerator; 243 if (Util.isBlank(drawioViewer)) { 244 diagramGenerator = DiagramGenerator.INSTANCE; 245 } else { 246 diagramGenerator = new DiagramGeneratorImpl() { 247 248 protected String getDrawioViewer() { 249 return drawioViewer; 250 }; 251 252 }; 253 } 254 if (diagramGenerators != null) { 255 for (DiagramGenerator dg: diagramGenerators) { 256 diagramGenerator = dg.compose(diagramGenerator); 257 } 258 } 259 260 RepresentationProcessor representationProcessor = new RepresentationProcessor() { 261 262 @Override 263 public Document processDrawioRepresentation( 264 Document document, 265 Action action, 266 Function<URI, EObject> semanticLinkResolver, 267 org.nasdanika.html.emf.EObjectActionResolver.Context context, 268 ProgressMonitor progressMonitor) { 269 270 for (SiteGeneratorContributor contributor: getContributors()) { 271 document = contributor.processDrawioRepresentation(document, action, semanticLinkResolver, context, progressMonitor); 272 } 273 274 return document; 275 } 276 277 }; 278 279 return context 280 .compose(Context.singleton(DiagramGenerator.class, diagramGenerator)) 281 .compose(Context.singleton(RepresentationProcessor.class, representationProcessor)) 282 .compose(super.createContext(progressMonitor)); 283 } 284 285 @Override 286 protected ProgressMonitor createProgressMonitor() { 287 return progressMonitor; 288 } 289 290 @Override 291 protected Collection<SiteGeneratorContributor> getContributors() { 292 Collection<SiteGeneratorContributor> allContributors = new ArrayList<>(super.getContributors()); 293 if (ActionSiteGeneratorMojo.this.contributors != null) { 294 allContributors.addAll(ActionSiteGeneratorMojo.this.contributors); 295 } 296 return allContributors; 297 } 298 299 }; 300 } 301 302}