001// Licensed under the Apache License, Version 2.0 (the "License"); 002// you may not use this file except in compliance with the License. 003// You may obtain a copy of the License at 004// 005// http://www.apache.org/licenses/LICENSE-2.0 006// 007// Unless required by applicable law or agreed to in writing, software 008// distributed under the License is distributed on an "AS IS" BASIS, 009// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 010// See the License for the specific language governing permissions and 011// limitations under the License. 012 013package org.apache.tapestry5.internal.services; 014 015import java.util.Collection; 016import java.util.Collections; 017import java.util.Formatter; 018import java.util.List; 019import java.util.Map; 020import java.util.Set; 021import java.util.regex.Pattern; 022 023import org.apache.tapestry5.SymbolConstants; 024import org.apache.tapestry5.commons.services.InvalidationListener; 025import org.apache.tapestry5.commons.util.AvailableValues; 026import org.apache.tapestry5.commons.util.CollectionFactory; 027import org.apache.tapestry5.commons.util.UnknownValueException; 028import org.apache.tapestry5.func.F; 029import org.apache.tapestry5.internal.InternalConstants; 030import org.apache.tapestry5.ioc.annotations.Symbol; 031import org.apache.tapestry5.ioc.internal.util.InternalUtils; 032import org.apache.tapestry5.ioc.services.ClassNameLocator; 033import org.apache.tapestry5.services.ComponentClassResolver; 034import org.apache.tapestry5.services.LibraryMapping; 035import org.apache.tapestry5.services.transform.ControlledPackageType; 036import org.slf4j.Logger; 037 038public class ComponentClassResolverImpl implements ComponentClassResolver, InvalidationListener 039{ 040 private static final String CORE_LIBRARY_PREFIX = "core/"; 041 042 private static final Pattern SPLIT_PACKAGE_PATTERN = Pattern.compile("\\."); 043 044 private static final Pattern SPLIT_FOLDER_PATTERN = Pattern.compile("/"); 045 046 private static final int LOGICAL_NAME_BUFFER_SIZE = 40; 047 048 private final Logger logger; 049 050 private final ClassNameLocator classNameLocator; 051 052 private final String startPageName; 053 054 // Map from library name to a list of root package names (usuallly just one). 055 private final Map<String, List<String>> libraryNameToPackageNames = CollectionFactory.newCaseInsensitiveMap(); 056 057 private final Map<String, ControlledPackageType> packageNameToType = CollectionFactory.newMap(); 058 059 /** 060 * Maps from a root package name to a component library name, including the empty string as the 061 * library name of the application. 062 */ 063 private final Map<String, String> packageNameToLibraryName = CollectionFactory.newMap(); 064 065 // Flag indicating that the maps have been cleared following an invalidation 066 // and need to be rebuilt. The flag and the four maps below are not synchronized 067 // because they are only modified inside a synchronized block. That should be strong enough ... 068 // and changes made will become "visible" at the end of the synchronized block. Because of the 069 // structure of Tapestry, there should not be any reader threads while the write thread 070 // is operating. 071 072 private volatile boolean needsRebuild = true; 073 074 private final Collection<LibraryMapping> libraryMappings; 075 076 private final Pattern endsWithPagePattern = Pattern.compile(".*/?\\w+page$", Pattern.CASE_INSENSITIVE); 077 078 private boolean endsWithPage(String name) 079 { 080 // Don't treat a name that's just "page" as a suffix to strip off. 081 082 return endsWithPagePattern.matcher(name).matches(); 083 } 084 085 private class Data 086 { 087 088 /** 089 * Logical page name to class name. 090 */ 091 private final Map<String, String> pageToClassName = CollectionFactory.newCaseInsensitiveMap(); 092 093 /** 094 * Component type to class name. 095 */ 096 private final Map<String, String> componentToClassName = CollectionFactory.newCaseInsensitiveMap(); 097 098 /** 099 * Mixing type to class name. 100 */ 101 private final Map<String, String> mixinToClassName = CollectionFactory.newCaseInsensitiveMap(); 102 103 /** 104 * Page class name to logical name (needed to build URLs). This one is case sensitive, since class names do always 105 * have a particular case. 106 */ 107 private final Map<String, String> pageClassNameToLogicalName = CollectionFactory.newMap(); 108 109 /** 110 * Used to convert a logical page name to the canonical form of the page name; this ensures that uniform case for 111 * page names is used. 112 */ 113 private final Map<String, String> pageNameToCanonicalPageName = CollectionFactory.newCaseInsensitiveMap(); 114 115 116 /** 117 * These are used to check for name overlaps: a single name (generated by different paths) that maps to more than one class. 118 */ 119 private Map<String, Set<String>> pageToClassNames = CollectionFactory.newCaseInsensitiveMap(); 120 121 private Map<String, Set<String>> componentToClassNames = CollectionFactory.newCaseInsensitiveMap(); 122 123 private Map<String, Set<String>> mixinToClassNames = CollectionFactory.newCaseInsensitiveMap(); 124 125 private boolean invalid = false; 126 127 private void rebuild(String pathPrefix, String rootPackage) 128 { 129 fill(pathPrefix, rootPackage, InternalConstants.PAGES_SUBPACKAGE, pageToClassName, pageToClassNames); 130 fill(pathPrefix, rootPackage, InternalConstants.COMPONENTS_SUBPACKAGE, componentToClassName, componentToClassNames); 131 fill(pathPrefix, rootPackage, InternalConstants.MIXINS_SUBPACKAGE, mixinToClassName, mixinToClassNames); 132 } 133 134 private void fill(String pathPrefix, String rootPackage, String subPackage, 135 Map<String, String> logicalNameToClassName, 136 Map<String, Set<String>> nameToClassNames) 137 { 138 String searchPackage = rootPackage + "." + subPackage; 139 boolean isPage = subPackage.equals(InternalConstants.PAGES_SUBPACKAGE); 140 141 Collection<String> classNames = classNameLocator.locateClassNames(searchPackage); 142 143 Set<String> aliases = CollectionFactory.newSet(); 144 145 int startPos = searchPackage.length() + 1; 146 147 for (String className : classNames) 148 { 149 aliases.clear(); 150 151 String logicalName = toLogicalName(className, pathPrefix, startPos, true); 152 String unstrippedName = toLogicalName(className, pathPrefix, startPos, false); 153 154 aliases.add(logicalName); 155 aliases.add(unstrippedName); 156 157 if (isPage) 158 { 159 if (endsWithPage(logicalName)) 160 { 161 logicalName = logicalName.substring(0, logicalName.length() - 4); 162 aliases.add(logicalName); 163 } 164 165 int lastSlashx = logicalName.lastIndexOf("/"); 166 167 String lastTerm = lastSlashx < 0 ? logicalName : logicalName.substring(lastSlashx + 1); 168 169 if (lastTerm.equalsIgnoreCase("index")) 170 { 171 String reducedName = lastSlashx < 0 ? "" : logicalName.substring(0, lastSlashx); 172 173 // Make the super-stripped name another alias to the class. 174 // TAP5-1444: Everything else but a start page has precedence 175 176 177 aliases.add(reducedName); 178 } 179 180 if (logicalName.equals(startPageName)) 181 { 182 aliases.add(""); 183 } 184 185 pageClassNameToLogicalName.put(className, logicalName); 186 } 187 188 for (String alias : aliases) 189 { 190 logicalNameToClassName.put(alias, className); 191 addNameMapping(nameToClassNames, alias, className); 192 193 if (isPage) 194 { 195 pageNameToCanonicalPageName.put(alias, logicalName); 196 } 197 } 198 } 199 } 200 201 /** 202 * Converts a fully qualified class name to a logical name 203 * 204 * @param className 205 * fully qualified class name 206 * @param pathPrefix 207 * prefix to be placed on the logical name (to identify the library from in which the class 208 * lives) 209 * @param startPos 210 * start position within the class name to extract the logical name (i.e., after the final '.' in 211 * "rootpackage.pages."). 212 * @param stripTerms 213 * @return a short logical name in folder format ('.' replaced with '/') 214 */ 215 private String toLogicalName(String className, String pathPrefix, int startPos, boolean stripTerms) 216 { 217 List<String> terms = CollectionFactory.newList(); 218 219 addAll(terms, SPLIT_FOLDER_PATTERN, pathPrefix); 220 221 addAll(terms, SPLIT_PACKAGE_PATTERN, className.substring(startPos)); 222 223 StringBuilder builder = new StringBuilder(LOGICAL_NAME_BUFFER_SIZE); 224 String sep = ""; 225 226 String logicalName = terms.remove(terms.size() - 1); 227 228 String unstripped = logicalName; 229 230 for (String term : terms) 231 { 232 builder.append(sep); 233 builder.append(term); 234 235 sep = "/"; 236 237 if (stripTerms) 238 { 239 logicalName = stripTerm(term, logicalName); 240 } 241 } 242 243 if (logicalName.equals("")) 244 { 245 logicalName = unstripped; 246 } 247 248 builder.append(sep); 249 builder.append(logicalName); 250 251 return builder.toString(); 252 } 253 254 private void addAll(List<String> terms, Pattern splitter, String input) 255 { 256 for (String term : splitter.split(input)) 257 { 258 if (term.equals("")) 259 continue; 260 261 terms.add(term); 262 } 263 } 264 265 private String stripTerm(String term, String logicalName) 266 { 267 if (isCaselessPrefix(term, logicalName)) 268 { 269 logicalName = logicalName.substring(term.length()); 270 } 271 272 if (isCaselessSuffix(term, logicalName)) 273 { 274 logicalName = logicalName.substring(0, logicalName.length() - term.length()); 275 } 276 277 return logicalName; 278 } 279 280 private boolean isCaselessPrefix(String prefix, String string) 281 { 282 return string.regionMatches(true, 0, prefix, 0, prefix.length()); 283 } 284 285 private boolean isCaselessSuffix(String suffix, String string) 286 { 287 return string.regionMatches(true, string.length() - suffix.length(), suffix, 0, suffix.length()); 288 } 289 290 private void addNameMapping(Map<String, Set<String>> map, String name, String className) 291 { 292 Set<String> classNames = map.get(name); 293 294 if (classNames == null) 295 { 296 classNames = CollectionFactory.newSet(); 297 map.put(name, classNames); 298 } 299 300 classNames.add(className); 301 } 302 303 private void validate() 304 { 305 validate("page name", pageToClassNames); 306 validate("component type", componentToClassNames); 307 validate("mixin type", mixinToClassNames); 308 309 // No longer needed after validation. 310 pageToClassNames = null; 311 componentToClassNames = null; 312 mixinToClassNames = null; 313 314 if (invalid) 315 { 316 throw new IllegalStateException("You must correct these validation issues to proceed."); 317 } 318 } 319 320 private void validate(String category, Map<String, Set<String>> map) 321 { 322 boolean header = false; 323 324 for (String name : F.flow(map.keySet()).sort()) 325 { 326 Set<String> classNames = map.get(name); 327 328 if (classNames.size() == 1) 329 { 330 continue; 331 } 332 333 if (!header) 334 { 335 logger.error(String.format("Some %s(s) map to more than one Java class.", category)); 336 header = true; 337 invalid = true; 338 } 339 340 logger.error(String.format("%s '%s' maps to %s", 341 InternalUtils.capitalize(category), 342 name, 343 InternalUtils.joinSorted(classNames))); 344 } 345 } 346 } 347 348 private volatile Data data = new Data(); 349 350 public ComponentClassResolverImpl(Logger logger, 351 352 ClassNameLocator classNameLocator, 353 354 @Symbol(SymbolConstants.START_PAGE_NAME) 355 String startPageName, 356 357 Collection<LibraryMapping> mappings) 358 { 359 this.logger = logger; 360 this.classNameLocator = classNameLocator; 361 362 this.startPageName = startPageName; 363 this.libraryMappings = Collections.unmodifiableCollection(mappings); 364 365 for (LibraryMapping mapping : mappings) 366 { 367 String libraryName = mapping.libraryName; 368 369 List<String> packages = this.libraryNameToPackageNames.get(libraryName); 370 371 if (packages == null) 372 { 373 packages = CollectionFactory.newList(); 374 this.libraryNameToPackageNames.put(libraryName, packages); 375 } 376 377 packages.add(mapping.rootPackage); 378 379 // These packages, which will contain classes subject to class transformation, 380 // must be registered with the component instantiator (which is responsible 381 // for transformation). 382 383 addSubpackagesToPackageMapping(mapping.rootPackage); 384 385 packageNameToLibraryName.put(mapping.rootPackage, libraryName); 386 } 387 } 388 389 private void addSubpackagesToPackageMapping(String rootPackage) 390 { 391 for (String subpackage : InternalConstants.SUBPACKAGES) 392 { 393 packageNameToType.put(rootPackage + "." + subpackage, ControlledPackageType.COMPONENT); 394 } 395 } 396 397 public Map<String, ControlledPackageType> getControlledPackageMapping() 398 { 399 return Collections.unmodifiableMap(packageNameToType); 400 } 401 402 /** 403 * When the class loader is invalidated, clear any cached page names or component types. 404 */ 405 public synchronized void objectWasInvalidated() 406 { 407 needsRebuild = true; 408 } 409 410 /** 411 * Returns the current data, or atomically rebuilds it. In rare race conditions, the data may be rebuilt more than once, overlapping. 412 */ 413 private Data getData() 414 { 415 if (!needsRebuild) 416 { 417 return data; 418 } 419 420 Data newData = new Data(); 421 422 for (Map.Entry<String, List<String>> entry : libraryNameToPackageNames.entrySet()) 423 { 424 List<String> packages = entry.getValue(); 425 426 String folder = entry.getKey() + "/"; 427 428 for (String packageName : packages) 429 { 430 newData.rebuild(folder, packageName); 431 } 432 } 433 434 newData.validate(); 435 436 showChanges("pages", data.pageToClassName, newData.pageToClassName); 437 showChanges("components", data.componentToClassName, newData.componentToClassName); 438 showChanges("mixins", data.mixinToClassName, newData.mixinToClassName); 439 440 needsRebuild = false; 441 442 data = newData; 443 444 return data; 445 } 446 447 private static int countUnique(Map<String, String> map) 448 { 449 return CollectionFactory.newSet(map.values()).size(); 450 } 451 452 /** 453 * Log (at INFO level) the changes between the two logical-name-to-class-name maps 454 * 455 * @param title 456 * the title of the things in the maps (e.g. "pages" or "components") 457 * @param savedMap 458 * the old map 459 * @param newMap 460 * the new map 461 */ 462 private void showChanges(String title, Map<String, String> savedMap, Map<String, String> newMap) 463 { 464 if (savedMap.equals(newMap) || !logger.isInfoEnabled()) // nothing to log? 465 { 466 return; 467 } 468 469 Map<String, String> core = CollectionFactory.newMap(); 470 Map<String, String> nonCore = CollectionFactory.newMap(); 471 472 473 int maxLength = 0; 474 475 // Pass # 1: Get all the stuff in the core library 476 477 for (String name : newMap.keySet()) 478 { 479 if (name.startsWith(CORE_LIBRARY_PREFIX)) 480 { 481 // Strip off the "core/" prefix. 482 483 String key = name.substring(CORE_LIBRARY_PREFIX.length()); 484 485 maxLength = Math.max(maxLength, key.length()); 486 487 core.put(key, newMap.get(name)); 488 } else 489 { 490 maxLength = Math.max(maxLength, name.length()); 491 492 nonCore.put(name, newMap.get(name)); 493 } 494 } 495 496 // Merge the non-core mappings into the core mappings. Where there are conflicts on name, it 497 // means the application overrode a core page/component/mixin and that's ok ... the 498 // merged core map will reflect the application's mapping. 499 500 core.putAll(nonCore); 501 502 StringBuilder builder = new StringBuilder(2000); 503 Formatter f = new Formatter(builder); 504 505 int oldCount = countUnique(savedMap); 506 int newCount = countUnique(newMap); 507 508 f.format("Available %s (%d", title, newCount); 509 510 if (oldCount > 0 && oldCount != newCount) 511 { 512 f.format(", +%d", newCount - oldCount); 513 } 514 515 builder.append("):\n"); 516 517 String formatString = "%" + maxLength + "s: %s\n"; 518 519 List<String> sorted = InternalUtils.sortedKeys(core); 520 521 for (String name : sorted) 522 { 523 String className = core.get(name); 524 525 if (name.equals("")) 526 name = "(blank)"; 527 528 f.format(formatString, name, className); 529 } 530 531 // log multi-line string with OS-specific line endings (TAP5-2294) 532 logger.info(builder.toString().replaceAll("\\n", System.getProperty("line.separator"))); 533 } 534 535 536 public String resolvePageNameToClassName(final String pageName) 537 { 538 Data data = getData(); 539 540 String result = locate(pageName, data.pageToClassName); 541 542 if (result == null) 543 { 544 throw new UnknownValueException(String.format("Unable to resolve '%s' to a page class name.", 545 pageName), new AvailableValues("Page names", presentableNames(data.pageToClassName))); 546 } 547 548 return result; 549 } 550 551 public boolean isPageName(final String pageName) 552 { 553 return locate(pageName, getData().pageToClassName) != null; 554 } 555 556 public boolean isPage(final String pageClassName) 557 { 558 return locate(pageClassName, getData().pageClassNameToLogicalName) != null; 559 } 560 561 562 public List<String> getPageNames() 563 { 564 Data data = getData(); 565 566 List<String> result = CollectionFactory.newList(data.pageClassNameToLogicalName.values()); 567 568 Collections.sort(result); 569 570 return result; 571 } 572 573 public List<String> getComponentNames() 574 { 575 Data data = getData(); 576 577 List<String> result = CollectionFactory.newList(data.componentToClassName.keySet()); 578 579 Collections.sort(result); 580 581 return result; 582 } 583 584 public List<String> getMixinNames() 585 { 586 Data data = getData(); 587 588 List<String> result = CollectionFactory.newList(data.mixinToClassName.keySet()); 589 590 Collections.sort(result); 591 592 return result; 593 } 594 595 public String resolveComponentTypeToClassName(final String componentType) 596 { 597 Data data = getData(); 598 599 String result = locate(componentType, data.componentToClassName); 600 601 if (result == null) 602 { 603 throw new UnknownValueException(String.format("Unable to resolve '%s' to a component class name.", 604 componentType), new AvailableValues("Component types", 605 presentableNames(data.componentToClassName))); 606 } 607 608 return result; 609 } 610 611 Collection<String> presentableNames(Map<String, ?> map) 612 { 613 Set<String> result = CollectionFactory.newSet(); 614 615 for (String name : map.keySet()) 616 { 617 618 if (name.startsWith(CORE_LIBRARY_PREFIX)) 619 { 620 result.add(name.substring(CORE_LIBRARY_PREFIX.length())); 621 continue; 622 } 623 624 result.add(name); 625 } 626 627 return result; 628 } 629 630 public String resolveMixinTypeToClassName(final String mixinType) 631 { 632 Data data = getData(); 633 634 String result = locate(mixinType, data.mixinToClassName); 635 636 if (result == null) 637 { 638 throw new UnknownValueException(String.format("Unable to resolve '%s' to a mixin class name.", 639 mixinType), new AvailableValues("Mixin types", presentableNames(data.mixinToClassName))); 640 } 641 642 return result; 643 } 644 645 /** 646 * Locates a class name within the provided map, given its logical name. If not found naturally, a search inside the 647 * "core" library is included. 648 * 649 * @param logicalName 650 * name to search for 651 * @param logicalNameToClassName 652 * mapping from logical name to class name 653 * @return the located class name or null 654 */ 655 private String locate(String logicalName, Map<String, String> logicalNameToClassName) 656 { 657 String result = logicalNameToClassName.get(logicalName); 658 659 // If not found, see if it exists under the core package. In this way, 660 // anything in core is "inherited" (but overridable) by the application. 661 662 if (result != null) 663 { 664 return result; 665 } 666 667 return logicalNameToClassName.get(CORE_LIBRARY_PREFIX + logicalName); 668 } 669 670 public String resolvePageClassNameToPageName(final String pageClassName) 671 { 672 String result = getData().pageClassNameToLogicalName.get(pageClassName); 673 674 if (result == null) 675 { 676 throw new IllegalArgumentException(String.format("Unable to resolve class name %s to a logical page name.", pageClassName)); 677 } 678 679 return result; 680 } 681 682 public String canonicalizePageName(final String pageName) 683 { 684 Data data = getData(); 685 686 String result = locate(pageName, data.pageNameToCanonicalPageName); 687 688 if (result == null) 689 { 690 throw new UnknownValueException(String.format("Unable to resolve '%s' to a known page name.", 691 pageName), new AvailableValues("Page names", presentableNames(data.pageNameToCanonicalPageName))); 692 } 693 694 return result; 695 } 696 697 public Map<String, String> getFolderToPackageMapping() 698 { 699 Map<String, String> result = CollectionFactory.newCaseInsensitiveMap(); 700 701 for (Map.Entry<String, List<String>> entry : libraryNameToPackageNames.entrySet()) 702 { 703 String folder = entry.getKey(); 704 705 List<String> packageNames = entry.getValue(); 706 707 String packageName = findCommonPackageNameForFolder(folder, packageNames); 708 709 result.put(folder, packageName); 710 } 711 712 return result; 713 } 714 715 static String findCommonPackageNameForFolder(String folder, List<String> packageNames) 716 { 717 String packageName = findCommonPackageName(packageNames); 718 719 if (packageName == null) 720 throw new RuntimeException( 721 String.format( 722 "Package names for library folder '%s' (%s) can not be reduced to a common base package (of at least one term).", 723 folder, InternalUtils.joinSorted(packageNames))); 724 return packageName; 725 } 726 727 static String findCommonPackageName(List<String> packageNames) 728 { 729 // BTW, this is what reduce is for in Clojure ... 730 731 String commonPackageName = packageNames.get(0); 732 733 for (int i = 1; i < packageNames.size(); i++) 734 { 735 commonPackageName = findCommonPackageName(commonPackageName, packageNames.get(i)); 736 737 if (commonPackageName == null) 738 break; 739 } 740 741 return commonPackageName; 742 } 743 744 static String findCommonPackageName(String commonPackageName, String packageName) 745 { 746 String[] commonExploded = explode(commonPackageName); 747 String[] exploded = explode(packageName); 748 749 int count = Math.min(commonExploded.length, exploded.length); 750 751 int commonLength = 0; 752 int commonTerms = 0; 753 754 for (int i = 0; i < count; i++) 755 { 756 if (exploded[i].equals(commonExploded[i])) 757 { 758 // Keep track of the number of shared characters (including the dot seperators) 759 760 commonLength += exploded[i].length() + (i == 0 ? 0 : 1); 761 commonTerms++; 762 } else 763 { 764 break; 765 } 766 } 767 768 if (commonTerms < 1) 769 return null; 770 771 return commonPackageName.substring(0, commonLength); 772 } 773 774 private static final Pattern DOT = Pattern.compile("\\."); 775 776 private static String[] explode(String packageName) 777 { 778 return DOT.split(packageName); 779 } 780 781 public List<String> getLibraryNames() 782 { 783 return F.flow(libraryNameToPackageNames.keySet()).remove(F.IS_BLANK).sort().toList(); 784 } 785 786 public String getLibraryNameForClass(String className) 787 { 788 assert className != null; 789 790 String current = className; 791 792 while (true) 793 { 794 795 int dotx = current.lastIndexOf('.'); 796 797 if (dotx < 1) 798 { 799 throw new IllegalArgumentException(String.format("Class %s is not inside any package associated with any library.", 800 className)); 801 } 802 803 current = current.substring(0, dotx); 804 805 String libraryName = packageNameToLibraryName.get(current); 806 807 if (libraryName != null) 808 { 809 return libraryName; 810 } 811 } 812 } 813 814 @Override 815 public Collection<LibraryMapping> getLibraryMappings() 816 { 817 return libraryMappings; 818 } 819 820 @Override 821 public String getLogicalName(String className) 822 { 823 final Data thisData = getData(); 824 String result = thisData.pageClassNameToLogicalName.get(className); 825 if (result == null) 826 { 827 result = getKeyByValue(thisData.componentToClassName, className); 828 } 829 if (result == null ) 830 { 831 result = getKeyByValue(thisData.mixinToClassName, className); 832 } 833 834 return result; 835 } 836 837 @Override 838 public String getClassName(String logicalName) 839 { 840 final Data thisData = getData(); 841 String result = getKeyByValue(thisData.pageClassNameToLogicalName, logicalName); 842 if (result == null) 843 { 844 result = thisData.componentToClassName.get(logicalName); 845 } 846 if (result == null ) 847 { 848 result = thisData.mixinToClassName.get(logicalName); 849 } 850 return result; 851 } 852 853 854 private String getKeyByValue(Map<String, String> map, String value) 855 { 856 return map.entrySet().stream() 857 .filter(e -> e.getValue().equals(value)) 858 .map(e -> e.getKey()) 859 .findAny() 860 .orElse(null); 861 } 862 863}