    001    // Copyright 2006, 2007, 2008, 2009, 2010, 2011 The Apache Software Foundation
    002    //
    003    // Licensed under the Apache License, Version 2.0 (the "License");
    004    // you may not use this file except in compliance with the License.
    005    // You may obtain a copy of the License at
    006    //
    007    // http://www.apache.org/licenses/LICENSE-2.0
    008    //
    009    // Unless required by applicable law or agreed to in writing, software
    010    // distributed under the License is distributed on an "AS IS" BASIS,
    011    // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    012    // See the License for the specific language governing permissions and
    013    // limitations under the License.
    014
    015    package org.apache.tapestry5.internal.services;
    016
    017    import org.apache.tapestry5.SymbolConstants;
    018    import org.apache.tapestry5.internal.InternalConstants;
    019    import org.apache.tapestry5.ioc.annotations.Symbol;
    020    import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
    021    import org.apache.tapestry5.ioc.internal.util.InternalUtils;
    022    import org.apache.tapestry5.ioc.services.ClassNameLocator;
    023    import org.apache.tapestry5.ioc.util.AvailableValues;
    024    import org.apache.tapestry5.ioc.util.UnknownValueException;
    025    import org.apache.tapestry5.services.ComponentClassResolver;
    026    import org.apache.tapestry5.services.InvalidationListener;
    027    import org.apache.tapestry5.services.LibraryMapping;
    028    import org.apache.tapestry5.services.transform.ControlledPackageType;
    029    import org.slf4j.Logger;
    030
    031    import java.util.*;
    032    import java.util.regex.Pattern;
    033
    034    public class ComponentClassResolverImpl implements ComponentClassResolver, InvalidationListener
    035    {
    036        private static final String CORE_LIBRARY_PREFIX = "core/";
    037
    038        private static final Pattern SPLIT_PACKAGE_PATTERN = Pattern.compile("\\.");
    039
    040        private static final Pattern SPLIT_FOLDER_PATTERN = Pattern.compile("/");
    041
    042        private static final int LOGICAL_NAME_BUFFER_SIZE = 40;
    043
    044        private final Logger logger;
    045
    046        private final ClassNameLocator classNameLocator;
    047
    048        private final String startPageName;
    049
    050        // Map from folder name to a list of root package names.
    051        // The key does not begin or end with a slash.
    052
    053        private final Map<String, List<String>> mappings = CollectionFactory.newCaseInsensitiveMap();
    054
    055        private final Map<String, ControlledPackageType> packageMappings = CollectionFactory.newMap();
    056
    057        // Flag indicating that the maps have been cleared following an invalidation
    058        // and need to be rebuilt. The flag and the four maps below are not synchronized
    059        // because they are only modified inside a synchronized block. That should be strong enough ...
    060        // and changes made will become "visible" at the end of the synchronized block. Because of the
    061        // structure of Tapestry, there should not be any reader threads while the write thread
    062        // is operating.
    063
    064        private volatile boolean needsRebuild = true;
    065
    066        private class Data
    067        {
    068
    069            /**
    070             * Logical page name to class name.
    071             */
    072            private final Map<String, String> pageToClassName = CollectionFactory.newCaseInsensitiveMap();
    073
    074            /**
    075             * Component type to class name.
    076             */
    077            private final Map<String, String> componentToClassName = CollectionFactory.newCaseInsensitiveMap();
    078
    079            /**
    080             * Mixing type to class name.
    081             */
    082            private final Map<String, String> mixinToClassName = CollectionFactory.newCaseInsensitiveMap();
    083
    084            /**
    085             * Page class name to logical name (needed to build URLs). This one is case sensitive, since class names do always
    086             * have a particular case.
    087             */
    088            private final Map<String, String> pageClassNameToLogicalName = CollectionFactory.newMap();
    089
    090            /**
    091             * Used to convert a logical page name to the canonical form of the page name; this ensures that uniform case for
    092             * page names is used.
    093             */
    094            private final Map<String, String> pageNameToCanonicalPageName = CollectionFactory.newCaseInsensitiveMap();
    095
    096            private void rebuild(String pathPrefix, String rootPackage)
    097            {
    098                fillNameToClassNameMap(pathPrefix, rootPackage, InternalConstants.PAGES_SUBPACKAGE, pageToClassName);
    099                fillNameToClassNameMap(pathPrefix, rootPackage, InternalConstants.COMPONENTS_SUBPACKAGE, componentToClassName);
    100                fillNameToClassNameMap(pathPrefix, rootPackage, InternalConstants.MIXINS_SUBPACKAGE, mixinToClassName);
    101            }
    102
    103            private void fillNameToClassNameMap(String pathPrefix, String rootPackage, String subPackage,
    104                                                Map<String, String> logicalNameToClassName)
    105            {
    106                String searchPackage = rootPackage + "." + subPackage;
    107                boolean isPage = subPackage.equals(InternalConstants.PAGES_SUBPACKAGE);
    108
    109                Collection<String> classNames = classNameLocator.locateClassNames(searchPackage);
    110
    111                int startPos = searchPackage.length() + 1;
    112
    113                for (String name : classNames)
    114                {
    115                    String logicalName = toLogicalName(name, pathPrefix, startPos, true);
    116                    String unstrippedName = toLogicalName(name, pathPrefix, startPos, false);
    117
    118                    if (isPage)
    119                    {
    120                        int lastSlashx = logicalName.lastIndexOf("/");
    121
    122                        String lastTerm = lastSlashx < 0 ? logicalName : logicalName.substring(lastSlashx + 1);
    123
    124                        if (lastTerm.equalsIgnoreCase("index") || lastTerm.equalsIgnoreCase(startPageName))
    125                        {
    126                            String reducedName = lastSlashx < 0 ? "" : logicalName.substring(0, lastSlashx);
    127
    128                            // Make the super-stripped name another alias to the class.
    129                            // TAP5-1444: Everything else but a start page has precedence
    130
    131                            if (!(lastTerm.equalsIgnoreCase(startPageName) && logicalNameToClassName.containsKey(reducedName)))
    132                            {
    133                                logicalNameToClassName.put(reducedName, name);
    134                                pageNameToCanonicalPageName.put(reducedName, logicalName);
    135                            }
    136                        }
    137
    138                        pageClassNameToLogicalName.put(name, logicalName);
    139                        pageNameToCanonicalPageName.put(logicalName, logicalName);
    140                        pageNameToCanonicalPageName.put(unstrippedName, logicalName);
    141                    }
    142
    143                    logicalNameToClassName.put(logicalName, name);
    144                    logicalNameToClassName.put(unstrippedName, name);
    145                }
    146            }
    147
    148            /**
    149             * Converts a fully qualified class name to a logical name
    150             *
    151             * @param className  fully qualified class name
    152             * @param pathPrefix prefix to be placed on the logical name (to identify the library from in which the class
    153             *                   lives)
    154             * @param startPos   start position within the class name to extract the logical name (i.e., after the final '.' in
    155             *                   "rootpackage.pages.").
    156             * @param stripTerms
    157             * @return a short logical name in folder format ('.' replaced with '/')
    158             */
    159            private String toLogicalName(String className, String pathPrefix, int startPos, boolean stripTerms)
    160            {
    161                List<String> terms = CollectionFactory.newList();
    162
    163                addAll(terms, SPLIT_FOLDER_PATTERN, pathPrefix);
    164
    165                addAll(terms, SPLIT_PACKAGE_PATTERN, className.substring(startPos));
    166
    167                StringBuilder builder = new StringBuilder(LOGICAL_NAME_BUFFER_SIZE);
    168                String sep = "";
    169
    170                String logicalName = terms.remove(terms.size() - 1);
    171
    172                String unstripped = logicalName;
    173
    174                for (String term : terms)
    175                {
    176                    builder.append(sep);
    177                    builder.append(term);
    178
    179                    sep = "/";
    180
    181                    if (stripTerms)
    182                        logicalName = stripTerm(term, logicalName);
    183                }
    184
    185                if (logicalName.equals(""))
    186                    logicalName = unstripped;
    187
    188                builder.append(sep);
    189                builder.append(logicalName);
    190
    191                return builder.toString();
    192            }
    193
    194            private void addAll(List<String> terms, Pattern splitter, String input)
    195            {
    196                for (String term : splitter.split(input))
    197                {
    198                    if (term.equals(""))
    199                        continue;
    200
    201                    terms.add(term);
    202                }
    203            }
    204
    205            private String stripTerm(String term, String logicalName)
    206            {
    207                if (isCaselessPrefix(term, logicalName))
    208                {
    209                    logicalName = logicalName.substring(term.length());
    210                }
    211
    212                if (isCaselessSuffix(term, logicalName))
    213                {
    214                    logicalName = logicalName.substring(0, logicalName.length() - term.length());
    215                }
    216
    217                return logicalName;
    218            }
    219
    220            private boolean isCaselessPrefix(String prefix, String string)
    221            {
    222                return string.regionMatches(true, 0, prefix, 0, prefix.length());
    223            }
    224
    225            private boolean isCaselessSuffix(String suffix, String string)
    226            {
    227                return string.regionMatches(true, string.length() - suffix.length(), suffix, 0, suffix.length());
    228            }
    229        }
    230
    231        private volatile Data data = new Data();
    232
    233        public ComponentClassResolverImpl(Logger logger,
    234
    235                                          ClassNameLocator classNameLocator,
    236
    237                                          @Symbol(SymbolConstants.START_PAGE_NAME)
    238                                          String startPageName,
    239
    240                                          Collection<LibraryMapping> mappings)
    241        {
    242            this.logger = logger;
    243            this.classNameLocator = classNameLocator;
    244
    245            this.startPageName = startPageName;
    246
    247            for (LibraryMapping mapping : mappings)
    248            {
    249                String prefix = mapping.getPathPrefix();
    250
    251                while (prefix.startsWith("/"))
    252                {
    253                    prefix = prefix.substring(1);
    254                }
    255
    256                while (prefix.endsWith("/"))
    257                {
    258                    prefix = prefix.substring(0, prefix.length() - 1);
    259                }
    260
    261                String rootPackage = mapping.getRootPackage();
    262
    263                List<String> packages = this.mappings.get(prefix);
    264
    265                if (packages == null)
    266                {
    267                    packages = CollectionFactory.newList();
    268                    this.mappings.put(prefix, packages);
    269                }
    270
    271                packages.add(rootPackage);
    272
    273                // These packages, which will contain classes subject to class transformation,
    274                // must be registered with the component instantiator (which is responsible
    275                // for transformation).
    276
    277                addSubpackagesToPackageMapping(rootPackage);
    278            }
    279        }
    280
    281        private void addSubpackagesToPackageMapping(String rootPackage)
    282        {
    283            for (String subpackage : InternalConstants.SUBPACKAGES)
    284            {
    285                packageMappings.put(rootPackage + "." + subpackage, ControlledPackageType.COMPONENT);
    286            }
    287        }
    288
    289        public Map<String, ControlledPackageType> getControlledPackageMapping()
    290        {
    291            return Collections.unmodifiableMap(packageMappings);
    292        }
    293
    294        /**
    295         * When the class loader is invalidated, clear any cached page names or component types.
    296         */
    297        public synchronized void objectWasInvalidated()
    298        {
    299            needsRebuild = true;
    300        }
    301
    302        /**
    303         * Invoked from within a withRead() block, checks to see if a rebuild is needed, and then performs the rebuild
    304         * within a withWrite() block.
    305         */
    306        private Data getData()
    307        {
    308            if (!needsRebuild)
    309            {
    310                return data;
    311            }
    312
    313            Data newData = new Data();
    314
    315            for (String prefix : mappings.keySet())
    316            {
    317                List<String> packages = mappings.get(prefix);
    318
    319                String folder = prefix + "/";
    320
    321                for (String packageName : packages)
    322                    newData.rebuild(folder, packageName);
    323            }
    324
    325            showChanges("pages", data.pageToClassName, newData.pageToClassName);
    326            showChanges("components", data.componentToClassName, newData.componentToClassName);
    327            showChanges("mixins", data.mixinToClassName, newData.mixinToClassName);
    328
    329            needsRebuild = false;
    330
    331            data = newData;
    332
    333            return data;
    334        }
    335
    336        private static int countUnique(Map<String, String> map)
    337        {
    338            return CollectionFactory.newSet(map.values()).size();
    339        }
    340
    341        private void showChanges(String title, Map<String, String> savedMap, Map<String, String> newMap)
    342        {
    343            if (savedMap.equals(newMap))
    344                return;
    345
    346            Map<String, String> core = CollectionFactory.newMap();
    347            Map<String, String> nonCore = CollectionFactory.newMap();
    348
    349
    350            int maxLength = 0;
    351
    352            // Pass # 1: Get all the stuff in the core library
    353
    354            for (String name : newMap.keySet())
    355            {
    356                if (name.startsWith(CORE_LIBRARY_PREFIX))
    357                {
    358                    // Strip off the "core/" prefix.
    359
    360                    String key = name.substring(CORE_LIBRARY_PREFIX.length());
    361
    362                    maxLength = Math.max(maxLength, key.length());
    363
    364                    core.put(key, newMap.get(name));
    365                } else
    366                {
    367                    maxLength = Math.max(maxLength, name.length());
    368
    369                    nonCore.put(name, newMap.get(name));
    370                }
    371            }
    372
    373            // Merge the non-core mappings into the core mappings. Where there are conflicts on name, it
    374            // means the application overrode a core page/component/mixin and that's ok ... the
    375            // merged core map will reflect the application's mapping.
    376
    377            core.putAll(nonCore);
    378
    379            StringBuilder builder = new StringBuilder(2000);
    380            Formatter f = new Formatter(builder);
    381
    382            int oldCount = countUnique(savedMap);
    383            int newCount = countUnique(newMap);
    384
    385            f.format("Available %s (%d", title, newCount);
    386
    387            if (oldCount > 0 && oldCount != newCount)
    388            {
    389                f.format(", +%d", newCount - oldCount);
    390            }
    391
    392            builder.append("):\n");
    393
    394            String formatString = "%" + maxLength + "s: %s\n";
    395
    396            List<String> sorted = InternalUtils.sortedKeys(core);
    397
    398            for (String name : sorted)
    399            {
    400                String className = core.get(name);
    401
    402                if (name.equals(""))
    403                    name = "(blank)";
    404
    405                f.format(formatString, name, className);
    406            }
    407
    408            logger.info(builder.toString());
    409        }
    410
    411
    412        public String resolvePageNameToClassName(final String pageName)
    413        {
    414            Data data = getData();
    415
    416            String result = locate(pageName, data.pageToClassName);
    417
    418            if (result == null)
    419            {
    420                throw new UnknownValueException(String.format("Unable to resolve '%s' to a page class name.",
    421                        pageName), new AvailableValues("Page names", presentableNames(data.pageToClassName)));
    422            }
    423
    424            return result;
    425        }
    426
    427        public boolean isPageName(final String pageName)
    428        {
    429            return locate(pageName, getData().pageToClassName) != null;
    430        }
    431
    432        public boolean isPage(final String pageClassName)
    433        {
    434            return locate(pageClassName, getData().pageClassNameToLogicalName) != null;
    435        }
    436
    437        public List<String> getPageNames()
    438        {
    439            Data data = getData();
    440
    441            List<String> result = CollectionFactory.newList(data.pageClassNameToLogicalName.values());
    442
    443            Collections.sort(result);
    444
    445            return result;
    446        }
    447
    448        public String resolveComponentTypeToClassName(final String componentType)
    449        {
    450            Data data = getData();
    451
    452            String result = locate(componentType, data.componentToClassName);
    453
    454            if (result == null)
    455            {
    456                throw new UnknownValueException(String.format("Unable to resolve '%s' to a component class name.",
    457                        componentType), new AvailableValues("Component types",
    458                        presentableNames(data.componentToClassName)));
    459            }
    460
    461            return result;
    462        }
    463
    464        Collection<String> presentableNames(Map<String, ?> map)
    465        {
    466            Set<String> result = CollectionFactory.newSet();
    467
    468            for (String name : map.keySet())
    469            {
    470
    471                if (name.startsWith(CORE_LIBRARY_PREFIX))
    472                {
    473                    result.add(name.substring(CORE_LIBRARY_PREFIX.length()));
    474                    continue;
    475                }
    476
    477                result.add(name);
    478            }
    479
    480            return result;
    481        }
    482
    483        public String resolveMixinTypeToClassName(final String mixinType)
    484        {
    485            Data data = getData();
    486
    487            String result = locate(mixinType, data.mixinToClassName);
    488
    489            if (result == null)
    490            {
    491                throw new UnknownValueException(String.format("Unable to resolve '%s' to a mixin class name.",
    492                        mixinType), new AvailableValues("Mixin types", presentableNames(data.mixinToClassName)));
    493            }
    494
    495            return result;
    496        }
    497
    498        /**
    499         * Locates a class name within the provided map, given its logical name. If not found naturally, a search inside the
    500         * "core" library is included.
    501         *
    502         * @param logicalName            name to search for
    503         * @param logicalNameToClassName mapping from logical name to class name
    504         * @return the located class name or null
    505         */
    506        private String locate(String logicalName, Map<String, String> logicalNameToClassName)
    507        {
    508            String result = logicalNameToClassName.get(logicalName);
    509
    510            // If not found, see if it exists under the core package. In this way,
    511            // anything in core is "inherited" (but overridable) by the application.
    512
    513            if (result != null)
    514            {
    515                return result;
    516            }
    517
    518            return logicalNameToClassName.get(CORE_LIBRARY_PREFIX + logicalName);
    519        }
    520
    521        public String resolvePageClassNameToPageName(final String pageClassName)
    522        {
    523            String result = getData().pageClassNameToLogicalName.get(pageClassName);
    524
    525            if (result == null)
    526            {
    527                throw new IllegalArgumentException(ServicesMessages.pageNameUnresolved(pageClassName));
    528            }
    529
    530            return result;
    531        }
    532
    533        public String canonicalizePageName(final String pageName)
    534        {
    535            Data data = getData();
    536
    537            String result = locate(pageName, data.pageNameToCanonicalPageName);
    538
    539            if (result == null)
    540            {
    541                throw new UnknownValueException(String.format("Unable to resolve '%s' to a known page name.",
    542                        pageName), new AvailableValues("Page names", presentableNames(data.pageNameToCanonicalPageName)));
    543            }
    544
    545            return result;
    546        }
    547
    548        public Map<String, String> getFolderToPackageMapping()
    549        {
    550            Map<String, String> result = CollectionFactory.newCaseInsensitiveMap();
    551
    552            for (String folder : mappings.keySet())
    553            {
    554                List<String> packageNames = mappings.get(folder);
    555
    556                String packageName = findCommonPackageNameForFolder(folder, packageNames);
    557
    558                result.put(folder, packageName);
    559            }
    560
    561            return result;
    562        }
    563
    564        static String findCommonPackageNameForFolder(String folder, List<String> packageNames)
    565        {
    566            String packageName = findCommonPackageName(packageNames);
    567
    568            if (packageName == null)
    569                throw new RuntimeException(
    570                        String.format(
    571                                "Package names for library folder '%s' (%s) can not be reduced to a common base package (of at least one term).",
    572                                folder, InternalUtils.joinSorted(packageNames)));
    573            return packageName;
    574        }
    575
    576        static String findCommonPackageName(List<String> packageNames)
    577        {
    578            // BTW, this is what reduce is for in Clojure ...
    579
    580            String commonPackageName = packageNames.get(0);
    581
    582            for (int i = 1; i < packageNames.size(); i++)
    583            {
    584                commonPackageName = findCommonPackageName(commonPackageName, packageNames.get(i));
    585
    586                if (commonPackageName == null)
    587                    break;
    588            }
    589
    590            return commonPackageName;
    591        }
    592
    593        static String findCommonPackageName(String commonPackageName, String packageName)
    594        {
    595            String[] commonExploded = explode(commonPackageName);
    596            String[] exploded = explode(packageName);
    597
    598            int count = Math.min(commonExploded.length, exploded.length);
    599
    600            int commonLength = 0;
    601            int commonTerms = 0;
    602
    603            for (int i = 0; i < count; i++)
    604            {
    605                if (exploded[i].equals(commonExploded[i]))
    606                {
    607                    // Keep track of the number of shared characters (including the dot seperators)
    608
    609                    commonLength += exploded[i].length() + (i == 0 ? 0 : 1);
    610                    commonTerms++;
    611                } else
    612                {
    613                    break;
    614                }
    615            }
    616
    617            if (commonTerms < 1)
    618                return null;
    619
    620            return commonPackageName.substring(0, commonLength);
    621        }
    622
    623        private static final Pattern DOT = Pattern.compile("\\.");
    624
    625        private static String[] explode(String packageName)
    626        {
    627            return DOT.split(packageName);
    628        }
    629    }