1 /* 2 * Copyright (C) 2007-2010 Júlio Vilmar Gesser. 3 * Copyright (C) 2011, 2013-2016 The JavaParser Team. 4 * 5 * This file is part of JavaParser. 6 * 7 * JavaParser can be used either under the terms of 8 * a) the GNU Lesser General Public License as published by 9 * the Free Software Foundation, either version 3 of the License, or 10 * (at your option) any later version. 11 * b) the terms of the Apache License 12 * 13 * You should have received a copy of both licenses in LICENCE.LGPL and 14 * LICENCE.APACHE. Please refer to those files for details. 15 * 16 * JavaParser is distributed in the hope that it will be useful, 17 * but WITHOUT ANY WARRANTY; without even the implied warranty of 18 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 19 * GNU Lesser General Public License for more details. 20 */ 21 22 package com.github.javaparser.printer.lexicalpreservation; 23 24 import com.github.javaparser.*; 25 import com.github.javaparser.ast.DataKey; 26 import com.github.javaparser.ast.Modifier; 27 import com.github.javaparser.ast.Node; 28 import com.github.javaparser.ast.NodeList; 29 import com.github.javaparser.ast.body.VariableDeclarator; 30 import com.github.javaparser.ast.comments.BlockComment; 31 import com.github.javaparser.ast.comments.Comment; 32 import com.github.javaparser.ast.comments.JavadocComment; 33 import com.github.javaparser.ast.comments.LineComment; 34 import com.github.javaparser.ast.nodeTypes.NodeWithVariables; 35 import com.github.javaparser.ast.observer.AstObserver; 36 import com.github.javaparser.ast.observer.ObservableProperty; 37 import com.github.javaparser.ast.observer.PropagatingAstObserver; 38 import com.github.javaparser.ast.type.PrimitiveType; 39 import com.github.javaparser.ast.visitor.TreeVisitor; 40 import com.github.javaparser.printer.ConcreteSyntaxModel; 41 import com.github.javaparser.printer.concretesyntaxmodel.*; 42 import com.github.javaparser.utils.Pair; 43 import com.github.javaparser.utils.Utils; 44 45 import java.io.IOException; 46 import java.io.StringWriter; 47 import java.io.Writer; 48 import java.lang.reflect.InvocationTargetException; 49 import java.lang.reflect.Method; 50 import java.lang.reflect.ParameterizedType; 51 import java.util.*; 52 import java.util.stream.Collectors; 53 54 import static com.github.javaparser.GeneratedJavaParserConstants.*; 55 import static com.github.javaparser.TokenTypes.eolTokenKind; 56 import static com.github.javaparser.utils.Utils.assertNotNull; 57 import static com.github.javaparser.utils.Utils.decapitalize; 58 import static java.util.Comparator.*; 59 60 /** 61 * A Lexical Preserving Printer is used to capture all the lexical information while parsing, update them when 62 * operating on the AST and then used them to reproduce the source code 63 * in its original formatting including the AST changes. 64 */ 65 public class LexicalPreservingPrinter { 66 67 private static AstObserver observer; 68 69 /** 70 * The nodetext for a node is stored in the node's data field. This is the key to set and retrieve it. 71 */ 72 public static final DataKey<NodeText> NODE_TEXT_DATA = new DataKey<NodeText>() { 73 }; 74 75 private static final LexicalDifferenceCalculator LEXICAL_DIFFERENCE_CALCULATOR = new LexicalDifferenceCalculator(); 76 77 // 78 // Factory methods 79 // 80 81 /** 82 * Prepares the node so it can be used in the print methods. 83 * The correct order is: 84 * <ol> 85 * <li>Parse some code</li> 86 * <li>Call this setup method on the result</li> 87 * <li>Make changes to the AST as desired</li> 88 * <li>Use one of the print methods on this class to print out the original source code with your changes added</li> 89 * </ol> 90 * 91 * @return the node passed as a parameter for your convenience. 92 */ setup(N node)93 public static <N extends Node> N setup(N node) { 94 assertNotNull(node); 95 96 if(observer == null) { 97 observer = createObserver(); 98 } 99 100 node.getTokenRange().ifPresent(r -> { 101 storeInitialText(node); 102 // Setup observer 103 if(!node.isRegistered(observer)) { 104 node.registerForSubtree(observer); 105 } 106 }); 107 return node; 108 } 109 110 // 111 // Constructor and setup 112 // 113 createObserver()114 private static AstObserver createObserver() { 115 return new LexicalPreservingPrinter.Observer(); 116 } 117 118 private static class Observer extends PropagatingAstObserver { 119 @Override concretePropertyChange(Node observedNode, ObservableProperty property, Object oldValue, Object newValue)120 public void concretePropertyChange(Node observedNode, ObservableProperty property, Object oldValue, Object newValue) { 121 // Not really a change, ignoring 122 if ((oldValue != null && oldValue.equals(newValue)) || (oldValue == null && newValue == null)) { 123 return; 124 } 125 if (property == ObservableProperty.RANGE || property == ObservableProperty.COMMENTED_NODE) { 126 return; 127 } 128 if (property == ObservableProperty.COMMENT) { 129 if (!observedNode.getParentNode().isPresent()) { 130 throw new IllegalStateException(); 131 } 132 133 NodeText nodeText = getOrCreateNodeText(observedNode.getParentNode().get()); 134 135 if (oldValue == null) { 136 // Find the position of the comment node and put in front of it the comment and a newline 137 int index = nodeText.findChild(observedNode); 138 nodeText.addChild(index, (Comment) newValue); 139 nodeText.addToken(index + 1, eolTokenKind(), Utils.EOL); 140 } else if (newValue == null) { 141 if (oldValue instanceof Comment) { 142 if (((Comment) oldValue).isOrphan()){ 143 nodeText = getOrCreateNodeText(observedNode); 144 } 145 int index = getIndexOfComment ((Comment) oldValue, nodeText); 146 nodeText.removeElement(index); 147 if (nodeText.getElements().get(index).isNewline()) { 148 nodeText.removeElement(index); 149 } 150 } else { 151 throw new UnsupportedOperationException(); 152 } 153 } else { 154 if (oldValue instanceof JavadocComment) { 155 List<TokenTextElement> matchingTokens = findTokenTextElementForComment((JavadocComment) oldValue, nodeText); 156 157 if (matchingTokens.size() != 1) { 158 throw new IllegalStateException("The matching comment to be replaced could not be found"); 159 } 160 161 JavadocComment newJavadocComment = (JavadocComment) newValue; 162 TokenTextElement matchingElement = matchingTokens.get(0); 163 nodeText.replace(matchingElement.and(matchingElement.matchByRange()), new TokenTextElement(JAVADOC_COMMENT, "/**" + newJavadocComment.getContent() + "*/")); 164 } else { 165 throw new UnsupportedOperationException(); 166 } 167 } 168 } 169 NodeText nodeText = getOrCreateNodeText(observedNode); 170 171 if (nodeText == null) { 172 throw new NullPointerException(observedNode.getClass().getSimpleName()); 173 } 174 175 LEXICAL_DIFFERENCE_CALCULATOR.calculatePropertyChange(nodeText, observedNode, property, oldValue, newValue); 176 } 177 getIndexOfComment(Comment oldValue, NodeText nodeText)178 private int getIndexOfComment (Comment oldValue, NodeText nodeText) { 179 int index; 180 List<TokenTextElement> matchingTokens = findTokenTextElementForComment((Comment) oldValue, nodeText); 181 182 if (!matchingTokens.isEmpty()){ 183 TextElement matchingElement = matchingTokens.get(0); 184 index = nodeText.findElement(matchingElement.and(matchingElement.matchByRange())); 185 } else { 186 // If no matching TokenTextElements were found, we try searching through ChildTextElements as well 187 List<ChildTextElement> matchingChilds = findChildTextElementForComment (oldValue, nodeText); 188 ChildTextElement matchingChild = matchingChilds.get(0); 189 index = nodeText.findElement(matchingChild.and(matchingChild.matchByRange())); 190 } 191 192 return index; 193 } 194 findChildTextElementForComment(Comment oldValue, NodeText nodeText)195 private List<ChildTextElement> findChildTextElementForComment (Comment oldValue, NodeText nodeText) { 196 List<ChildTextElement> matchingChildElements; 197 198 matchingChildElements = nodeText.getElements().stream() 199 .filter(e -> e.isChild()) 200 .map(c -> (ChildTextElement) c) 201 .filter(c -> c.isComment()) 202 .filter(c -> ((Comment)c.getChild()).getContent().equals(oldValue.getContent())) 203 .collect(Collectors.toList()); 204 205 if (matchingChildElements.size() > 1) { 206 // Duplicate child nodes found, refine the result 207 matchingChildElements = matchingChildElements.stream() 208 .filter(t -> isEqualRange(t.getChild().getRange(), oldValue.getRange())) 209 .collect(Collectors.toList()); 210 } 211 212 if (matchingChildElements.size() != 1) { 213 throw new IllegalStateException("The matching child text element for the comment to be removed could not be found."); 214 } 215 216 return matchingChildElements; 217 } 218 findTokenTextElementForComment(Comment oldValue, NodeText nodeText)219 private List<TokenTextElement> findTokenTextElementForComment(Comment oldValue, NodeText nodeText) { 220 List<TokenTextElement> matchingTokens; 221 222 if (oldValue instanceof JavadocComment) { 223 matchingTokens = nodeText.getElements().stream() 224 .filter(e -> e.isToken(JAVADOC_COMMENT)) 225 .map(e -> (TokenTextElement) e) 226 .filter(t -> t.getText().equals("/**" + oldValue.getContent() + "*/")) 227 .collect(Collectors.toList()); 228 } else if (oldValue instanceof BlockComment) { 229 matchingTokens = nodeText.getElements().stream() 230 .filter(e -> e.isToken(MULTI_LINE_COMMENT)) 231 .map(e -> (TokenTextElement) e) 232 .filter(t -> t.getText().equals("/*" + oldValue.getContent() + "*/")) 233 .collect(Collectors.toList()); 234 } else { 235 matchingTokens = nodeText.getElements().stream() 236 .filter(e -> e.isToken(SINGLE_LINE_COMMENT)) 237 .map(e -> (TokenTextElement) e) 238 .filter(t -> t.getText().trim().equals(("//" + oldValue.getContent()).trim())) 239 .collect(Collectors.toList()); 240 } 241 242 if (matchingTokens.size() > 1) { 243 // Duplicate comments found, refine the result 244 matchingTokens = matchingTokens.stream() 245 .filter(t -> isEqualRange(t.getToken().getRange(), oldValue.getRange())) 246 .collect(Collectors.toList()); 247 } 248 249 return matchingTokens; 250 } 251 252 isEqualRange(Optional<Range> range1, Optional<Range> range2)253 private boolean isEqualRange(Optional<Range> range1, Optional<Range> range2) { 254 if (range1.isPresent() && range2.isPresent()) { 255 return range1.get().equals(range2.get()); 256 } 257 258 return false; 259 } 260 261 @Override concreteListChange(NodeList changedList, AstObserver.ListChangeType type, int index, Node nodeAddedOrRemoved)262 public void concreteListChange(NodeList changedList, AstObserver.ListChangeType type, int index, Node nodeAddedOrRemoved) { 263 NodeText nodeText = getOrCreateNodeText(changedList.getParentNodeForChildren()); 264 final List<DifferenceElement> differenceElements; 265 if (type == AstObserver.ListChangeType.REMOVAL) { 266 differenceElements = LEXICAL_DIFFERENCE_CALCULATOR.calculateListRemovalDifference(findNodeListName(changedList), changedList, index); 267 } else if (type == AstObserver.ListChangeType.ADDITION) { 268 differenceElements = LEXICAL_DIFFERENCE_CALCULATOR.calculateListAdditionDifference(findNodeListName(changedList), changedList, index, nodeAddedOrRemoved); 269 } else { 270 throw new UnsupportedOperationException(); 271 } 272 273 Difference difference = new Difference(differenceElements, nodeText, changedList.getParentNodeForChildren()); 274 difference.apply(); 275 } 276 277 @Override concreteListReplacement(NodeList changedList, int index, Node oldValue, Node newValue)278 public void concreteListReplacement(NodeList changedList, int index, Node oldValue, Node newValue) { 279 NodeText nodeText = getOrCreateNodeText(changedList.getParentNodeForChildren()); 280 List<DifferenceElement> differenceElements = LEXICAL_DIFFERENCE_CALCULATOR.calculateListReplacementDifference(findNodeListName(changedList), changedList, index, newValue); 281 282 Difference difference = new Difference(differenceElements, nodeText, changedList.getParentNodeForChildren()); 283 difference.apply(); 284 } 285 } 286 storeInitialText(Node root)287 private static void storeInitialText(Node root) { 288 Map<Node, List<JavaToken>> tokensByNode = new IdentityHashMap<>(); 289 290 // We go over tokens and find to which nodes they belong. Note that we do not traverse the tokens as they were 291 // on a list but as they were organized in a tree. At each time we select only the branch corresponding to the 292 // range of interest and ignore all other branches 293 for (JavaToken token : root.getTokenRange().get()) { 294 Range tokenRange = token.getRange().orElseThrow(() -> new RuntimeException("Token without range: " + token)); 295 Node owner = findNodeForToken(root, tokenRange); 296 if (owner == null) { 297 throw new RuntimeException("Token without node owning it: " + token); 298 } 299 if (!tokensByNode.containsKey(owner)) { 300 tokensByNode.put(owner, new LinkedList<>()); 301 } 302 tokensByNode.get(owner).add(token); 303 } 304 305 // Now that we know the tokens we use them to create the initial NodeText for each node 306 new TreeVisitor() { 307 @Override 308 public void process(Node node) { 309 if (!PhantomNodeLogic.isPhantomNode(node)) { 310 LexicalPreservingPrinter.storeInitialTextForOneNode(node, tokensByNode.get(node)); 311 } 312 } 313 }.visitBreadthFirst(root); 314 } 315 findNodeForToken(Node node, Range tokenRange)316 private static Node findNodeForToken(Node node, Range tokenRange) { 317 if (PhantomNodeLogic.isPhantomNode(node)) { 318 return null; 319 } 320 if (node.getRange().get().contains(tokenRange)) { 321 for (Node child : node.getChildNodes()) { 322 Node found = findNodeForToken(child, tokenRange); 323 if (found != null) { 324 return found; 325 } 326 } 327 return node; 328 } else { 329 return null; 330 } 331 } 332 storeInitialTextForOneNode(Node node, List<JavaToken> nodeTokens)333 private static void storeInitialTextForOneNode(Node node, List<JavaToken> nodeTokens) { 334 if (nodeTokens == null) { 335 nodeTokens = Collections.emptyList(); 336 } 337 List<Pair<Range, TextElement>> elements = new LinkedList<>(); 338 for (Node child : node.getChildNodes()) { 339 if (!PhantomNodeLogic.isPhantomNode(child)) { 340 if (!child.getRange().isPresent()) { 341 throw new RuntimeException("Range not present on node " + child); 342 } 343 elements.add(new Pair<>(child.getRange().get(), new ChildTextElement(child))); 344 } 345 } 346 for (JavaToken token : nodeTokens) { 347 elements.add(new Pair<>(token.getRange().get(), new TokenTextElement(token))); 348 } 349 elements.sort(comparing(e -> e.a.begin)); 350 node.setData(NODE_TEXT_DATA, new NodeText(elements.stream().map(p -> p.b).collect(Collectors.toList()))); 351 } 352 353 // 354 // Iterators 355 // 356 tokensPreceeding(final Node node)357 private static Iterator<TokenTextElement> tokensPreceeding(final Node node) { 358 if (!node.getParentNode().isPresent()) { 359 return new TextElementIteratorsFactory.EmptyIterator<>(); 360 } 361 // There is the awfully painful case of the fake types involved in variable declarators and 362 // fields or variable declaration that are, of course, an exception... 363 364 NodeText parentNodeText = getOrCreateNodeText(node.getParentNode().get()); 365 int index = parentNodeText.tryToFindChild(node); 366 if (index == NodeText.NOT_FOUND) { 367 if (node.getParentNode().get() instanceof VariableDeclarator) { 368 return tokensPreceeding(node.getParentNode().get()); 369 } else { 370 throw new IllegalArgumentException( 371 String.format("I could not find child '%s' in parent '%s'. parentNodeText: %s", 372 node, node.getParentNode().get(), parentNodeText)); 373 } 374 } 375 376 return new TextElementIteratorsFactory.CascadingIterator<>( 377 TextElementIteratorsFactory.partialReverseIterator(parentNodeText, index - 1), 378 () -> tokensPreceeding(node.getParentNode().get())); 379 } 380 381 // 382 // Printing methods 383 // 384 385 /** 386 * Print a Node into a String, preserving the lexical information. 387 */ print(Node node)388 public static String print(Node node) { 389 StringWriter writer = new StringWriter(); 390 try { 391 print(node, writer); 392 } catch (IOException e) { 393 throw new RuntimeException("Unexpected IOException on a StringWriter", e); 394 } 395 return writer.toString(); 396 } 397 398 /** 399 * Print a Node into a Writer, preserving the lexical information. 400 */ print(Node node, Writer writer)401 public static void print(Node node, Writer writer) throws IOException { 402 if (!node.containsData(NODE_TEXT_DATA)) { 403 getOrCreateNodeText(node); 404 } 405 final NodeText text = node.getData(NODE_TEXT_DATA); 406 writer.append(text.expand()); 407 } 408 409 // 410 // Methods to handle transformations 411 // 412 prettyPrintingTextNode(Node node, NodeText nodeText)413 private static void prettyPrintingTextNode(Node node, NodeText nodeText) { 414 if (node instanceof PrimitiveType) { 415 PrimitiveType primitiveType = (PrimitiveType) node; 416 switch (primitiveType.getType()) { 417 case BOOLEAN: 418 nodeText.addToken(BOOLEAN, node.toString()); 419 break; 420 case CHAR: 421 nodeText.addToken(CHAR, node.toString()); 422 break; 423 case BYTE: 424 nodeText.addToken(BYTE, node.toString()); 425 break; 426 case SHORT: 427 nodeText.addToken(SHORT, node.toString()); 428 break; 429 case INT: 430 nodeText.addToken(INT, node.toString()); 431 break; 432 case LONG: 433 nodeText.addToken(LONG, node.toString()); 434 break; 435 case FLOAT: 436 nodeText.addToken(FLOAT, node.toString()); 437 break; 438 case DOUBLE: 439 nodeText.addToken(DOUBLE, node.toString()); 440 break; 441 default: 442 throw new IllegalArgumentException(); 443 } 444 return; 445 } 446 if (node instanceof JavadocComment) { 447 nodeText.addToken(JAVADOC_COMMENT, "/**" + ((JavadocComment) node).getContent() + "*/"); 448 return; 449 } 450 if (node instanceof BlockComment) { 451 nodeText.addToken(MULTI_LINE_COMMENT, "/*" + ((BlockComment) node).getContent() + "*/"); 452 return; 453 } 454 if (node instanceof LineComment) { 455 nodeText.addToken(SINGLE_LINE_COMMENT, "//" + ((LineComment) node).getContent()); 456 return; 457 } 458 if (node instanceof Modifier) { 459 Modifier modifier = (Modifier)node; 460 nodeText.addToken(LexicalDifferenceCalculator.toToken(modifier), modifier.getKeyword().asString()); 461 return; 462 } 463 464 interpret(node, ConcreteSyntaxModel.forClass(node.getClass()), nodeText); 465 } 466 467 /** 468 * TODO: Process CsmIndent and CsmUnindent before reaching this point 469 */ interpret(Node node, CsmElement csm, NodeText nodeText)470 private static NodeText interpret(Node node, CsmElement csm, NodeText nodeText) { 471 LexicalDifferenceCalculator.CalculatedSyntaxModel calculatedSyntaxModel = new LexicalDifferenceCalculator().calculatedSyntaxModelForNode(csm, node); 472 473 List<TokenTextElement> indentation = findIndentation(node); 474 475 boolean pendingIndentation = false; 476 for (CsmElement element : calculatedSyntaxModel.elements) { 477 if (pendingIndentation && !(element instanceof CsmToken && ((CsmToken) element).isNewLine())) { 478 indentation.forEach(nodeText::addElement); 479 } 480 pendingIndentation = false; 481 if (element instanceof LexicalDifferenceCalculator.CsmChild) { 482 nodeText.addChild(((LexicalDifferenceCalculator.CsmChild) element).getChild()); 483 } else if (element instanceof CsmToken) { 484 CsmToken csmToken = (CsmToken) element; 485 nodeText.addToken(csmToken.getTokenType(), csmToken.getContent(node)); 486 if (csmToken.isNewLine()) { 487 pendingIndentation = true; 488 } 489 } else if (element instanceof CsmMix) { 490 CsmMix csmMix = (CsmMix) element; 491 csmMix.getElements().forEach(e -> interpret(node, e, nodeText)); 492 493 // Indentation should probably be dealt with before because an indentation has effects also on the 494 // following lines 495 496 } else if (element instanceof CsmIndent) { 497 for (int i = 0; i < Difference.STANDARD_INDENTATION_SIZE; i++) { 498 nodeText.addToken(SPACE, " "); 499 } 500 } else if (element instanceof CsmUnindent) { 501 for (int i = 0; i < Difference.STANDARD_INDENTATION_SIZE; i++) { 502 if (nodeText.endWithSpace()) { 503 nodeText.removeLastElement(); 504 } 505 } 506 } else { 507 throw new UnsupportedOperationException(element.getClass().getSimpleName()); 508 } 509 } 510 // Array brackets are a pain... we do not have a way to represent them explicitly in the AST 511 // so they have to be handled in a special way 512 if (node instanceof VariableDeclarator) { 513 VariableDeclarator variableDeclarator = (VariableDeclarator) node; 514 variableDeclarator.getParentNode().ifPresent(parent -> 515 ((NodeWithVariables<?>) parent).getMaximumCommonType().ifPresent(mct -> { 516 int extraArrayLevels = variableDeclarator.getType().getArrayLevel() - mct.getArrayLevel(); 517 for (int i = 0; i < extraArrayLevels; i++) { 518 nodeText.addElement(new TokenTextElement(LBRACKET)); 519 nodeText.addElement(new TokenTextElement(RBRACKET)); 520 } 521 }) 522 ); 523 } 524 return nodeText; 525 } 526 527 // Visible for testing getOrCreateNodeText(Node node)528 static NodeText getOrCreateNodeText(Node node) { 529 if (!node.containsData(NODE_TEXT_DATA)) { 530 NodeText nodeText = new NodeText(); 531 node.setData(NODE_TEXT_DATA, nodeText); 532 prettyPrintingTextNode(node, nodeText); 533 } 534 return node.getData(NODE_TEXT_DATA); 535 } 536 537 // Visible for testing findIndentation(Node node)538 static List<TokenTextElement> findIndentation(Node node) { 539 List<TokenTextElement> followingNewlines = new LinkedList<>(); 540 Iterator<TokenTextElement> it = tokensPreceeding(node); 541 while (it.hasNext()) { 542 TokenTextElement tte = it.next(); 543 if (tte.getTokenKind() == SINGLE_LINE_COMMENT 544 || tte.isNewline()) { 545 break; 546 } else { 547 followingNewlines.add(tte); 548 } 549 } 550 Collections.reverse(followingNewlines); 551 for (int i = 0; i < followingNewlines.size(); i++) { 552 if (!followingNewlines.get(i).isSpaceOrTab()) { 553 return followingNewlines.subList(0, i); 554 } 555 } 556 return followingNewlines; 557 } 558 559 // 560 // Helper methods 561 // 562 isReturningOptionalNodeList(Method m)563 private static boolean isReturningOptionalNodeList(Method m) { 564 if (!m.getReturnType().getCanonicalName().equals(Optional.class.getCanonicalName())) { 565 return false; 566 } 567 if (!(m.getGenericReturnType() instanceof ParameterizedType)) { 568 return false; 569 } 570 ParameterizedType parameterizedType = (ParameterizedType) m.getGenericReturnType(); 571 java.lang.reflect.Type optionalArgument = parameterizedType.getActualTypeArguments()[0]; 572 return (optionalArgument.getTypeName().startsWith(NodeList.class.getCanonicalName())); 573 } 574 findNodeListName(NodeList nodeList)575 private static ObservableProperty findNodeListName(NodeList nodeList) { 576 Node parent = nodeList.getParentNodeForChildren(); 577 for (Method m : parent.getClass().getMethods()) { 578 if (m.getParameterCount() == 0 && m.getReturnType().getCanonicalName().equals(NodeList.class.getCanonicalName())) { 579 try { 580 Object raw = m.invoke(parent); 581 if (!(raw instanceof NodeList)) { 582 throw new IllegalStateException("Expected NodeList, found " + raw.getClass().getCanonicalName()); 583 } 584 NodeList result = (NodeList) raw; 585 if (result == nodeList) { 586 String name = m.getName(); 587 if (name.startsWith("get")) { 588 name = name.substring("get".length()); 589 } 590 return ObservableProperty.fromCamelCaseName(decapitalize(name)); 591 } 592 } catch (IllegalAccessException | InvocationTargetException e) { 593 throw new RuntimeException(e); 594 } 595 } else if (m.getParameterCount() == 0 && isReturningOptionalNodeList(m)) { 596 try { 597 Optional<NodeList<?>> raw = (Optional<NodeList<?>>) m.invoke(parent); 598 if (raw.isPresent() && raw.get() == nodeList) { 599 String name = m.getName(); 600 if (name.startsWith("get")) { 601 name = name.substring("get".length()); 602 } 603 return ObservableProperty.fromCamelCaseName(decapitalize(name)); 604 } 605 } catch (IllegalAccessException | InvocationTargetException e) { 606 throw new RuntimeException(e); 607 } 608 } 609 } 610 throw new IllegalArgumentException("Cannot find list name of NodeList of size " + nodeList.size()); 611 } 612 } 613