001 002package org.nasdanika.html.model.app.graph.emf; 003 004import java.lang.reflect.InvocationTargetException; 005import java.lang.reflect.Method; 006import java.util.ArrayList; 007import java.util.Collection; 008import java.util.Collections; 009import java.util.LinkedHashMap; 010import java.util.List; 011import java.util.Map; 012import java.util.Map.Entry; 013import java.util.Objects; 014import java.util.concurrent.atomic.AtomicInteger; 015import java.util.stream.Collectors; 016import java.util.stream.Stream; 017 018import org.apache.commons.text.StringEscapeUtils; 019import org.eclipse.emf.common.notify.Adapter; 020import org.eclipse.emf.common.util.URI; 021import org.eclipse.emf.ecore.EClass; 022import org.eclipse.emf.ecore.EObject; 023import org.eclipse.emf.ecore.EOperation; 024import org.eclipse.emf.ecore.EReference; 025import org.eclipse.emf.ecore.util.EcoreUtil; 026import org.nasdanika.common.CollectionCompoundConsumer; 027import org.nasdanika.common.Consumer; 028import org.nasdanika.common.Context; 029import org.nasdanika.common.ExecutionException; 030import org.nasdanika.common.Function; 031import org.nasdanika.common.MapCompoundSupplier; 032import org.nasdanika.common.MarkdownHelper; 033import org.nasdanika.common.ProgressMonitor; 034import org.nasdanika.common.Status; 035import org.nasdanika.common.Supplier; 036import org.nasdanika.common.Supplier.FunctionResult; 037import org.nasdanika.emf.persistence.MarkerFactory; 038import org.nasdanika.common.SupplierFactory; 039import org.nasdanika.common.Util; 040import org.nasdanika.exec.content.ContentFactory; 041import org.nasdanika.exec.content.Interpolator; 042import org.nasdanika.exec.content.Markdown; 043import org.nasdanika.exec.content.Text; 044import org.nasdanika.graph.Connection; 045import org.nasdanika.graph.emf.EClassConnection; 046import org.nasdanika.graph.emf.EObjectNode; 047import org.nasdanika.graph.emf.EOperationConnection; 048import org.nasdanika.graph.emf.EReferenceConnection; 049import org.nasdanika.graph.processor.ChildProcessors; 050import org.nasdanika.graph.processor.IncomingEndpoint; 051import org.nasdanika.graph.processor.IncomingHandler; 052import org.nasdanika.graph.processor.NodeProcessorConfig; 053import org.nasdanika.graph.processor.OutgoingEndpoint; 054import org.nasdanika.graph.processor.OutgoingHandler; 055import org.nasdanika.graph.processor.ParentProcessor; 056import org.nasdanika.graph.processor.ProcessorElement; 057import org.nasdanika.graph.processor.ProcessorInfo; 058import org.nasdanika.html.Tag; 059import org.nasdanika.html.model.app.Action; 060import org.nasdanika.html.model.app.AppFactory; 061import org.nasdanika.html.model.app.Label; 062import org.nasdanika.html.model.app.Link; 063import org.nasdanika.html.model.app.gen.AppAdapterFactory; 064import org.nasdanika.html.model.app.graph.WidgetFactory; 065import org.nasdanika.html.model.bootstrap.BootstrapElement; 066import org.nasdanika.html.model.bootstrap.BootstrapFactory; 067import org.nasdanika.html.model.bootstrap.Modal; 068import org.nasdanika.html.model.html.HtmlFactory; 069import org.nasdanika.ncore.Documented; 070import org.nasdanika.ncore.NamedElement; 071import org.nasdanika.ncore.util.SemanticInfo; 072 073/** 074 * Base class for node processors. 075 * Groups connections by reference, creates a consumer per reference (builder), chains the labels supplier with the consumers. 076 * @author Pavel 077 * 078 */ 079public class EObjectNodeProcessor<T extends EObject> implements WidgetFactory { 080 081 private static final String HELP_DECORATOR_ICON = "far fa-question-circle"; 082 083 private static final String HELP_DECORATOR_STYLE = "vertical-align:super;font-size:x-small;margin-left:0.2em"; 084 085 private static AtomicInteger counter = new AtomicInteger(); 086 087 private int id = counter.incrementAndGet(); 088 089 /** 090 * Generated unique ID for grouping and comparing/ordering. 091 * E.g. deciding which processor of two is responsible for combining opposite references (Ecore level and Nasdanika level) 092 * or grouping all cross-references into one for graph generation. 093 * @return 094 */ 095 public int getId() { 096 return id; 097 } 098 099 public static Selector<EObject> TARGET_SELECTOR = (widgetFactory, base, progressMonitor) -> { 100 return ((EObjectNodeProcessor<?>) widgetFactory).getTarget(); 101 }; 102 103 public static Selector<EObjectNodeProcessor<?>> SELF_SELECTOR = (widgetFactory, base, progressMonitor) -> { 104 return ((EObjectNodeProcessor<?>) widgetFactory); 105 }; 106 107 protected java.util.function.Function<ProgressMonitor, Action> prototypeProvider; 108 protected NodeProcessorConfig<WidgetFactory, WidgetFactory> config; 109 protected Context context; 110 protected URI uri; 111 112 /** 113 * Facets are used to provide support for multiple inheritance. For example, {@link EClass} C has EClasses A and B as supertypes. 114 * EClass C node processor would extend class A node processor and have class B node processor as a facet delegating to it. 115 */ 116 protected List<WidgetFactory> facets = new ArrayList<>(); 117 118 public EObjectNodeProcessor( 119 NodeProcessorConfig<WidgetFactory, WidgetFactory> config, 120 Context context, 121 java.util.function.Function<ProgressMonitor, Action> prototypeProvider) { 122 this.config = config; 123 this.context = context; 124 this.prototypeProvider = prototypeProvider; 125 this.uri = URI.createURI("index.html"); 126 // Create facets in sub-classes 127 } 128 129 protected Map<EObjectNode, ProcessorInfo<Object>> childProcessors; 130 131 @ChildProcessors 132 public void setChildProcessors(Map<EObjectNode, ProcessorInfo<Object>> childProcessors) { 133 this.childProcessors = childProcessors; 134 } 135 136 protected ConnectionProcessor parentProcessor; 137 138 @ParentProcessor 139 public void setParentProcessor(ConnectionProcessor parentProcessor) { 140 this.parentProcessor = parentProcessor; 141 } 142 143 protected EObjectNode node; 144 145 @ProcessorElement 146 public void setNode(EObjectNode node) { 147 this.node = node; 148 } 149 150 @SuppressWarnings("unchecked") 151 public T getTarget() { 152 return (T) node.get(); 153 } 154 155 public NodeProcessorConfig<WidgetFactory, WidgetFactory> getConfig() { 156 return config; 157 } 158 159 protected Map<EReferenceConnection, WidgetFactory> incomingReferenceEndpoints = new LinkedHashMap<>(); 160 protected Map<EReferenceConnection, WidgetFactory> outgoingReferenceEndpoints = new LinkedHashMap<>(); 161 162 @IncomingEndpoint 163 public void setIncomingRefernceEndpoint(EReferenceConnection connection, WidgetFactory endpoint) { 164 incomingReferenceEndpoints.put(connection, endpoint); 165 } 166 167 @OutgoingEndpoint 168 public void setOutgoingRefernceEndpoint(EReferenceConnection connection, WidgetFactory endpoint) { 169 outgoingReferenceEndpoints.put(connection, endpoint); 170 } 171 172 protected Map<EOperationConnection, WidgetFactory> incomingOperationEndpoints = new LinkedHashMap<>(); 173 protected Map<EOperationConnection, WidgetFactory> outgoingOperationEndpoints = new LinkedHashMap<>(); 174 175 @IncomingEndpoint 176 public void setIncominOperationgEndpoint(EOperationConnection connection, WidgetFactory endpoint) { 177 incomingOperationEndpoints.put(connection, endpoint); 178 } 179 180 @OutgoingEndpoint 181 public void setOutgoingOperationEndpoint(EOperationConnection connection, WidgetFactory endpoint) { 182 outgoingOperationEndpoints.put(connection, endpoint); 183 } 184 185 @IncomingHandler 186 public WidgetFactory getIncomingHandler(Connection connection) { 187 return this; 188 } 189 190 @OutgoingHandler 191 public WidgetFactory getOutgoingHandler(Connection connection) { 192 return this; 193 } 194 195 protected Collection<Label> createLabels(ProgressMonitor progressMonitor) { 196 return Collections.singleton(createAction(progressMonitor)); 197 } 198 199 /** 200 * Creates action for the node. 201 * @param progressMonitor 202 * @return 203 */ 204 protected Label createAction(ProgressMonitor progressMonitor) { 205 Action action = createAction(getTarget(), progressMonitor); 206 if (action.getDecorator() == null && eClassWidgetFactory != null) { 207 Label helpDecorator = eClassWidgetFactory.createHelpDecorator(progressMonitor); 208 action.setDecorator(helpDecorator); 209 } 210 if (getTarget() instanceof Documented) { 211 action.getContent().addAll(EcoreUtil.copyAll(((Documented) getTarget()).getDocumentation())); 212 } 213 214 return action; 215 } 216 217 /** 218 * @return Supplier of labels with object's own information, without references. 219 * Reference-related information is added by reference consumers/builders. 220 */ 221 protected Supplier<Collection<Label>> doCreateLabelsSupplier() { 222 return new Supplier<Collection<Label>>() { 223 224 @Override 225 public double size() { 226 return 1; 227 } 228 229 @Override 230 public String name() { 231 return "Labels supplier for " + getTarget(); 232 } 233 234 @Override 235 public Collection<Label> execute(ProgressMonitor progressMonitor) { 236 return createLabels(progressMonitor); 237 } 238 239 }; 240 } 241 242// /** 243// * Override to filter out references which should not contribute to label building. 244// * @param eReference 245// * @return 246// */ 247// protected boolean isBuilderReference(EReference eReference) { 248// return true; // TODO - from configuration and possibly annotations 249// } 250 251 /** 252 * Comparator for reference sorting 253 * @param a 254 * @param b 255 * @return 256 */ 257 protected int compareIncomingReferences(EReference a, EReference b) { 258 return a.getName().compareTo(b.getName()); 259 } 260 261 /** 262 * Comparator for reference sorting 263 * @param a 264 * @param b 265 * @return 266 */ 267 protected int compareOutgoingReferences(EReference a, EReference b) { 268 return a.getName().compareTo(b.getName()); 269 } 270 271 /** 272 * Comparator for operation binding sorting 273 * @return 274 */ 275 protected int compareIncomingOperations(EOperation aOp, List<Object> aArgs, EOperation bOp, List<Object> bArgs) { 276 return aOp.getName().compareTo(bOp.getName()); 277 } 278 279 /** 280 * Comparator for operator binding sorting 281 * @return 282 */ 283 protected int compareOutgoingOperations(EOperation aOp, List<Object> aArgs, EOperation bOp, List<Object> bArgs) { 284 return aOp.getName().compareTo(bOp.getName()); 285 } 286 287 protected List<Consumer<Collection<Label>>> getReferenceLabelBuilders() { 288 List<Consumer<Collection<Label>>> ret = new ArrayList<>(); 289 290 Map<EReference, List<Entry<EReferenceConnection, WidgetFactory>>> groupedOutgoingReferenceEndpoints = org.nasdanika.common.Util.groupBy(outgoingReferenceEndpoints.entrySet(), e -> e.getKey().getReference()); 291 groupedOutgoingReferenceEndpoints 292 .entrySet() 293 .stream() 294 .sorted((a, b) -> compareOutgoingReferences(a.getKey(), b.getKey())) 295 .map(e -> createOutgoingReferenceLabelConsumer(e.getKey(), e.getValue())) 296 .filter(Objects::nonNull) 297 .forEach(ret::add); 298 299 Map<EReference, List<Entry<EReferenceConnection, WidgetFactory>>> groupedIncomingReferenceEndpoints = org.nasdanika.common.Util.groupBy(incomingReferenceEndpoints.entrySet(), e -> e.getKey().getReference()); 300 groupedIncomingReferenceEndpoints 301 .entrySet() 302 .stream() 303 .sorted((a, b) -> compareIncomingReferences(a.getKey(), b.getKey())) 304 .map(e -> createIncomingReferenceLabelConsumer(e.getKey(), e.getValue())) 305 .filter(Objects::nonNull) 306 .forEach(ret::add); 307 308 return ret; 309 } 310 311 protected List<Consumer<Collection<Label>>> getOperationLabelBuilders() { 312 List<Consumer<Collection<Label>>> ret = new ArrayList<>(); 313 record Binding(EOperation operation, List<Object> arguments) {}; 314 315 Map<Binding, List<Entry<EOperationConnection, WidgetFactory>>> groupedOutgoingOperationEndpoints = org.nasdanika.common.Util.groupBy(outgoingOperationEndpoints.entrySet(), e -> new Binding(e.getKey().getOperation(), e.getKey().getArguments())); 316 groupedOutgoingOperationEndpoints 317 .entrySet() 318 .stream() 319 .sorted((a, b) -> compareOutgoingOperations(a.getKey().operation(), a.getKey().arguments(), b.getKey().operation(), b.getKey().arguments())) 320 .map(e -> createOutgoingOperationLabelConsumer(e.getKey().operation(), e.getKey().arguments(), e.getValue())) 321 .filter(Objects::nonNull) 322 .forEach(ret::add); 323 324 Map<Binding, List<Entry<EOperationConnection, WidgetFactory>>> groupedIncomingOperationEndpoints = org.nasdanika.common.Util.groupBy(incomingOperationEndpoints.entrySet(), e -> new Binding(e.getKey().getOperation(), e.getKey().getArguments())); 325 groupedIncomingOperationEndpoints 326 .entrySet() 327 .stream() 328 .sorted((a, b) -> compareIncomingOperations(a.getKey().operation(), a.getKey().arguments(), b.getKey().operation(), b.getKey().arguments())) 329 .map(e -> createIncomingOperationLabelConsumer(e.getKey().operation(), e.getKey().arguments(), e.getValue())) 330 .filter(Objects::nonNull) 331 .forEach(ret::add); 332 333 return ret; 334 } 335 336 // --- Label building methods --- 337 338 /** 339 * 340 * @param eReference 341 * @return true if lables suppliers shall be called to create labels/actions. 342 * This implementation returns false. 343 */ 344 protected boolean isCallIncomingReferenceLabelsSuppliers(EReference eReference) { 345 return false; 346 } 347 348 /** 349 * 350 * @param eReference 351 * @return true if lables suppliers shall be called to create labels/actions. 352 * This implementation returns true for containment references, i.e. actions for child objects shall be created. 353 */ 354 protected boolean isCallOutgoingReferenceLabelsSuppliers(EReference eReference) { 355 return eReference != null && eReference.isContainment(); 356 } 357 358 /** 359 * 360 * @param eOperation 361 * @return true if lables suppliers shall be called to create labels/actions. 362 * This implementation returns false. 363 */ 364 protected boolean isCallIncomingOperationLabelsSuppliers(EOperation eOperation, List<Object> arguments) { 365 return false; 366 } 367 368 /** 369 * 370 * @param eOperation 371 * @return true if lables suppliers shall be called to create labels/actions. 372 * This implementation returns false. 373 */ 374 protected boolean isCallOutgoingOperationLabelsSuppliers(EOperation eOperation, List<Object> arguments) { 375 return false; 376 } 377 378 /** 379 * 380 * @return 381 */ 382 protected Consumer<Collection<Label>> createIncomingReferenceLabelConsumer( 383 EReference eReference, 384 List<Entry<EReferenceConnection, WidgetFactory>> referenceIncomingEndpoints) { 385 386 @SuppressWarnings("resource") 387 MapCompoundSupplier<EReferenceConnection, Collection<Label>> endpointLabelsSupplier = new MapCompoundSupplier<>("Incoming reference endpoints supplier"); 388 if (isCallIncomingReferenceLabelsSuppliers(eReference)) { 389 for (Entry<EReferenceConnection, WidgetFactory> e: referenceIncomingEndpoints) { 390 endpointLabelsSupplier.put(e.getKey(), e.getValue().createLabelsSupplier()); 391 } 392 } 393 394 Consumer<Supplier.FunctionResult<Collection<Label>, Map<EReferenceConnection, Collection<Label>>>> referenceLabelBuilder = createIncomingReferenceLabelBuilder(eReference, referenceIncomingEndpoints); 395 Function<Collection<Label>, FunctionResult<Collection<Label>, Map<EReferenceConnection, Collection<Label>>>> endpointLabelsFunction = endpointLabelsSupplier.asFunction(); 396 return endpointLabelsFunction.then(referenceLabelBuilder); 397 } 398 399 /** 400 * Builds target labels 401 * @param eReference 402 * @return 403 */ 404 protected Consumer<Supplier.FunctionResult<Collection<Label>, Map<EReferenceConnection, Collection<Label>>>> createIncomingReferenceLabelBuilder( 405 EReference eReference, 406 List<Entry<EReferenceConnection, WidgetFactory>> referenceIncomingEndpoints) { 407 408 return new Consumer<Supplier.FunctionResult<Collection<Label>,Map<EReferenceConnection,Collection<Label>>>>() { 409 410 @Override 411 public double size() { 412 return 1; 413 } 414 415 @Override 416 public String name() { 417 return "Incoming reference label builder"; 418 } 419 420 @Override 421 public void execute(FunctionResult<Collection<Label>, Map<EReferenceConnection, Collection<Label>>> arg, ProgressMonitor progressMonitor) { 422 buildIncomingReference(eReference, referenceIncomingEndpoints, arg.argument(), arg.result(), progressMonitor); 423 } 424 }; 425 426 } 427 428 /** 429 * Called by builder/consumer's execute(); 430 * @param eReference 431 * @param referenceIncomingEndpoints 432 * @param labels 433 * @param incomingLabels 434 * @param progressMonitor 435 */ 436 protected void buildIncomingReference( 437 EReference eReference, 438 List<Entry<EReferenceConnection, WidgetFactory>> referenceIncomingEndpoints, 439 Collection<Label> labels, 440 Map<EReferenceConnection, Collection<Label>> incomingLabels, 441 ProgressMonitor progressMonitor) { 442 443 for (Method method: getClass().getMethods()) { 444 IncomingReferenceBuilder irb = method.getAnnotation(IncomingReferenceBuilder.class); 445 if (irb != null 446 && eReference.getFeatureID() == irb.referenceID() 447 && eReference.getEContainingClass().getClassifierID() == irb.classID() 448 && eReference.getEContainingClass().getEPackage().getNsURI().equals(irb.nsURI())) { 449 450 if (method.getParameterCount() != 5 || 451 !method.getParameterTypes()[0].isInstance(eReference) || 452 !method.getParameterTypes()[1].isInstance(referenceIncomingEndpoints) || 453 !method.getParameterTypes()[2].isInstance(labels) || 454 !method.getParameterTypes()[3].isInstance(incomingLabels) || 455 !method.getParameterTypes()[4].isAssignableFrom(ProgressMonitor.class)) { 456 throw new IllegalArgumentException("Incoming reference builder method shall have 5 parameters compatible with: " 457 + "EReference eReference, " 458 + "List<Entry<EReferenceConnection, WidgetFactory>> referenceIncomingEndpoints, " 459 + "Collection<Label> labels, " 460 + "Map<EReferenceConnection, Collection<Label>> incomingLabels, " 461 + "ProgressMonitor progressMonitor: " + method); 462 } 463 try { 464 method.invoke(this, eReference, referenceIncomingEndpoints, labels, incomingLabels, progressMonitor); 465 } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { 466 throw new ExecutionException(e); 467 } 468 } 469 } 470 } 471 472 /** 473 * 474 * @return 475 */ 476 protected Consumer<Collection<Label>> createIncomingOperationLabelConsumer( 477 EOperation eOperation, 478 List<Object> arguments, 479 List<Entry<EOperationConnection, WidgetFactory>> operationIncomingEndpoints) { 480 481 @SuppressWarnings("resource") 482 MapCompoundSupplier<EOperationConnection, Collection<Label>> endpointLabelsSupplier = new MapCompoundSupplier<>("Incoming operation endpoints supplier"); 483 if (isCallIncomingOperationLabelsSuppliers(eOperation, arguments)) { 484 for (Entry<EOperationConnection, WidgetFactory> e: operationIncomingEndpoints) { 485 endpointLabelsSupplier.put(e.getKey(), e.getValue().createLabelsSupplier()); 486 } 487 } 488 489 Consumer<Supplier.FunctionResult<Collection<Label>, Map<EOperationConnection, Collection<Label>>>> operationLabelBuilder = createIncomingOperationLabelBuilder(eOperation, arguments, operationIncomingEndpoints); 490 Function<Collection<Label>, FunctionResult<Collection<Label>, Map<EOperationConnection, Collection<Label>>>> endpointLabelsFunction = endpointLabelsSupplier.asFunction(); 491 return endpointLabelsFunction.then(operationLabelBuilder); 492 } 493 494 /** 495 * Builds target labels 496 * @param eReference 497 * @return 498 */ 499 protected Consumer<Supplier.FunctionResult<Collection<Label>, Map<EOperationConnection, Collection<Label>>>> createIncomingOperationLabelBuilder( 500 EOperation eOperation, 501 List<Object> arguments, 502 List<Entry<EOperationConnection, WidgetFactory>> operationIncomingEndpoints) { 503 504 return new Consumer<Supplier.FunctionResult<Collection<Label>,Map<EOperationConnection,Collection<Label>>>>() { 505 506 @Override 507 public double size() { 508 return 1; 509 } 510 511 @Override 512 public String name() { 513 return "Incoming operation label builder"; 514 } 515 516 @Override 517 public void execute(FunctionResult<Collection<Label>, Map<EOperationConnection, Collection<Label>>> arg, ProgressMonitor progressMonitor) { 518 buildIncomingOperation(eOperation, arguments, operationIncomingEndpoints, arg.argument(), arg.result(), progressMonitor); 519 } 520 }; 521 522 } 523 524 /** 525 * Called by builder/consumer's execute(); 526 */ 527 protected void buildIncomingOperation( 528 EOperation eOperation, 529 List<Object> arguments, 530 List<Entry<EOperationConnection, WidgetFactory>> operationIncomingEndpoints, 531 Collection<Label> labels, 532 Map<EOperationConnection, Collection<Label>> incomingLabels, 533 ProgressMonitor progressMonitor) { 534 535 for (Method method: getClass().getMethods()) { 536 IncomingOperationBuilder iob = method.getAnnotation(IncomingOperationBuilder.class); 537 if (iob != null 538 && eOperation.getOperationID() == iob.operationID() 539 && eOperation.getEContainingClass().getClassifierID() == iob.classID() 540 && eOperation.getEContainingClass().getEPackage().getNsURI().equals(iob.nsURI())) { 541 542 if (method.getParameterCount() != 6 || 543 !method.getParameterTypes()[0].isInstance(eOperation) || 544 !method.getParameterTypes()[1].isInstance(arguments) || 545 !method.getParameterTypes()[3].isInstance(operationIncomingEndpoints) || 546 !method.getParameterTypes()[4].isInstance(labels) || 547 !method.getParameterTypes()[5].isInstance(incomingLabels) || 548 !method.getParameterTypes()[6].isAssignableFrom(ProgressMonitor.class)) { 549 throw new IllegalArgumentException("Incoming operation builder method shall have 6 parameters compatible with: " 550 + "EOperation eOperation, " 551 + "List<Object> arguments, " 552 + "List<Entry<EOperationConnection, WidgetFactory>> operationIncomingEndpoints, " 553 + "Collection<Label> labels, " 554 + "Map<EOperationConnection, Collection<Label>> incomingLabels, " 555 + "ProgressMonitor progressMonitor: " + method); 556 } 557 558 try { 559 method.invoke(this, eOperation, arguments, operationIncomingEndpoints, labels, incomingLabels, progressMonitor); 560 } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { 561 throw new ExecutionException(e); 562 } 563 } 564 } 565 } 566 567 /** 568 * 569 * @return 570 */ 571 protected Consumer<Collection<Label>> createOutgoingOperationLabelConsumer( 572 EOperation eOperation, 573 List<Object> arguments, 574 List<Entry<EOperationConnection, WidgetFactory>> operationOutgoingEndpoints) { 575 576 @SuppressWarnings("resource") 577 MapCompoundSupplier<EOperationConnection, Collection<Label>> endpointLabelsSupplier = new MapCompoundSupplier<>("Outgoing operation endpoints supplier"); 578 if (isCallOutgoingOperationLabelsSuppliers(eOperation, arguments)) { 579 for (Entry<EOperationConnection, WidgetFactory> e: operationOutgoingEndpoints) { 580 endpointLabelsSupplier.put(e.getKey(), e.getValue().createLabelsSupplier()); 581 } 582 } 583 584 Consumer<Supplier.FunctionResult<Collection<Label>, Map<EOperationConnection, Collection<Label>>>> operationLabelBuilder = createOutgoingOperationLabelBuilder(eOperation, arguments, operationOutgoingEndpoints); 585 Function<Collection<Label>, FunctionResult<Collection<Label>, Map<EOperationConnection, Collection<Label>>>> endpointLabelsFunction = endpointLabelsSupplier.asFunction(); 586 return endpointLabelsFunction.then(operationLabelBuilder); 587 } 588 589 /** 590 * Builds target labels 591 * @param eReference 592 * @return 593 */ 594 protected Consumer<Supplier.FunctionResult<Collection<Label>, Map<EOperationConnection, Collection<Label>>>> createOutgoingOperationLabelBuilder( 595 EOperation eOperation, 596 List<Object> arguments, 597 List<Entry<EOperationConnection, WidgetFactory>> operationOutgoingEndpoints) { 598 599 return new Consumer<Supplier.FunctionResult<Collection<Label>,Map<EOperationConnection,Collection<Label>>>>() { 600 601 @Override 602 public double size() { 603 return 1; 604 } 605 606 @Override 607 public String name() { 608 return "Outgoing operation label builder"; 609 } 610 611 @Override 612 public void execute(FunctionResult<Collection<Label>, Map<EOperationConnection, Collection<Label>>> arg, ProgressMonitor progressMonitor) { 613 buildIncomingOperation(eOperation, arguments, operationOutgoingEndpoints, arg.argument(), arg.result(), progressMonitor); 614 } 615 }; 616 617 } 618 619 /** 620 * Called by builder/consumer's execute(); 621 */ 622 protected void buildOutgoingOperation( 623 EOperation eOperation, 624 List<Object> arguments, 625 List<Entry<EOperationConnection, WidgetFactory>> operationOutgoingEndpoints, 626 Collection<Label> labels, 627 Map<EOperationConnection, Collection<Label>> outgoingLabels, 628 ProgressMonitor progressMonitor) { 629 630 for (Method method: getClass().getMethods()) { 631 OutgoingOperationBuilder oob = method.getAnnotation(OutgoingOperationBuilder.class); 632 if (oob != null && eOperation.getOperationID() == oob.value()) { 633 634 if (method.getParameterCount() != 6 || 635 !method.getParameterTypes()[0].isInstance(eOperation) || 636 !method.getParameterTypes()[1].isInstance(arguments) || 637 !method.getParameterTypes()[3].isInstance(operationOutgoingEndpoints) || 638 !method.getParameterTypes()[4].isInstance(labels) || 639 !method.getParameterTypes()[5].isInstance(outgoingLabels) || 640 !method.getParameterTypes()[6].isAssignableFrom(ProgressMonitor.class)) { 641 throw new IllegalArgumentException("Outgoing operation builder method shall have 6 parameters compatible with: " 642 + "EOperation eOperation, " 643 + "List<Object> arguments, " 644 + "List<Entry<EOperationConnection, WidgetFactory>> operationOutgoingEndpoints, " 645 + "Collection<Label> labels, " 646 + "Map<EOperationConnection, Collection<Label>> outgoingLabels, " 647 + "ProgressMonitor progressMonitor: " + method); 648 } 649 try { 650 method.invoke(this, eOperation, arguments, operationOutgoingEndpoints, labels, outgoingLabels, progressMonitor); 651 } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { 652 throw new ExecutionException(e); 653 } 654 } 655 } 656 } 657 658 /** 659 * 660 * @return 661 */ 662 protected Consumer<Collection<Label>> createOutgoingReferenceLabelConsumer( 663 EReference eReference, 664 List<Entry<EReferenceConnection, WidgetFactory>> referenceOutgoingEndpoints) { 665 666 @SuppressWarnings("resource") 667 MapCompoundSupplier<EReferenceConnection, Collection<Label>> endpointLabelsSupplier = new MapCompoundSupplier<>("Outgoing endpoints supplier"); 668 if (isCallOutgoingReferenceLabelsSuppliers(eReference)) { 669 for (Entry<EReferenceConnection, WidgetFactory> e: referenceOutgoingEndpoints) { 670 endpointLabelsSupplier.put(e.getKey(), e.getValue().createLabelsSupplier()); 671 } 672 } 673 674 Consumer<Supplier.FunctionResult<Collection<Label>, Map<EReferenceConnection, Collection<Label>>>> referenceLabelBuilder = createOutgoingReferenceLabelBuilder(eReference, referenceOutgoingEndpoints); 675 Function<Collection<Label>, FunctionResult<Collection<Label>, Map<EReferenceConnection, Collection<Label>>>> endpointLabelsFunction = endpointLabelsSupplier.asFunction(); 676 return endpointLabelsFunction.then(referenceLabelBuilder); 677 } 678 679 /** 680 * Builds target labels 681 * @param eReference 682 * @return 683 */ 684 protected Consumer<Supplier.FunctionResult<Collection<Label>, Map<EReferenceConnection, Collection<Label>>>> createOutgoingReferenceLabelBuilder( 685 EReference eReference, 686 List<Entry<EReferenceConnection, WidgetFactory>> referenceOutgoingEndpoints) { 687 688 689 return new Consumer<Supplier.FunctionResult<Collection<Label>,Map<EReferenceConnection,Collection<Label>>>>() { 690 691 @Override 692 public double size() { 693 return 1; 694 } 695 696 @Override 697 public String name() { 698 return "Outgoing reference label builder"; 699 } 700 701 @Override 702 public void execute( 703 FunctionResult<Collection<Label>, Map<EReferenceConnection, Collection<Label>>> arg, 704 ProgressMonitor progressMonitor) { 705 706 buildOutgoingReference( 707 eReference, 708 referenceOutgoingEndpoints, 709 arg.argument(), 710 arg.result(), 711 progressMonitor); 712 } 713 }; 714 715 } 716 717 /** 718 * Called by builder/consumer's execute(); 719 * @param eReference 720 * @param referenceOutgoingEndpoints 721 * @param labels 722 * @param incomingLabels 723 * @param progressMonitor 724 */ 725 protected void buildOutgoingReference( 726 EReference eReference, 727 List<Entry<EReferenceConnection, WidgetFactory>> referenceOutgoingEndpoints, 728 Collection<Label> labels, 729 Map<EReferenceConnection, Collection<Label>> outgoingLabels, 730 ProgressMonitor progressMonitor) { 731 732 for (Method method: getClass().getMethods()) { 733 OutgoingReferenceBuilder orb = method.getAnnotation(OutgoingReferenceBuilder.class); 734 if (orb != null && eReference.getFeatureID() == orb.value()) { 735 if (method.getParameterCount() != 5 || 736 !method.getParameterTypes()[0].isInstance(eReference) || 737 !method.getParameterTypes()[1].isInstance(referenceOutgoingEndpoints) || 738 !method.getParameterTypes()[2].isInstance(labels) || 739 !method.getParameterTypes()[3].isInstance(outgoingLabels) || 740 !method.getParameterTypes()[4].isAssignableFrom(ProgressMonitor.class)) { 741 throw new IllegalArgumentException("Outgoing reference builder method shall have 5 parameters compatible with: " 742 + "EReference eReference, " 743 + "List<Entry<EReferenceConnection, WidgetFactory>> referenceOutgoingEndpoints, " 744 + "Collection<Label> labels, " 745 + "Map<EReferenceConnection, Collection<Label>> outgoingLabels, " 746 + "ProgressMonitor progressMonitor: " + method); 747 } 748 try { 749 method.invoke(this, eReference, referenceOutgoingEndpoints, labels, outgoingLabels, progressMonitor); 750 } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { 751 throw new ExecutionException(e); 752 } 753 } 754 } 755 756 addReferenceChildren(eReference, labels, outgoingLabels, progressMonitor); 757 } 758 759 protected void addReferenceChildren( 760 EReference eReference, 761 Collection<Label> labels, 762 Map<EReferenceConnection, Collection<Label>> outgoingLabels, 763 ProgressMonitor progressMonitor) { 764 765 for (Label tLabel: labels) { 766 Label refLabel = createLabel(eReference, progressMonitor); 767 for (Entry<EReferenceConnection, Collection<Label>> re: outgoingLabels.entrySet()) { 768 refLabel.getChildren().addAll(re.getValue()); 769 } 770 if (!refLabel.getChildren().isEmpty()) { 771 tLabel.getChildren().add(refLabel); 772 } 773 } 774 } 775 776 private WidgetFactory eClassWidgetFactory; 777 778 @OutgoingEndpoint 779 public final void setEClassEndpoint(EClassConnection connection, WidgetFactory eClassWidgetFactory) { 780 this.eClassWidgetFactory = eClassWidgetFactory; 781 } 782 783 784 /** 785 * Creates and configures an action for eObject. 786 * Override to create from prototypes. 787 * @param eObject 788 * @return 789 */ 790 protected Action createAction(EObject eObject, ProgressMonitor progressMonitor) { 791 Action action = newAction(eObject, progressMonitor); 792 configureLabel(eObject, action, progressMonitor); 793 return action; 794 } 795 796 /** 797 * Creates a new action using a factory. 798 * Override to create from prototypes. 799 * @return 800 */ 801 protected Action newAction(EObject eObject, ProgressMonitor progressMonitor) { 802 if (prototypeProvider != null) { 803 Action prototype = prototypeProvider.apply(progressMonitor); 804 if (prototype != null) { 805 return prototype; 806 } 807 } 808 return AppFactory.eINSTANCE.createAction(); 809 } 810 811 /** 812 * Creates and configures a link for eObject. 813 * Override to create from prototypes. 814 * @param eObject 815 * @return 816 */ 817 protected Link createLink(EObject eObject, String path, ProgressMonitor progressMonitor) { 818 Link link = AppFactory.eINSTANCE.createLink(); 819 configureLabel(eObject, link, progressMonitor); 820 if (Util.isBlank(path)) { 821 link.setLocation(uri.toString()); 822 } else { 823 link.setLocation(URI.createURI(path).resolve(uri).toString()); 824 } 825 return link; 826 } 827 828 /** 829 * Creates and configures a label for eObject. 830 * Override to create from prototypes. 831 * @param eObject 832 * @return 833 */ 834 protected Label createLabel(EObject eObject, ProgressMonitor progressMonitor) { 835 Label label = AppFactory.eINSTANCE.createLabel(); 836 configureLabel(eObject, label, progressMonitor); 837 return label; 838 } 839 840 /** 841 * Configures label. 842 * @param eObject 843 * @param label 844 */ 845 @Override 846 public void configureLabel(Object source, Label label, ProgressMonitor progressMonitor) { 847 if (source instanceof EObject) { 848 configureLabel((EObject) source, label, progressMonitor); 849 } 850 for (WidgetFactory facet: facets) { 851 facet.configureLabel(source, label, progressMonitor); 852 } 853 } 854 855 protected void configureLabel(EObject eObject, Label label, ProgressMonitor progressMonitor) { 856 if (eObject instanceof NamedElement && Util.isBlank(label.getText())) { 857 label.setText(StringEscapeUtils.escapeHtml4(getName((NamedElement) eObject))); 858 } 859 if (label instanceof Link && uri != null) { 860 ((Link) label).setLocation(uri.toString()); 861 } 862 863 new SemanticInfo(eObject).annotate(label); 864 } 865 866 /** 867 * Override to customize name, e.g. replace blank name with some generated name 868 * @param namedElement 869 * @return 870 */ 871 protected String getName(NamedElement namedElement) { 872 return namedElement.getName(); 873 } 874 875 protected String render(Object object, ProgressMonitor progressMonitor) { 876 if (object instanceof EObject) { 877 Adapter adapter = AppAdapterFactory.INSTANCE.adapt((EObject) object, SupplierFactory.Provider.class); 878 if (adapter instanceof SupplierFactory.Provider) { 879 SupplierFactory<Tag> supplierFactory = ((SupplierFactory.Provider) adapter).getFactory(Tag.class); 880 Supplier<Tag> supplier = supplierFactory.create(context); 881 Tag tag = supplier.call(progressMonitor, null, Status.FAIL, Status.ERROR); 882 return tag.toString(); 883 } 884 // Adapt to just supplier factory here and see what comes out - string, stream, ...? 885 } else if (object instanceof Iterable) { 886 StringBuilder ret = new StringBuilder(); 887 ((Iterable<?>) object).forEach(e -> ret.append(render(e, progressMonitor))); 888 return ret.toString(); 889 } 890 if (object instanceof Stream) { 891 return ((Stream<?>) object).map(e -> render(e, progressMonitor)).collect(Collectors.joining()); 892 } 893 894 return object == null ? "" : object.toString(); 895 } 896 897 // --- WidgetFactory methods --- 898 899 @Override 900 public void resolve(URI base, ProgressMonitor progressMonitor) { 901 uri = uri.resolve(base); 902 for (WidgetFactory oe: outgoingReferenceEndpoints.values()) { 903 oe.resolve(uri, progressMonitor); 904 } 905 for (WidgetFactory ie: incomingReferenceEndpoints.values()) { 906 ie.resolve(uri, progressMonitor); 907 } 908 } 909 910 @Override 911 public String selectString(Object selector, URI base, ProgressMonitor progressMonitor) { 912 return render(select(selector, base, progressMonitor), progressMonitor); 913 } 914 915 @Override 916 public String createLinkString(URI base, ProgressMonitor progressMonitor) { 917 return render(createLink(base, progressMonitor), progressMonitor); 918 } 919 920 @Override 921 public Object createLink(URI base, ProgressMonitor progressMonitor) { 922 Link link = createLink(getTarget(), null, progressMonitor); 923 link.rebase(null, base); 924 return link; 925 } 926 927 /** 928 * Override to configure the modal. E.g. center, change size, make scrollable, etc. 929 * @param helpModal 930 */ 931 protected void configureHelpModal(Modal helpModal) { 932// helpModal.setSize("large"); 933// helpModal.setCentered(true); 934 } 935 936 /** 937 * Convenience method for creating help decorators. 938 * Returns null if the tooltip, location and contents are blank/empty. 939 * If the contents is empty returns a label or a link with a help icon and a tooltip. 940 * Otherwise returns a link with a tooltip and a modal. The modal header links to the location if it is not empty. 941 * @param tooltip Tooltip to show 942 * @param location If not blank 943 * @param title Modal header text 944 * @param icon 945 * @param contents 946 * @param modalConfigurator 947 * @return 948 */ 949 public static Label createHelpDecorator( 950 String tooltip, 951 String location, 952 String title, 953 String icon, 954 Collection<EObject> contents, 955 java.util.function.Consumer<Modal> modalConfigurator) { 956 957 if (contents == null || contents.isEmpty()) { 958 if (Util.isBlank(location)) { 959 if (Util.isBlank(tooltip)) { 960 return null; 961 } 962 Label helpDecorator = AppFactory.eINSTANCE.createLabel(); 963 helpDecorator.setIcon(HELP_DECORATOR_ICON); 964 helpDecorator.getAttributes().put("style", createText(HELP_DECORATOR_STYLE)); 965 helpDecorator.setTooltip(tooltip); 966 return helpDecorator; 967 } 968 } 969 970 Link helpDecorator = AppFactory.eINSTANCE.createLink(); 971 helpDecorator.setIcon(HELP_DECORATOR_ICON); 972 helpDecorator.getAttributes().put("style", createText(HELP_DECORATOR_STYLE)); 973 helpDecorator.setTooltip(tooltip); 974 if (contents == null || contents.isEmpty()) { 975 helpDecorator.setLocation(location); 976 } else { 977 Modal helpModal = BootstrapFactory.eINSTANCE.createModal(); 978 979 if (!Util.isBlank(title)) { 980 BootstrapElement header = BootstrapFactory.eINSTANCE.createBootstrapElement(); 981 org.nasdanika.html.model.html.Tag h2 = HtmlFactory.eINSTANCE.createTag(); 982 h2.setName("H2"); 983 header.getContent().add(h2); 984 if (Util.isBlank(location)) { 985 // Label 986 Label tLabel = AppFactory.eINSTANCE.createLabel(); 987 tLabel.setText(title); 988 tLabel.setIcon(icon); 989 h2.getContent().add(tLabel); 990 } else { 991 // Link 992 Link typeLink = AppFactory.eINSTANCE.createLink(); 993 typeLink.setText(title); 994 typeLink.setIcon(icon); 995 typeLink.setLocation(location); 996 h2.getContent().add(typeLink); 997 } 998 helpModal.setHeader(header); 999 } 1000 1001 BootstrapElement body = BootstrapFactory.eINSTANCE.createBootstrapElement(); 1002 body.getContent().addAll(contents); 1003 helpModal.setBody(body); 1004 if (modalConfigurator != null) { 1005 modalConfigurator.accept(helpModal); 1006 } 1007 helpDecorator.setModal(helpModal); 1008 } 1009 1010 return helpDecorator; 1011 } 1012 1013 public static Label createHelpDecorator( 1014 String tooltip, 1015 String location, 1016 String title, 1017 String icon, 1018 String contents, 1019 java.util.function.Consumer<Modal> modalConfigurator) { 1020 return createHelpDecorator( 1021 tooltip, 1022 location, 1023 title, 1024 icon, 1025 Util.isBlank(contents) ? Collections.emptyList() : Collections.singleton(createText(contents)), 1026 modalConfigurator); 1027 } 1028 1029 public Label createMarkdownHelpDecorator( 1030 String tooltip, 1031 String location, 1032 String title, 1033 String icon, 1034 String markdown, 1035 java.util.function.Consumer<Modal> modalConfigurator) { 1036 return createHelpDecorator( 1037 tooltip, 1038 location, 1039 title, 1040 icon, 1041 context.get(MarkdownHelper.class, MarkdownHelper.INSTANCE).markdownToHtml(markdown), 1042 modalConfigurator); 1043 } 1044 1045 protected Collection<EObject> createHelpContents(URI base, ProgressMonitor progressMonitor) { 1046 return Collections.emptyList(); 1047 } 1048 1049 @Override 1050 public Label createHelpDecorator(URI base, ProgressMonitor progressMonitor) { 1051 Link link = createLink(getTarget(), null, progressMonitor); 1052 link.rebase(null, base); 1053 1054 Collection<EObject> helpContents = createHelpContents(base, progressMonitor); 1055 1056 return createHelpDecorator( 1057 link.getTooltip(), 1058 link.getLocation(), 1059 link.getText(), 1060 link.getIcon(), 1061 helpContents, 1062 this::configureHelpModal); 1063 } 1064 1065 @Override 1066 public Supplier<Collection<Label>> createLabelsSupplier() { 1067 List<Consumer<Collection<Label>>> referenceLabelBuilders = getReferenceLabelBuilders(); 1068 List<Consumer<Collection<Label>>> operationLabelBuilders = getOperationLabelBuilders(); 1069 if (referenceLabelBuilders == null && operationLabelBuilders == null || referenceLabelBuilders.isEmpty() && operationLabelBuilders.isEmpty()) { 1070 return doCreateLabelsSupplier(); 1071 } 1072 @SuppressWarnings("resource") 1073 CollectionCompoundConsumer<Collection<Label>> collectionCompoundConsumer = new CollectionCompoundConsumer<Collection<Label>>("Reference Label Builder"); 1074 referenceLabelBuilders.forEach(collectionCompoundConsumer::add); 1075 operationLabelBuilders.forEach(collectionCompoundConsumer::add); 1076 return doCreateLabelsSupplier().then(collectionCompoundConsumer.asFunction()); 1077 } 1078 1079 @Override 1080 public String createLabelString(ProgressMonitor progressMonitor) { 1081 return render(createLabel(progressMonitor), progressMonitor); 1082 } 1083 1084 @Override 1085 public Object createLabel(ProgressMonitor progressMonitor) { 1086 return createLabel(getTarget(), progressMonitor); 1087 } 1088 1089 // --- Convenience methods -- 1090 /** 1091 * Adds textual content. 1092 * @param content 1093 */ 1094 protected static void addContent(Action action, String content) { 1095 Text text = ContentFactory.eINSTANCE.createText(); 1096 text.setContent(content); 1097 action.getContent().add(text); 1098 } 1099 1100 /** 1101 * Convenience method to create Text and set content in one shot. 1102 * @param content 1103 * @return 1104 */ 1105 public static Text createText(String content) { 1106 Text text = ContentFactory.eINSTANCE.createText(); 1107 text.setContent(content); 1108 return text; 1109 } 1110 1111 /** 1112 * @param markdown Markdown text 1113 * @return Spec for interpolating markdown and then converting to HTML. 1114 */ 1115 protected Markdown interpolatedMarkdown(String markdown, URI location, ProgressMonitor progressMonitor) { 1116 if (Util.isBlank(markdown)) { 1117 return null; 1118 } 1119 Markdown ret = ContentFactory.eINSTANCE.createMarkdown(); 1120 Interpolator interpolator = ContentFactory.eINSTANCE.createInterpolator(); 1121 Text text = ContentFactory.eINSTANCE.createText(); 1122 text.setContent(markdown); 1123 interpolator.setSource(text); 1124 ret.setSource(interpolator); 1125 ret.setStyle(true); 1126 1127 // Creating a marker with EObject resource location for resource resolution in Markdown 1128 if (location != null) { 1129 org.nasdanika.ncore.Marker marker = context.get(MarkerFactory.class, MarkerFactory.INSTANCE).createMarker(location.toString(), progressMonitor); 1130 ret.getMarkers().add(marker); 1131 } 1132 1133 return ret; 1134 } 1135 1136}