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