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