    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}