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