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