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}