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 * Register AppDrawioResourceFactory instead DrawioResourceFactory 051 */ 052 @Parameter(defaultValue = "true") 053 private boolean appDrawioFactory; 054 055 056 /** 057 * Directory to output generated site 058 */ 059 @Parameter(defaultValue = "target/action-site") 060 private File outputDirectory; 061 062 /** 063 * Working directory for storing intermediate files 064 */ 065 @Parameter(defaultValue = "target/action-site-work-dir") 066 private File workDirectory; 067 068 /** 069 * If true the working directory is cleaned before generation 070 */ 071 @Parameter 072 private boolean cleanWorkDir; 073 074 /** 075 * URI of the action from which to generate a site. Resolved relative to the project base directory 076 */ 077 @Parameter(required = true) 078 private String action; 079 080 /** 081 * URI of the page template. Resolved relative to the project base directory 082 */ 083 @Parameter(required = true) 084 private String pageTemplate; 085 086 /** 087 * Site map domain/URL to output to sitemap.xml. 088 * Site map is not generated if this parameter is not set. 089 */ 090 @Parameter 091 private String siteMapDomain; 092 093 /** 094 * URL of the Drawio viewer script for rendering Drawio diagrams. 095 * Set if you are hosting your own Drawio site. 096 */ 097 @Parameter 098 private String drawioViewer; 099 100 /** 101 * 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. 102 */ 103 @Parameter(required = false) 104 private int errors; 105 106 /** 107 * Pluggable diagram generators. 108 */ 109 @Parameter 110 private List<DiagramGenerator> diagramGenerators; 111 112 /** 113 * Contributors to site generation. 114 */ 115 @Parameter 116 private List<SiteGeneratorContributor> contributors; 117 118 /** 119 * Files to keep in the output directory. 120 */ 121 @Parameter 122 private List<String> outputCleanExcludes; 123 124 /** 125 * URL's of JSON resources with information about external semantic elements. Such JSON resources are created as part of site generation. 126 * They are named semantic-info.json 127 * 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 128 * classes in that module may reference elements from the required modules by their fully qualified names. 129 * 130 * Semantic info may be created programmatically using {@link SemanticInfo} and {@link SemanticRegistry} classes. 131 */ 132 @Parameter 133 private List<String> semanticInfos; 134 135 /** 136 * If true, generation is performed in parallel threads. 137 */ 138 @Parameter(required = false) 139 protected boolean parallel; 140 141 @Override 142 protected void execute(Context context, ProgressMonitor progressMonitor) { 143 ActionSiteGenerator actionSiteGenerator = createActionSiteGenerator(context, progressMonitor); 144 145 File baseDir = project.getBasedir(); 146 URI baseDirURI = URI.createFileURI(baseDir.getAbsolutePath()).appendSegment(""); 147 148 URI actionURI = URI.createURI(action).resolve(baseDirURI); 149 URI pageTemplateURI = URI.createURI(pageTemplate).resolve(baseDirURI); 150 151 try { 152 Map<String, Collection<String>> errors = actionSiteGenerator.generate( 153 actionURI, 154 pageTemplateURI, 155 siteMapDomain, 156 outputDirectory, 157 workDirectory, 158 cleanWorkDir); 159 160 int errorCount = 0; 161 for (Entry<String, Collection<String>> ee: errors.entrySet()) { 162 getLog().error(ee.getKey()); 163 for (String error: ee.getValue()) { 164 ++errorCount; 165 getLog().error("\t" + error); 166 } 167 } 168 if (errorCount != this.errors) { 169 String message = "There are " + errorCount + " site errors"; 170 getLog().error(message); 171 throw new NasdanikaException(message); 172 } 173 } catch (IOException | DiagnosticException ex) { 174 throw new NasdanikaException(ex); 175 } 176 } 177 178 protected boolean isDeleteOutputPath(String path) { 179 if (outputCleanExcludes != null) { 180 Builder builder = new AntPathMatcher.Builder(); 181 AntPathMatcher matcher = builder.build(); 182 for (String exclude: outputCleanExcludes) { 183 if (matcher.isMatch(exclude, path)) { 184 return false; 185 } 186 } 187 } 188 189 return true; 190 } 191 192 protected ActionSiteGenerator createActionSiteGenerator(Context context, ProgressMonitor progressMonitor) { 193 File baseDir = project.getBasedir(); 194 URI baseDirURI = URI.createFileURI(baseDir.getAbsolutePath()).appendSegment(""); 195 SemanticRegistry semanticRegistry = new SemanticRegistry(); 196 if (semanticInfos != null) { 197 for (String smLocation: semanticInfos) { 198 URI semanticInfoURI = URI.createURI(smLocation).resolve(baseDirURI); 199 try { 200 semanticRegistry.load(new URL(semanticInfoURI.toString())); 201 } catch (IOException e) { 202 String message = "Could not load semantic info from " + semanticInfoURI; 203 getLog().error(message, e); 204 throw new NasdanikaException(message, e); 205 } 206 } 207 } 208 209 return new ActionSiteGenerator() { 210 211 @Override 212 protected boolean isDeleteOutputPath(String path) { 213 return ActionSiteGeneratorMojo.this.isDeleteOutputPath(path); 214 } 215 216 { 217 this.parallel = ActionSiteGeneratorMojo.this.parallel; 218 } 219 220 @Override 221 protected ResourceSet createResourceSet(Context context, ProgressMonitor progressMonitor) { 222 ResourceSet resourceSet = super.createResourceSet(context, progressMonitor); 223 if (appDrawioFactory) { 224 resourceSet.getResourceFactoryRegistry().getExtensionToFactoryMap().put("drawio", new AppDrawioResourceFactory(uri -> resourceSet.getEObject(uri, true))); 225 } 226 return resourceSet; 227 } 228 229 @Override 230 protected Iterable<SemanticInfo> getSemanticInfos() { 231 return semanticRegistry 232 .stream() 233 .filter(SemanticInfo.class::isInstance) 234 .map(SemanticInfo.class::cast) 235 .toList(); 236 } 237 238 @Override 239 protected boolean isSemanticInfoLink(Link link) { 240 if (link == null || Util.isBlank(link.getLocation())) { 241 return false; 242 } 243 String linkLocation = link.getLocation(); 244 return semanticRegistry 245 .stream() 246 .filter(SemanticInfo.class::isInstance) 247 .map(SemanticInfo.class::cast) 248 .map(SemanticInfo::getLocation) 249 .filter(Objects::nonNull) 250 .map(Object::toString) 251 .filter(linkLocation::equals) 252 .findFirst() 253 .isPresent(); 254 } 255 256 @Override 257 protected Context createContext(ProgressMonitor progressMonitor) { 258 DiagramGenerator diagramGenerator; 259 if (Util.isBlank(drawioViewer)) { 260 diagramGenerator = DiagramGenerator.INSTANCE; 261 } else { 262 diagramGenerator = new DiagramGeneratorImpl() { 263 264 protected String getDrawioViewer() { 265 return drawioViewer; 266 }; 267 268 }; 269 } 270 if (diagramGenerators != null) { 271 for (DiagramGenerator dg: diagramGenerators) { 272 diagramGenerator = dg.compose(diagramGenerator); 273 } 274 } 275 276 RepresentationProcessor representationProcessor = new RepresentationProcessor() { 277 278 @Override 279 public Document processDrawioRepresentation( 280 Document document, 281 Action action, 282 Function<URI, EObject> semanticLinkResolver, 283 org.nasdanika.html.emf.EObjectActionResolver.Context context, 284 ProgressMonitor progressMonitor) { 285 286 for (SiteGeneratorContributor contributor: getContributors()) { 287 document = contributor.processDrawioRepresentation(document, action, semanticLinkResolver, context, progressMonitor); 288 } 289 290 return document; 291 } 292 293 }; 294 295 return context 296 .compose(Context.singleton(DiagramGenerator.class, diagramGenerator)) 297 .compose(Context.singleton(RepresentationProcessor.class, representationProcessor)) 298 .compose(super.createContext(progressMonitor)); 299 } 300 301 @Override 302 protected ProgressMonitor createProgressMonitor() { 303 return progressMonitor; 304 } 305 306 @Override 307 protected Collection<SiteGeneratorContributor> getContributors() { 308 Collection<SiteGeneratorContributor> allContributors = new ArrayList<>(super.getContributors()); 309 if (ActionSiteGeneratorMojo.this.contributors != null) { 310 allContributors.addAll(ActionSiteGeneratorMojo.this.contributors); 311 } 312 return allContributors; 313 } 314 315 }; 316 } 317 318}