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("<"); 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(">"); 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 "<" + typeParameters +">"; 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("<"); 548 while (it.hasNext()) { 549 genericType(it.next(), contextClassifier, accumulator, monitor); 550 if (it.hasNext()) { 551 accumulator.accept(","); 552 } 553 } 554 accumulator.accept(">"); 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}