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}