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("<"); 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(">"); 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 "<" + typeParameters +">"; 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("<"); 488 while (it.hasNext()) { 489 genericType(it.next(), contextClassifier, accumulator, monitor); 490 if (it.hasNext()) { 491 accumulator.add(","); 492 } 493 } 494 accumulator.add(">"); 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}