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