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