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    }