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.Objects; 011import java.util.Set; 012import java.util.function.BiFunction; 013import java.util.function.Consumer; 014import java.util.function.Predicate; 015import java.util.stream.Collectors; 016 017import org.apache.commons.codec.binary.Hex; 018import org.eclipse.emf.common.notify.Notifier; 019import org.eclipse.emf.common.util.TreeIterator; 020import org.eclipse.emf.common.util.URI; 021import org.eclipse.emf.ecore.EClass; 022import org.eclipse.emf.ecore.EClassifier; 023import org.eclipse.emf.ecore.EGenericType; 024import org.eclipse.emf.ecore.EModelElement; 025import org.eclipse.emf.ecore.ENamedElement; 026import org.eclipse.emf.ecore.EObject; 027import org.eclipse.emf.ecore.EOperation; 028import org.eclipse.emf.ecore.EPackage; 029import org.eclipse.emf.ecore.EReference; 030import org.eclipse.emf.ecore.EStructuralFeature; 031import org.eclipse.emf.ecore.ETypeParameter; 032import org.eclipse.emf.ecore.ETypedElement; 033import org.eclipse.emf.ecore.resource.Resource; 034import org.eclipse.emf.ecore.resource.ResourceSet; 035import org.nasdanika.common.Context; 036import org.nasdanika.common.DiagramGenerator; 037import org.nasdanika.common.MarkdownHelper; 038import org.nasdanika.common.MutableContext; 039import org.nasdanika.common.ProgressMonitor; 040import org.nasdanika.common.PropertyComputer; 041import org.nasdanika.common.Util; 042import org.nasdanika.emf.EmfUtil; 043import org.nasdanika.emf.EmfUtil.EModelElementDocumentation; 044import org.nasdanika.emf.persistence.MarkerFactory; 045import org.nasdanika.exec.content.ContentFactory; 046import org.nasdanika.exec.content.Interpolator; 047import org.nasdanika.exec.content.Markdown; 048import org.nasdanika.exec.content.Text; 049import org.nasdanika.html.model.app.Action; 050import org.nasdanika.html.model.app.AppFactory; 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(NcoreUtil.getNasdanikaAnnotationDetail(eObject, "icon", 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 String tooltip = NcoreUtil.getNasdanikaAnnotationDetail(eObject, "description", markdownHelper.firstPlainTextSentence(documentation.getDocumentation())); 175 ret.setTooltip(tooltip); 176 } 177 178 return ret; 179 } 180 181 /** 182 * Content before documentation. 183 * @param action 184 * @param progressMonitor 185 */ 186 protected void header(Action action, ProgressMonitor progressMonitor) {} 187 188 @Override 189 public double size() { 190 return 1; 191 } 192 193 @Override 194 public String name() { 195 return eObject.eClass().getName(); 196 } 197 198 /** 199 * @param markdown Markdown text 200 * @return Spec for interpolating markdown and then converting to HTML. 201 */ 202 protected Markdown interpolatedMarkdown(String markdown, URI location, ProgressMonitor progressMonitor) { 203 if (Util.isBlank(markdown)) { 204 return null; 205 } 206 Markdown ret = ContentFactory.eINSTANCE.createMarkdown(); 207 Interpolator interpolator = ContentFactory.eINSTANCE.createInterpolator(); 208 Text text = ContentFactory.eINSTANCE.createText(); 209 text.setContent(markdown); 210 interpolator.setSource(text); 211 ret.setSource(interpolator); 212 ret.setStyle(true); 213 214 // Creating a marker with EObject resource location for resource resolution in Markdown 215 if (location != null) { 216 org.nasdanika.ncore.Marker marker = context.get(MarkerFactory.class, MarkerFactory.INSTANCE).createMarker(location.toString(), progressMonitor); 217 ret.getMarkers().add(marker); 218 } 219 220 return ret; 221 } 222 223 protected String getEModelElementFirstDocSentence(EModelElement modelElement) { 224 EModelElementDocumentation documentation = EmfUtil.getDocumentation(modelElement); 225// String markdown = EObjectAdaptable.getResourceContext(modelElement).getString("documentation", EcoreUtil.getDocumentation(modelElement)); 226// if (Util.isBlank(markdown)) { 227// markdown = EmfUtil.getDocumentation(modelElement); 228// } 229 if (documentation == null) { 230 return null; 231 } 232 233 MarkdownHelper markdownHelper = new MarkdownHelper() { 234 235 @Override 236 protected URI getResourceBase() { 237 return documentation.getLocation(); 238 } 239 240 @Override 241 protected DiagramGenerator getDiagramGenerator() { 242 return context == null ? super.getDiagramGenerator() : context.get(DiagramGenerator.class, super.getDiagramGenerator()); 243 } 244 245 }; 246 247 String ret = /* context.computingContext().get(MarkdownHelper.class, markdownHelper) */ markdownHelper.firstPlainTextSentence(documentation.getDocumentation()); 248 return String.join(" ", ret.split("\\R")); // Replacing new lines, shall they be in the first sentence, with spaces. 249 } 250 251 /** 252 * In situations where classes referencing this class are known this method can be overridden. 253 * @return 254 */ 255 protected Collection<EClass> getReferrers(EClass eClass) { 256 return getReferrers(eClass, true); 257 } 258 259 /** 260 * In situations where classes referencing this class are known this method can be overridden. 261 * @return 262 */ 263 private Collection<EClass> getReferrers(EClass eClass, boolean includeAssociations) { 264 TreeIterator<?> acit; 265 Resource eResource = eClass.eResource(); 266 if (eResource == null) { 267 EPackage ePackage = eClass.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 EReference && ((EReference) obj).getEReferenceType() == eClass) { 279 EClass referrer = ((EReference) obj).getEContainingClass(); 280 if (includeAssociations) { 281 for (EClass superReferrer: getReferrers(referrer, false)) { 282 for (EReference superReference: superReferrer.getEReferences()) { 283 if (superReference.getEReferenceType() == referrer) { 284 EClass associationTarget = NcoreUtil.getAssociationTarget(superReference); 285 if (associationTarget == eClass) { 286 ret.add(superReferrer); 287 } 288 } 289 } 290 } 291 } 292 ret.add(referrer); 293 } 294 }); 295 return ret; 296 } 297 298 /** 299 * Finds all type uses in the resourceset. 300 * @return 301 */ 302 protected Collection<EClass> getUses(EClassifier eClassifier) { 303 TreeIterator<?> acit; 304 Resource eResource = eClassifier.eResource(); 305 if (eResource == null) { 306 EPackage ePackage = eClassifier.getEPackage(); 307 if (ePackage == null) { 308 return Collections.emptySet(); 309 } 310 acit = ePackage.eAllContents(); 311 } else { 312 ResourceSet resourceSet = eResource.getResourceSet(); 313 acit = resourceSet == null ? eResource.getAllContents() : resourceSet.getAllContents(); 314 } 315 Set<EClass> ret = new HashSet<>(); 316 acit.forEachRemaining(obj -> { 317 if (obj instanceof EClass && (org.nasdanika.emf.EmfUtil.collectTypeDependencies((EClass) obj).contains(eClassifier))) { 318 ret.add((EClass) obj); 319 } 320 }); 321 return ret; 322 } 323 324 protected static String cardinality(ETypedElement typedElement) { 325 int lowerBound = typedElement.getLowerBound(); 326 int upperBound = typedElement.getUpperBound(); 327 String cardinality; 328 if (lowerBound == upperBound) { 329 cardinality = String.valueOf(lowerBound); 330 } else { 331 cardinality = lowerBound + ".." + (upperBound == -1 ? "*" : String.valueOf(upperBound)); 332 } 333 if (typedElement instanceof EReference && ((EReference) typedElement).isContainment()) { 334 cardinality = "<B>"+cardinality+"</B>"; 335 } 336 return cardinality; 337 } 338 339 // --- Handling generic types in action text --- 340 341 protected String computeLabel(EGenericType genericType, ProgressMonitor monitor) { 342 EObject container = genericType.eContainer(); 343 EClassifier rawType = genericType.getERawType(); 344 String rawTypeText = labelProvider.apply(rawType, rawType.getName()); // rawTypeViewActionSupplierFactory == null ? rawType.getName() : rawTypeViewActionSupplierFactory.create(context).execute(monitor).getText(); 345 if (container == null || !container.eIsSet(genericType.eContainingFeature())) { 346 return rawTypeText; 347 } 348 349 StringBuilder label = new StringBuilder(); 350 if (genericType.getEClassifier() != null) { 351 label.append(rawTypeText); 352 353 if (!genericType.getETypeArguments().isEmpty()) { 354 label.append("<"); 355 for (Iterator<EGenericType> i = genericType.getETypeArguments().iterator(); i.hasNext();) { 356 EGenericType typeArgument = i.next(); 357 label.append(computeLabel(typeArgument, monitor)); 358 if (i.hasNext()) { 359 label.append(", "); 360 } 361 } 362 label.append(">"); 363 } 364 } else { 365 ETypeParameter typeParameter = genericType.getETypeParameter(); 366 String name = typeParameter != null ? labelProvider.apply(typeParameter, typeParameter.getName()) : "?"; 367 label.append(name); 368 369 if (genericType.getELowerBound() != null) { 370 label.append(" super "); 371 label.append(computeLabel(genericType.getELowerBound(), monitor)); 372 } else if (genericType.getEUpperBound() != null) { 373 label.append(" extends "); 374 label.append(computeLabel(genericType.getEUpperBound(), monitor)); 375 } 376 } 377 return label.toString(); 378 } 379 380 // --- Generics --- 381 382 /** 383 * @param eClassifier 384 * @return Type parameters string. 385 */ 386 protected String typeParameters(EClassifier eClassifier) { 387 if (eClassifier.getETypeParameters().isEmpty()) { 388 return ""; 389 } 390 StringBuilder typeParameters = new StringBuilder(); 391 for (ETypeParameter typeParameter: eClassifier.getETypeParameters()) { 392 if (typeParameters.length() > 0) { 393 typeParameters.append(","); 394 } 395 typeParameters.append(genericName(typeParameter)); 396 } 397 398 return "<" + typeParameters +">"; 399 } 400 401 protected String genericName(ETypeParameter typeParameter) { 402 StringBuilder ret = new StringBuilder(labelProvider.apply(typeParameter, typeParameter.getName())); 403 for (EGenericType bound : typeParameter.getEBounds()) { 404 if (bound.getEUpperBound() != null) { 405 ret.append(" extends ").append(genericName(bound.getEUpperBound())); 406 } 407 if (bound.getELowerBound() != null) { 408 ret.append(" super ").append(genericName(bound.getELowerBound())); 409 } 410 } 411 412 return ret.toString(); 413 } 414 415 protected String genericName(EGenericType eGenericType) { 416 StringBuilder ret = new StringBuilder(); 417 ETypeParameter eTypeParameter = eGenericType.getETypeParameter(); 418 if (eTypeParameter != null) { 419 ret.append(labelProvider.apply(eTypeParameter, eTypeParameter.getName())); 420 } else { 421 EClassifier eClassifier = eGenericType.getEClassifier(); 422 if (eClassifier != null) { 423 ret.append(labelProvider.apply(eClassifier, eClassifier.getName())); 424 } 425 } 426 ret.append(genericTypeArguments(eGenericType)); 427 return ret.toString(); 428 } 429 430 protected String genericTypeArguments(EGenericType eGenericType) { 431 StringBuilder ret = new StringBuilder(); 432 Iterator<EGenericType> it = eGenericType.getETypeArguments().iterator(); 433 if (it.hasNext()) { 434 ret.append("<"); 435 while (it.hasNext()) { 436 ret.append(genericName(it.next())); 437 if (it.hasNext()) { 438 ret.append(","); 439 } 440 } 441 ret.append(">"); 442 } 443 return ret.toString(); 444 } 445 446 /** 447 * Generates generic type text with links to classifiers. 448 * @param eGenericType 449 * @param accumulator 450 */ 451 protected void genericType(EGenericType eGenericType, EClassifier contextClassifier, Consumer<String> accumulator, ProgressMonitor monitor) { 452 if (eGenericType == null) { 453 accumulator.accept("void"); 454 } else { 455 ETypeParameter eTypeParameter = eGenericType.getETypeParameter(); 456 if (eTypeParameter != null) { 457 accumulator.accept(labelProvider.apply(eTypeParameter, eTypeParameter.getName())); 458 } else if (eGenericType.getEClassifier() != null) { 459 accumulator.accept(link(eGenericType.getEClassifier(), contextClassifier)); 460 genericTypeArguments(eGenericType, contextClassifier, accumulator, monitor); 461 } else { 462 accumulator.accept("?"); 463 if (eGenericType.getELowerBound() != null) { 464 accumulator.accept(" super "); 465 genericType(eGenericType.getELowerBound(), contextClassifier, accumulator, monitor); 466 } else if (eGenericType.getEUpperBound() != null) { 467 accumulator.accept(" extends "); 468 genericType(eGenericType.getEUpperBound(), contextClassifier, accumulator, monitor); 469 } 470 } 471 } 472 } 473 474 /** 475 * @param eClassifier 476 * @return Relative path to the argument {@link EClassifier} or null if the classifier is not part of the documentation resource set. 477 */ 478 protected String path(EClassifier eClassifier, EClassifier contextClassifier) { 479 if (!elementPredicate.test(eClassifier)) { 480 return null; 481 } 482 // TODO - resolution of external eClassifiers for federated/hierarchical documentation - from the adapter factory. 483 Resource targetResource = eClassifier.eResource(); 484 if (targetResource == null) { 485 return null; 486 } 487 ResourceSet targetResourceSet = targetResource.getResourceSet(); 488 if (targetResourceSet != eObject.eResource().getResourceSet()) { 489 return null; 490 } 491 492 String targetEPackagePath = encodeEPackage(eClassifier.getEPackage()); 493 if (Util.isBlank(targetEPackagePath)) { 494 return null; 495 } 496 497 String targetPath = targetEPackagePath + "/" + eClassifier.getName() + ".html"; 498 String thisPath = null; 499 if (contextClassifier == null) { 500 if (eObject instanceof EClassifier) { 501 contextClassifier = (EClassifier) eObject; 502 } else if (eObject.eContainer() instanceof EClassifier) { 503 contextClassifier = (EClassifier) eObject.eContainer(); 504 } else if (eObject.eContainer() instanceof EOperation) { 505 contextClassifier = (EClassifier) eObject.eContainer().eContainer(); 506 } 507 } 508 509 if (contextClassifier != null) { 510 thisPath = encodeEPackage(contextClassifier.getEPackage()) + "/" + contextClassifier.getName() + ".html"; 511 } else if (eObject instanceof EPackage) { 512 thisPath = encodeEPackage(((EPackage) eObject)) + "/package-summary.html"; 513 } 514 515 if (thisPath == null) { 516 return null; 517 } 518 519 URI base = URI.createURI(context.getString(Context.BASE_URI_PROPERTY, "tmp://base/doc/")); 520 URI target = URI.createURI(targetPath).resolve(base); 521 URI source = URI.createURI(thisPath).resolve(base); 522 URI relativeTarget = target.deresolve(source, true, true, true); 523 return relativeTarget.toString(); 524 } 525 526 /** 527 * @return Link to {@link EClassifier} if it is part of the doc or plain text if it is not. 528 */ 529 protected String link(EClassifier eClassifier, EClassifier contextClassifier) { 530 String path = path(eClassifier, contextClassifier); 531 String label = labelProvider.apply(eClassifier, eClassifier.getName()); 532 return Util.isBlank(path) ? label : "<a href=\"" + path + "\">" + label + "</a>"; 533 } 534 535 /** 536 * @return Link to {@link EClassifier} if it is part of the doc or plain text if it is not. 537 */ 538 protected String link(EStructuralFeature feature, EClassifier contextClassifier) { 539 String path = path(feature.getEContainingClass(), contextClassifier); 540 String fragment = "#" + feature.eClass().getName() + "-" + feature.getName(); 541 path = Util.isBlank(path) ? fragment : path + fragment; 542 return "<a href=\"" + path + "\">" + labelProvider.apply(feature, feature.getName()) + "</a>"; 543 } 544 545 protected void genericTypeArguments(EGenericType eGenericType, EClassifier contextClassifier, Consumer<String> accumulator, ProgressMonitor monitor) { 546 Iterator<EGenericType> it = eGenericType.getETypeArguments().iterator(); 547 if (it.hasNext()) { 548 accumulator.accept("<"); 549 while (it.hasNext()) { 550 genericType(it.next(), contextClassifier, accumulator, monitor); 551 if (it.hasNext()) { 552 accumulator.accept(","); 553 } 554 } 555 accumulator.accept(">"); 556 } 557 } 558 559 /** 560 * Encodes ePackage path. 561 * @param ePackage 562 * @return 563 */ 564 public String encodeEPackage(EPackage ePackage) { 565 return ePackagePathComputer == null ? Hex.encodeHexString(ePackage.getNsURI().getBytes(StandardCharsets.UTF_8)) : ePackagePathComputer.apply(ePackage); 566// 567// 568// String ret = null; 569// 570// for (EPackage p = ePackage; p != null; p = p.getESuperPackage()) { 571// String segment = ePackagePathComputer == null ? Hex.encodeHexString(p.getNsURI().getBytes(StandardCharsets.UTF_8)) : ePackagePathComputer.apply(p); 572// if (ret == null) { 573// ret = segment; 574// } else { 575// ret = segment + "/" + ret; 576// } 577// } 578// 579// return ret; 580 } 581 582 /** 583 * Adds textual content. 584 * @param content 585 */ 586 protected static void addContent(Action action, String content) { 587 Text text = ContentFactory.eINSTANCE.createText(); 588 text.setContent(content); 589 action.getContent().add(text); 590 } 591 592 /** 593 * Filters the collection retaining only model elements which shall be documented. 594 * @param <M> 595 * @param elements 596 * @return 597 */ 598 protected <M extends EModelElement> List<M> retainDocumentable(Collection<M> elements) { 599 return elements.stream().filter(elementPredicate).collect(Collectors.toList()); 600 } 601 602 protected static Class<?> getInstanceClass(EClassifier eClassifier, java.util.function.Function<String, Object> ePackageResolver) { 603 Class<?> instanceClass = eClassifier.getInstanceClass(); 604 if (instanceClass == null) { 605 EPackage registeredPackage = getRegisteredPackage(eClassifier, ePackageResolver); 606 if (registeredPackage != null) { 607 EClassifier registeredClassifier = registeredPackage.getEClassifier(eClassifier.getName()); 608 if (registeredClassifier != null) { 609 instanceClass = registeredClassifier.getInstanceClass(); 610 } 611 } 612 } 613 return instanceClass; 614 } 615 616 private static EPackage getRegisteredPackage(EClassifier eObject, java.util.function.Function<String, Object> ePackageResolver) { 617 String nsURI = eObject.getEPackage().getNsURI(); 618 Object value = ePackageResolver.apply(nsURI); 619 if (value instanceof EPackage) { 620 return (EPackage) value; 621 } 622 if (value instanceof EPackage.Descriptor) { 623 return Objects.requireNonNull(((EPackage.Descriptor) value).getEPackage(), "EPackage is null for " + nsURI); 624 } 625 626 if (value instanceof EPackage) { 627 return (EPackage) value; 628 } 629 return null; 630 } 631 632 633}