001package org.nasdanika.html.ecore;
002
003import java.nio.charset.StandardCharsets;
004import java.util.Collection;
005import java.util.Collections;
006import java.util.HashSet;
007import java.util.Iterator;
008import java.util.List;
009import java.util.Set;
010
011import org.apache.commons.codec.binary.Hex;
012import org.eclipse.emf.common.notify.Notifier;
013import org.eclipse.emf.common.util.TreeIterator;
014import org.eclipse.emf.common.util.URI;
015import org.eclipse.emf.ecore.EClass;
016import org.eclipse.emf.ecore.EClassifier;
017import org.eclipse.emf.ecore.EGenericType;
018import org.eclipse.emf.ecore.EModelElement;
019import org.eclipse.emf.ecore.EObject;
020import org.eclipse.emf.ecore.EOperation;
021import org.eclipse.emf.ecore.EPackage;
022import org.eclipse.emf.ecore.EReference;
023import org.eclipse.emf.ecore.ETypeParameter;
024import org.eclipse.emf.ecore.ETypedElement;
025import org.eclipse.emf.ecore.resource.Resource;
026import org.eclipse.emf.ecore.resource.ResourceSet;
027import org.nasdanika.common.Context;
028import org.nasdanika.common.DiagramGenerator;
029import org.nasdanika.common.MarkdownHelper;
030import org.nasdanika.common.MutableContext;
031import org.nasdanika.common.ProgressMonitor;
032import org.nasdanika.common.PropertyComputer;
033import org.nasdanika.common.Util;
034import org.nasdanika.emf.EmfUtil;
035import org.nasdanika.emf.EmfUtil.EModelElementDocumentation;
036import org.nasdanika.emf.persistence.MarkerFactory;
037import org.nasdanika.exec.content.ContentFactory;
038import org.nasdanika.exec.content.Interpolator;
039import org.nasdanika.exec.content.Markdown;
040import org.nasdanika.exec.content.Text;
041import org.nasdanika.html.model.app.Action;
042import org.nasdanika.html.model.app.AppFactory;
043import org.nasdanika.ncore.Marker;
044
045public class EModelElementActionSupplier<T extends EModelElement> extends EObjectActionSupplier<T> {
046        
047        public static final String ICONS_BASE = "https://www.nasdanika.org/resources/images/ecore/";
048                
049        /**
050         * Descriptions shorter than this value are put on the top of the tabs, longer
051         * ones end up in their own tab. 
052         */
053        protected int descriptionTabLengthThreshold = 2500;
054
055        protected Context context;
056
057        protected java.util.function.Function<EPackage,String> ePackagePathComputer; 
058                
059        public EModelElementActionSupplier(T value, Context context, java.util.function.Function<EPackage,String> ePackagePathComputer) {
060                super(value);
061                this.context = context.fork();
062                PropertyComputer eClassifierPropertyComputer = new PropertyComputer() {
063                        
064                        @SuppressWarnings("unchecked")
065                        @Override
066                        public <P> P compute(Context context, String key, String path, Class<P> type) {
067                                EObject contextClassifier = value;
068                                while (contextClassifier != null && !(contextClassifier instanceof EClassifier)) {
069                                        contextClassifier = contextClassifier.eContainer();
070                                }
071                                if (contextClassifier == null) {
072                                        return null;
073                                }
074                                
075                                EClassifier targetClassifier = null;
076                                int atIdx = path.indexOf('@');
077                                if (atIdx == -1) {
078                                        targetClassifier = ((EClassifier) contextClassifier).getEPackage().getEClassifier(path);
079                                } else {
080                                        Resource contextResource = contextClassifier.eResource();
081                                        if (contextResource == null) {
082                                                return null;
083                                        }
084                                        ResourceSet resourceSet = contextResource.getResourceSet();
085                                        if (resourceSet == null) {
086                                                return null;
087                                        }
088                                        TreeIterator<Notifier> cit = resourceSet.getAllContents();
089                                        String targetNsUri = path.substring(atIdx + 1);
090                                        while (cit.hasNext()) {
091                                                Notifier next = cit.next();
092                                                if (next instanceof EPackage) {
093                                                        if (((EPackage) next).getNsURI().equals(targetNsUri)) {
094                                                                targetClassifier = ((EPackage) next).getEClassifier(path.substring(0, atIdx));
095                                                                break;
096                                                        }
097                                                }
098                                        }
099                                }
100                                
101                                if (targetClassifier == null) {
102                                        return null;
103                                }
104
105                                return (P) path(targetClassifier, (EClassifier) contextClassifier);
106                        }
107                        
108                };
109                ((MutableContext) this.context).put("classifier", eClassifierPropertyComputer);
110                this.ePackagePathComputer = ePackagePathComputer;
111        }
112
113        @Override
114        public Action execute(EClass contextClass, ProgressMonitor progressMonitor) throws Exception {          
115                // TODO - refactor to 
116//              EObject actionPrototype = NcoreUtil.getNasdanikaAnnotationDetail(eObject, "action-prototype");
117//              if (actionPrototype instanceof Action) {
118//                      return EcoreUtil.copy((Action) actionPrototype);
119//              }
120//              if (actionPrototype != null) {
121//                      ActionProvider actionProvider = Objects.requireNonNull((ActionProvider) EcoreUtil.getRegisteredAdapter(actionPrototype, ActionProvider.class), "Cannot adapt " + actionPrototype + " to " + ActionProvider.class);
122//                      return actionProvider.execute(registry, progressMonitor);
123//              }
124//              return AppFactory.eINSTANCE.createAction();
125                
126                
127                Action ret = AppFactory.eINSTANCE.createAction();
128                ret.setIcon(ICONS_BASE+eObject.eClass().getName()+".gif");
129                
130                header(ret, progressMonitor);
131                
132                EModelElementDocumentation documentation = EmfUtil.getDocumentation(eObject); //EObjectAdaptable.getResourceContext(eObject).getString("documentation", EcoreUtil.getDocumentation(eObject));
133//              if (Util.isBlank(markdown)) {
134//                      markdown = EmfUtil.getDocumentation(eObject);
135//              }
136                
137                MarkdownHelper markdownHelper = new MarkdownHelper() {
138                        
139                        @Override
140                        protected URI getResourceBase() {
141                                return documentation.getLocation();
142                        }
143                        
144                        @Override
145                        protected DiagramGenerator getDiagramGenerator() {
146                                return context == null ? super.getDiagramGenerator() : context.get(DiagramGenerator.class, super.getDiagramGenerator()); 
147                        }
148                        
149                };
150                
151                if (documentation != null) {
152                        ret.getContent().add(interpolatedMarkdown(context.interpolateToString(documentation.getDocumentation()), documentation.getLocation(), progressMonitor));
153                        ret.setTooltip(markdownHelper.firstPlainTextSentence(documentation.getDocumentation()));
154                }
155                
156                return ret;
157        }
158        
159        /**
160         * Content before documentation.
161         * @param action
162         * @param progressMonitor
163         * @throws Exception
164         */
165        protected void header(Action action, ProgressMonitor progressMonitor) throws Exception {}
166
167        @Override
168        public double size() {
169                return 1;
170        }
171
172        @Override
173        public String name() {
174                return eObject.eClass().getName();
175        }
176        
177        /**
178         * @param markdown Markdown text
179         * @return Spec for interpolating markdown and then converting to HTML. 
180         */
181        protected Markdown interpolatedMarkdown(String markdown, URI location, ProgressMonitor progressMonitor) {
182                if (Util.isBlank(markdown)) {
183                        return null;
184                }
185                Markdown ret = ContentFactory.eINSTANCE.createMarkdown();
186                Interpolator interpolator = ContentFactory.eINSTANCE.createInterpolator();
187                Text text = ContentFactory.eINSTANCE.createText();
188                text.setContent(markdown);
189                interpolator.setSource(text);
190                ret.setSource(interpolator);
191                ret.setStyle(true);
192                
193                // Creating a marker with EObject resource location for resource resolution in Markdown
194                if (location != null) {
195                        Marker marker = ret.getMarker();
196                        if (marker == null) {
197                                marker = context.get(MarkerFactory.class, MarkerFactory.INSTANCE).createMarker(location.toString(), progressMonitor);
198                                ret.setMarker(marker);
199                        }
200                }
201                
202                return ret;
203        }
204        
205        protected String getEModelElementFirstDocSentence(EModelElement modelElement) {
206                EModelElementDocumentation documentation = EmfUtil.getDocumentation(modelElement);
207//              String markdown = EObjectAdaptable.getResourceContext(modelElement).getString("documentation", EcoreUtil.getDocumentation(modelElement));
208//              if (Util.isBlank(markdown)) {
209//                      markdown = EmfUtil.getDocumentation(modelElement);
210//              }
211                if (documentation == null) {
212                        return null;
213                }
214                
215                MarkdownHelper markdownHelper = new MarkdownHelper() {
216                        
217                        @Override
218                        protected URI getResourceBase() {
219                                return documentation.getLocation();
220                        }
221                        
222                        @Override
223                        protected DiagramGenerator getDiagramGenerator() {
224                                return context == null ? super.getDiagramGenerator() : context.get(DiagramGenerator.class, super.getDiagramGenerator()); 
225                        }
226                        
227                };
228                
229                String ret = /* context.computingContext().get(MarkdownHelper.class, markdownHelper) */ markdownHelper.firstPlainTextSentence(documentation.getDocumentation());
230                return String.join(" ", ret.split("\\R")); // Replacing new lines, shall they be in the first sentence, with spaces.            
231        }
232                
233        /**
234         * In situations where classes referencing this class are known this method can be overridden. 
235         * @return
236         */     
237        protected Collection<EClass> getReferrers(EClass eClass) {
238                TreeIterator<?> acit;
239                Resource eResource = eClass.eResource();
240                if (eResource == null) {
241                        EPackage ePackage = eClass.getEPackage();
242                        if (ePackage == null) {
243                                return Collections.emptySet();
244                        }
245                        acit = ePackage.eAllContents();
246                } else {
247                        ResourceSet resourceSet = eResource.getResourceSet();
248                        acit = resourceSet == null ? eResource.getAllContents() : resourceSet.getAllContents();
249                }
250                Set<EClass> ret = new HashSet<>();
251                acit.forEachRemaining(obj -> {
252                        if (obj instanceof EReference && ((EReference) obj).getEReferenceType() == eClass) {
253                                ret.add(((EReference) obj).getEContainingClass());
254                        }
255                });
256                return ret;
257        }
258        
259        /**
260         * Finds all type uses in the resourceset. 
261         * @return
262         */
263        protected Collection<EClass> getUses(EClassifier eClassifier) {
264                TreeIterator<?> acit;
265                Resource eResource = eClassifier.eResource();
266                if (eResource == null) {
267                        EPackage ePackage = eClassifier.getEPackage();
268                        if (ePackage == null) {
269                                return Collections.emptySet();
270                        }
271                        acit = ePackage.eAllContents();
272                } else {
273                        ResourceSet resourceSet = eResource.getResourceSet();
274                        acit = resourceSet == null ? eResource.getAllContents() : resourceSet.getAllContents();
275                }
276                Set<EClass> ret = new HashSet<>();
277                acit.forEachRemaining(obj -> {
278                        if (obj instanceof EClass && (org.nasdanika.emf.EmfUtil.collectTypeDependencies((EClass) obj).contains(eClassifier))) {
279                                ret.add((EClass) obj);
280                        }
281                });
282                return ret;
283        }
284                
285        protected static String cardinality(ETypedElement typedElement) {
286                int lowerBound = typedElement.getLowerBound();
287                int upperBound = typedElement.getUpperBound();
288                String cardinality;
289                if (lowerBound == upperBound) {
290                        cardinality = String.valueOf(lowerBound);
291                } else {
292                        cardinality = lowerBound + ".." + (upperBound == -1 ? "*" : String.valueOf(upperBound));
293                }
294                if (typedElement instanceof EReference && ((EReference) typedElement).isContainment()) {
295                        cardinality = "<B>"+cardinality+"</B>";
296                }
297                return cardinality;
298        }
299        
300        // --- Handling generic types in action text --- 
301
302        protected String computeLabel(EGenericType genericType, ProgressMonitor monitor) throws Exception {
303                EObject container = genericType.eContainer();
304                EClassifier rawType = genericType.getERawType();
305                String rawTypeText = rawType.getName(); // rawTypeViewActionSupplierFactory == null ? rawType.getName() : rawTypeViewActionSupplierFactory.create(context).execute(monitor).getText();
306                if (container == null || !container.eIsSet(genericType.eContainingFeature())) {
307                        return rawTypeText;
308                }
309                
310                StringBuilder label = new StringBuilder();
311                if (genericType.getEClassifier() != null) {
312                        label.append(rawTypeText);
313
314                        if (!genericType.getETypeArguments().isEmpty()) {
315                                label.append("&lt;");
316                                for (Iterator<EGenericType> i = genericType.getETypeArguments().iterator(); i.hasNext();) {
317                                        EGenericType typeArgument = i.next();
318                                        label.append(computeLabel(typeArgument, monitor));
319                                        if (i.hasNext()) {
320                                                label.append(", ");
321                                        }
322                                }
323                                label.append("&gt;");
324                        }
325                } else {
326                        ETypeParameter typeParameter = genericType.getETypeParameter();
327                        String name = typeParameter != null ? typeParameter.getName() : "?";
328                        label.append(name);
329
330                        if (genericType.getELowerBound() != null) {
331                                label.append(" super ");
332                                label.append(computeLabel(genericType.getELowerBound(),  monitor));
333                        } else if (genericType.getEUpperBound() != null) {
334                                label.append(" extends ");
335                                label.append(computeLabel(genericType.getEUpperBound(), monitor));
336                        }
337                }
338                return label.toString();
339        }
340        
341        // --- Generics ---
342        
343        /**
344         * @param eClassifier
345         * @return Type parameters string.
346         */
347        protected String typeParameters(EClassifier eClassifier) {
348                if (eClassifier.getETypeParameters().isEmpty()) {
349                        return "";
350                }
351                StringBuilder typeParameters = new StringBuilder();
352                for (ETypeParameter typeParameter: eClassifier.getETypeParameters()) {
353                        if (typeParameters.length() > 0) {
354                                typeParameters.append(",");
355                        }
356                        typeParameters.append(genericName(typeParameter));
357                }               
358                
359                return "&lt;" + typeParameters +"&gt;";
360        }       
361        
362        protected String genericName(ETypeParameter typeParameter) {
363                StringBuilder ret = new StringBuilder(typeParameter.getName());
364                for (EGenericType bound : typeParameter.getEBounds()) {
365                        if (bound.getEUpperBound() != null) {
366                                ret.append(" extends ").append(genericName(bound.getEUpperBound()));
367                        }
368                        if (bound.getELowerBound() != null) {
369                                ret.append(" super ").append(genericName(bound.getELowerBound()));
370                        }
371                }
372                
373                return ret.toString();
374        }
375        
376        protected String genericName(EGenericType eGenericType) {
377                StringBuilder ret = new StringBuilder();
378                if (eGenericType.getETypeParameter() != null) {
379                        ret.append(eGenericType.getETypeParameter().getName());
380                } else if (eGenericType.getEClassifier() != null) {
381                        ret.append(eGenericType.getEClassifier().getName());                    
382                }
383                ret.append(genericTypeArguments(eGenericType));
384                return ret.toString();
385        }
386
387        protected String genericTypeArguments(EGenericType eGenericType) {
388                StringBuilder ret = new StringBuilder();
389                Iterator<EGenericType> it = eGenericType.getETypeArguments().iterator();
390                if (it.hasNext()) {
391                        ret.append("<");
392                        while (it.hasNext()) {
393                                ret.append(genericName(it.next()));
394                                if (it.hasNext()) {
395                                        ret.append(",");
396                                }
397                        }
398                        ret.append(">");
399                }
400                return ret.toString();
401        }
402
403        /**
404         * Generates generic type text with links to classifiers.
405         * @param eGenericType
406         * @param accumulator
407         * @throws Exception 
408         */
409        protected void genericType(EGenericType eGenericType, EClassifier contextClassifier, List<Object> accumulator, ProgressMonitor monitor) throws Exception {
410                if (eGenericType == null) {
411                        accumulator.add("void");
412                } else if (eGenericType.getETypeParameter() != null) {
413                        accumulator.add(eGenericType.getETypeParameter().getName());
414                } else if (eGenericType.getEClassifier() != null) {
415                        accumulator.add(link(eGenericType.getEClassifier(), contextClassifier));
416                        genericTypeArguments(eGenericType, contextClassifier, accumulator, monitor);
417                } else {
418                        accumulator.add('?');
419                        if (eGenericType.getELowerBound() != null) {
420                                accumulator.add(" super ");
421                                genericType(eGenericType.getELowerBound(), contextClassifier, accumulator, monitor);
422                        } else if (eGenericType.getEUpperBound() != null) {
423                                accumulator.add(" extends ");
424                                genericType(eGenericType.getEUpperBound(), contextClassifier, accumulator, monitor);
425                        }
426                }
427        }
428        
429        /**
430         * @param eClassifier
431         * @return Relatieve path to the argument {@link EClassifier} or null if the classifier is not part of the documentation resource set.
432         */
433        protected String path(EClassifier eClassifier, EClassifier contextClassifier) {
434                // TODO - resolution of external eClassifiers for federated/hierarchical documentation - from the adapter factory.
435                Resource targetResource = eClassifier.eResource();
436                if (targetResource == null) {
437                        return null;
438                }
439                ResourceSet targetResourceSet = targetResource.getResourceSet();
440                if (targetResourceSet != eObject.eResource().getResourceSet()) {
441                        return null;
442                }               
443
444                String targetEPackagePath = encodeEPackage(eClassifier.getEPackage());
445                if (Util.isBlank(targetEPackagePath)) {
446                        return null;
447                }
448                
449                String targetPath = targetEPackagePath + "/" + eClassifier.getName() + ".html";
450                String thisPath = null;
451                if (contextClassifier == null) {
452                        if (eObject instanceof EClassifier) {
453                                contextClassifier = (EClassifier) eObject;
454                        } else if (eObject.eContainer() instanceof EClassifier) {
455                                contextClassifier = (EClassifier) eObject.eContainer();
456                        } else if (eObject.eContainer() instanceof EOperation) {
457                                contextClassifier = (EClassifier) eObject.eContainer().eContainer();
458                        }
459                }
460                        
461                if (contextClassifier != null) {        
462                        thisPath = encodeEPackage(contextClassifier.getEPackage()) + "/" + contextClassifier.getName() + ".html";
463                } else if (eObject instanceof EPackage) {
464                        thisPath = encodeEPackage(((EPackage) eObject)) + "/package-summary.html";                                      
465                }
466                
467                if (thisPath == null) {
468                        return null;
469                }
470                
471                URI base = URI.createURI(context.getString(Context.BASE_URI_PROPERTY, "tmp://base/doc/"));
472                URI target = URI.createURI(targetPath).resolve(base);
473                URI source = URI.createURI(thisPath).resolve(base);
474                URI relativeTarget = target.deresolve(source, true, true, true);
475                return relativeTarget.toString();               
476        }
477        
478        /**
479         * @return Link to {@link EClassifier} if it is part of the doc or plain text if it is not.
480         */
481        protected String link(EClassifier eClassifier, EClassifier contextClassifier) {
482                String path = path(eClassifier, contextClassifier);
483                return Util.isBlank(path) ? eClassifier.getName() : "<a href=\"" + path + "\">" + eClassifier.getName() + "</a>";
484        }
485
486        protected void genericTypeArguments(EGenericType eGenericType, EClassifier contextClassifier, List<Object> accumulator, ProgressMonitor monitor) throws Exception {
487                Iterator<EGenericType> it = eGenericType.getETypeArguments().iterator();
488                if (it.hasNext()) {
489                        accumulator.add("&lt;");
490                        while (it.hasNext()) {
491                                genericType(it.next(), contextClassifier, accumulator, monitor);
492                                if (it.hasNext()) {
493                                        accumulator.add(",");
494                                }
495                        }
496                        accumulator.add("&gt;");
497                }
498        }
499        
500        /**
501         * Encodes ePackage path.
502         * @param ePackage
503         * @return
504         */
505        public String encodeEPackage(EPackage ePackage) {
506                String ret = null;
507                                
508                for (EPackage p = ePackage; p != null; p = p.getESuperPackage()) {
509                        String segment = ePackagePathComputer == null ? Hex.encodeHexString(p.getNsURI().getBytes(StandardCharsets.UTF_8)) : ePackagePathComputer.apply(p);
510                        if (ret == null) {
511                                ret = segment;
512                        } else {
513                                ret = segment + "/" + ret;
514                        }
515                }
516                
517                return ret;
518        }
519        
520        /**
521         * Adds textual content.
522         * @param content
523         */
524        protected static void addContent(Action action, String content) {
525                Text text = ContentFactory.eINSTANCE.createText();
526                text.setContent(content);
527                action.getContent().add(text);
528        }
529        
530}