All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.tapestry5.internal.services.ComponentClassResolverImpl Maven / Gradle / Ivy

Go to download

Central module for Tapestry, containing interfaces to the Java Servlet API and all core services and components.

There is a newer version: 5.8.6
Show newest version
// Copyright 2006, 2007, 2008, 2009, 2010 The Apache Software Foundation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package org.apache.tapestry5.internal.services;

import java.util.Collection;
import java.util.Collections;
import java.util.Formatter;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;

import org.apache.tapestry5.SymbolConstants;
import org.apache.tapestry5.internal.InternalConstants;
import org.apache.tapestry5.ioc.Invokable;
import org.apache.tapestry5.ioc.annotations.Symbol;
import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
import org.apache.tapestry5.ioc.internal.util.ConcurrentBarrier;
import org.apache.tapestry5.ioc.internal.util.InternalUtils;
import org.apache.tapestry5.ioc.services.ClassNameLocator;
import org.apache.tapestry5.ioc.util.AvailableValues;
import org.apache.tapestry5.ioc.util.UnknownValueException;
import org.apache.tapestry5.services.ComponentClassResolver;
import org.apache.tapestry5.services.InvalidationListener;
import org.apache.tapestry5.services.LibraryMapping;
import org.slf4j.Logger;

public class ComponentClassResolverImpl implements ComponentClassResolver, InvalidationListener
{
    private static final String CORE_LIBRARY_PREFIX = "core/";

    private final Logger logger;

    private final ComponentInstantiatorSource componentInstantiatorSource;

    private final ClassNameLocator classNameLocator;

    private final String appRootPackage;

    private final String startPageName;

    // Map from folder name to a list of root package names.
    // The key does not begin or end with a slash.

    private final Map> mappings = CollectionFactory.newCaseInsensitiveMap();

    // Flag indicating that the maps have been cleared following an invalidation
    // and need to be rebuilt. The flag and the four maps below are not synchronized
    // because they are only modified inside a synchronized block. That should be strong enough ...
    // and changes made will become "visible" at the end of the synchronized block. Because of the
    // structure of Tapestry, there should not be any reader threads while the write thread
    // is operating.

    private boolean needsRebuild = true;

    /**
     * Logical page name to class name.
     */
    private final Map pageToClassName = CollectionFactory.newCaseInsensitiveMap();

    /**
     * Component type to class name.
     */
    private final Map componentToClassName = CollectionFactory.newCaseInsensitiveMap();

    /**
     * Mixing type to class name.
     */
    private final Map mixinToClassName = CollectionFactory.newCaseInsensitiveMap();

    /**
     * Page class name to logical name (needed to build URLs). This one is case sensitive, since class names do always
     * have a particular case.
     */
    private final Map pageClassNameToLogicalName = CollectionFactory.newMap();

    /**
     * Used to convert a logical page name to the canonical form of the page name; this ensures that uniform case for
     * page names is used.
     */
    private final Map pageNameToCanonicalPageName = CollectionFactory.newCaseInsensitiveMap();

    private final ConcurrentBarrier barrier = new ConcurrentBarrier();

    private static final Pattern SPLIT_PACKAGE_PATTERN = Pattern.compile("\\.");

    private static final Pattern SPLIT_FOLDER_PATTERN = Pattern.compile("/");

    private static final int LOGICAL_NAME_BUFFER_SIZE = 40;

    public ComponentClassResolverImpl(Logger logger,

    ComponentInstantiatorSource componentInstantiatorSource,

    ClassNameLocator classNameLocator,

    @Symbol(InternalConstants.TAPESTRY_APP_PACKAGE_PARAM)
    String appRootPackage,

    @Symbol(SymbolConstants.START_PAGE_NAME)
    String startPageName,

    Collection mappings)
    {
        this.logger = logger;
        this.componentInstantiatorSource = componentInstantiatorSource;
        this.classNameLocator = classNameLocator;

        this.appRootPackage = appRootPackage;
        this.startPageName = startPageName;

        addPackagesToInstantiatorSource(this.appRootPackage);

        for (LibraryMapping mapping : mappings)
        {
            String prefix = mapping.getPathPrefix();

            while (prefix.startsWith("/"))
            {
                prefix = prefix.substring(1);
            }

            while (prefix.endsWith("/"))
            {
                prefix = prefix.substring(0, prefix.length() - 1);
            }

            String rootPackage = mapping.getRootPackage();

            List packages = this.mappings.get(prefix);

            if (packages == null)
            {
                packages = CollectionFactory.newList();
                this.mappings.put(prefix, packages);
            }

            packages.add(rootPackage);

            // These packages, which will contain classes subject to class transformation,
            // must be registered with the component instantiator (which is responsible
            // for transformation).

            addPackagesToInstantiatorSource(rootPackage);
        }
    }

    private void addPackagesToInstantiatorSource(String rootPackage)
    {
        componentInstantiatorSource.addPackage(rootPackage + "." + InternalConstants.PAGES_SUBPACKAGE);
        componentInstantiatorSource.addPackage(rootPackage + "." + InternalConstants.COMPONENTS_SUBPACKAGE);
        componentInstantiatorSource.addPackage(rootPackage + "." + InternalConstants.MIXINS_SUBPACKAGE);
        componentInstantiatorSource.addPackage(rootPackage + "." + InternalConstants.BASE_SUBPACKAGE);
    }

    /**
     * When the class loader is invalidated, clear any cached page names or component types.
     */
    public synchronized void objectWasInvalidated()
    {
        barrier.withWrite(new Runnable()
        {
            public void run()
            {
                needsRebuild = true;
            }
        });
    }

    /**
     * Invoked from within a withRead() block, checks to see if a rebuild is needed, and then performs the rebuild
     * within a withWrite() block.
     */
    private void rebuild()
    {
        if (!needsRebuild)
            return;

        barrier.withWrite(new Runnable()
        {
            public void run()
            {
                performRebuild();
            }
        });
    }

    private void performRebuild()
    {

        Map savedPages = CollectionFactory.newMap(pageToClassName);
        Map savedComponents = CollectionFactory.newMap(componentToClassName);
        Map savedMixins = CollectionFactory.newMap(mixinToClassName);

        pageToClassName.clear();
        componentToClassName.clear();
        mixinToClassName.clear();
        pageClassNameToLogicalName.clear();
        pageNameToCanonicalPageName.clear();

        rebuild("", appRootPackage);

        for (String prefix : mappings.keySet())
        {
            List packages = mappings.get(prefix);

            String folder = prefix + "/";

            for (String packageName : packages)
                rebuild(folder, packageName);
        }

        showChanges("pages", savedPages, pageToClassName);
        showChanges("components", savedComponents, componentToClassName);
        showChanges("mixins", savedMixins, mixinToClassName);

        needsRebuild = false;
    }

    private void showChanges(String title, Map savedMap, Map newMap)
    {
        if (savedMap.equals(newMap))
            return;

        Map core = CollectionFactory.newMap();
        Map nonCore = CollectionFactory.newMap();

        int maxLength = 0;

        // Pass # 1: Get all the stuff in the core library

        for (String name : newMap.keySet())
        {
            if (name.startsWith(CORE_LIBRARY_PREFIX))
            {
                // Strip off the "core/" prefix.

                String key = name.substring(CORE_LIBRARY_PREFIX.length());

                maxLength = Math.max(maxLength, key.length());

                core.put(key, newMap.get(name));
            }
            else
            {
                maxLength = Math.max(maxLength, name.length());

                nonCore.put(name, newMap.get(name));
            }
        }

        // Merge the non-core mappings into the core mappings. Where there are conflicts on name, it
        // means the application overrode a core page/component/mixin and that's ok ... the
        // merged core map will reflect the application's mapping.

        core.putAll(nonCore);

        StringBuilder builder = new StringBuilder(2000);
        Formatter f = new Formatter(builder);

        f.format("Available %s:\n", title);

        String formatString = "%" + maxLength + "s: %s\n";

        List sorted = InternalUtils.sortedKeys(core);

        for (String name : sorted)
        {
            String className = core.get(name);

            if (name.equals(""))
                name = "(blank)";

            f.format(formatString, name, className);
        }

        logger.info(builder.toString());
    }

    private void rebuild(String pathPrefix, String rootPackage)
    {
        fillNameToClassNameMap(pathPrefix, rootPackage, InternalConstants.PAGES_SUBPACKAGE, pageToClassName);
        fillNameToClassNameMap(pathPrefix, rootPackage, InternalConstants.COMPONENTS_SUBPACKAGE, componentToClassName);
        fillNameToClassNameMap(pathPrefix, rootPackage, InternalConstants.MIXINS_SUBPACKAGE, mixinToClassName);
    }

    private void fillNameToClassNameMap(String pathPrefix, String rootPackage, String subPackage,
            Map logicalNameToClassName)
    {
        String searchPackage = rootPackage + "." + subPackage;
        boolean isPage = subPackage.equals(InternalConstants.PAGES_SUBPACKAGE);

        Collection classNames = classNameLocator.locateClassNames(searchPackage);

        int startPos = searchPackage.length() + 1;

        for (String name : classNames)
        {
            String logicalName = toLogicalName(name, pathPrefix, startPos, true);
            String unstrippedName = toLogicalName(name, pathPrefix, startPos, false);

            if (isPage)
            {
                int lastSlashx = logicalName.lastIndexOf("/");

                String lastTerm = lastSlashx < 0 ? logicalName : logicalName.substring(lastSlashx + 1);

                if (lastTerm.equalsIgnoreCase("index") || lastTerm.equalsIgnoreCase(startPageName))
                {
                    String reducedName = lastSlashx < 0 ? "" : logicalName.substring(0, lastSlashx);

                    // Make the super-stripped name another alias to the class.

                    logicalNameToClassName.put(reducedName, name);
                    pageNameToCanonicalPageName.put(reducedName, logicalName);
                }

                pageClassNameToLogicalName.put(name, logicalName);
                pageNameToCanonicalPageName.put(logicalName, logicalName);
                pageNameToCanonicalPageName.put(unstrippedName, logicalName);
            }

            logicalNameToClassName.put(logicalName, name);
            logicalNameToClassName.put(unstrippedName, name);
        }
    }

    /**
     * Converts a fully qualified class name to a logical name
     * 
     * @param className
     *            fully qualified class name
     * @param pathPrefix
     *            prefix to be placed on the logical name (to identify the library from in which the class
     *            lives)
     * @param startPos
     *            start position within the class name to extract the logical name (i.e., after the final '.' in
     *            "rootpackage.pages.").
     * @param stripTerms
     * @return a short logical name in folder format ('.' replaced with '/')
     */
    private String toLogicalName(String className, String pathPrefix, int startPos, boolean stripTerms)
    {
        List terms = CollectionFactory.newList();

        addAll(terms, SPLIT_FOLDER_PATTERN, pathPrefix);

        addAll(terms, SPLIT_PACKAGE_PATTERN, className.substring(startPos));

        StringBuilder builder = new StringBuilder(LOGICAL_NAME_BUFFER_SIZE);
        String sep = "";

        String logicalName = terms.remove(terms.size() - 1);

        String unstripped = logicalName;

        for (String term : terms)
        {
            builder.append(sep);
            builder.append(term);

            sep = "/";

            if (stripTerms)
                logicalName = stripTerm(term, logicalName);
        }

        if (logicalName.equals(""))
            logicalName = unstripped;

        builder.append(sep);
        builder.append(logicalName);

        return builder.toString();
    }

    private void addAll(List terms, Pattern splitter, String input)
    {
        for (String term : splitter.split(input))
        {
            if (term.equals(""))
                continue;

            terms.add(term);
        }
    }

    private String stripTerm(String term, String logicalName)
    {
        if (isCaselessPrefix(term, logicalName))
        {
            logicalName = logicalName.substring(term.length());
        }

        if (isCaselessSuffix(term, logicalName))
        {
            logicalName = logicalName.substring(0, logicalName.length() - term.length());
        }

        return logicalName;
    }

    private boolean isCaselessPrefix(String prefix, String string)
    {
        return string.regionMatches(true, 0, prefix, 0, prefix.length());
    }

    private boolean isCaselessSuffix(String suffix, String string)
    {
        return string.regionMatches(true, string.length() - suffix.length(), suffix, 0, suffix.length());
    }

    public String resolvePageNameToClassName(final String pageName)
    {
        return barrier.withRead(new Invokable()
        {
            public String invoke()
            {
                String result = locate(pageName, pageToClassName);

                if (result == null)
                    throw new UnknownValueException(String.format("Unable to resolve '%s' to a page class name.",
                            pageName), new AvailableValues("Page names", presentableNames(pageToClassName)));

                return result;
            }
        });
    }

    public boolean isPageName(final String pageName)
    {
        return barrier.withRead(new Invokable()
        {
            public Boolean invoke()
            {
                return locate(pageName, pageToClassName) != null;
            }
        });
    }

    public List getPageNames()
    {
        return barrier.withRead(new Invokable>()
        {
            public List invoke()
            {
                rebuild();

                List result = CollectionFactory.newList(pageClassNameToLogicalName.values());

                Collections.sort(result);

                return result;
            }
        });
    }

    public String resolveComponentTypeToClassName(final String componentType)
    {
        return barrier.withRead(new Invokable()
        {
            public String invoke()
            {
                String result = locate(componentType, componentToClassName);

                if (result == null)
                    throw new UnknownValueException(String.format("Unable to resolve '%s' to a component class name.",
                            componentType), new AvailableValues("Component types",
                            presentableNames(componentToClassName)));

                return result;
            }
        });
    }

    Collection presentableNames(Map map)
    {
        Set result = CollectionFactory.newSet();

        for (String name : map.keySet())
        {

            if (name.startsWith(CORE_LIBRARY_PREFIX))
            {
                result.add(name.substring(CORE_LIBRARY_PREFIX.length()));
                continue;
            }

            result.add(name);
        }

        return result;
    }

    public String resolveMixinTypeToClassName(final String mixinType)
    {
        return barrier.withRead(new Invokable()
        {
            public String invoke()
            {
                String result = locate(mixinType, mixinToClassName);

                if (result == null)
                    throw new UnknownValueException(String.format("Unable to resolve '%s' to a mixin class name.",
                            mixinType), new AvailableValues("Mixin types", presentableNames(mixinToClassName)));

                return result;
            }
        });
    }

    /**
     * Locates a class name within the provided map, given its logical name. If not found naturally, a search inside the
     * "core" library is included.
     * 
     * @param logicalName
     *            name to search for
     * @param logicalNameToClassName
     *            mapping from logical name to class name
     * @return the located class name or null
     */
    private String locate(String logicalName, Map logicalNameToClassName)
    {
        rebuild();

        String result = logicalNameToClassName.get(logicalName);

        // If not found, see if it exists under the core package. In this way,
        // anything in core is "inherited" (but overridable) by the application.

        if (result == null)
            result = logicalNameToClassName.get(CORE_LIBRARY_PREFIX + logicalName);

        return result;
    }

    public String resolvePageClassNameToPageName(final String pageClassName)
    {
        return barrier.withRead(new Invokable()
        {
            public String invoke()
            {
                rebuild();

                String result = pageClassNameToLogicalName.get(pageClassName);

                if (result == null)
                    throw new IllegalArgumentException(ServicesMessages.pageNameUnresolved(pageClassName));

                return result;
            }
        });
    }

    public String canonicalizePageName(final String pageName)
    {
        return barrier.withRead(new Invokable()
        {
            public String invoke()
            {
                String result = locate(pageName, pageNameToCanonicalPageName);

                if (result == null)
                    throw new UnknownValueException(String.format("Unable to resolve '%s' to a known page name.",
                            pageName), new AvailableValues("Page names", presentableNames(pageNameToCanonicalPageName)));

                return result;
            }
        });
    }

    public Map getFolderToPackageMapping()
    {
        Map result = CollectionFactory.newCaseInsensitiveMap();

        for (String folder : mappings.keySet())
        {
            List packageNames = mappings.get(folder);

            String packageName = findCommonPackageNameForFolder(folder, packageNames);

            result.put(folder, packageName);
        }

        return result;
    }

    static String findCommonPackageNameForFolder(String folder, List packageNames)
    {
        String packageName = findCommonPackageName(packageNames);

        if (packageName == null)
            throw new RuntimeException(
                    String
                            .format(
                                    "Package names for library folder '%s' (%s) can not be reduced to a common base package (of at least two terms).",
                                    folder, InternalUtils.joinSorted(packageNames)));
        return packageName;
    }

    static String findCommonPackageName(List packageNames)
    {
        // BTW, this is what reduce is for in Clojure ...

        String commonPackageName = packageNames.get(0);

        for (int i = 1; i < packageNames.size(); i++)
        {
            commonPackageName = findCommonPackageName(commonPackageName, packageNames.get(i));

            if (commonPackageName == null)
                break;
        }

        return commonPackageName;
    }

    static String findCommonPackageName(String commonPackageName, String packageName)
    {
        String[] commonExploded = explode(commonPackageName);
        String[] exploded = explode(packageName);

        int count = Math.min(commonExploded.length, exploded.length);

        int commonLength = 0;
        int commonTerms = 0;

        for (int i = 0; i < count; i++)
        {
            if (exploded[i].equals(commonExploded[i]))
            {
                // Keep track of the number of shared characters (including the dot seperators)

                commonLength += exploded[i].length() + (i == 0 ? 0 : 1);
                commonTerms++;
            }
            else
            {
                break;
            }
        }

        if (commonTerms < 2)
            return null;

        return commonPackageName.substring(0, commonLength);
    }

    private static final Pattern DOT = Pattern.compile("\\.");

    private static String[] explode(String packageName)
    {
        return DOT.split(packageName);
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy