    001// Copyright 2010, 2011, 2012 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
    015package org.apache.tapestry5.internal.services;
    016
    017import java.lang.ref.SoftReference;
    018import java.util.ArrayList;
    019import java.util.Collections;
    020import java.util.Iterator;
    021import java.util.List;
    022import java.util.Map;
    023import java.util.Map.Entry;
    024import java.util.Set;
    025
    026import org.apache.tapestry5.SymbolConstants;
    027import org.apache.tapestry5.commons.services.InvalidationEventHub;
    028import org.apache.tapestry5.commons.util.CollectionFactory;
    029import org.apache.tapestry5.func.F;
    030import org.apache.tapestry5.func.Mapper;
    031import org.apache.tapestry5.internal.services.ComponentDependencyRegistry.DependencyType;
    032import org.apache.tapestry5.internal.services.assets.ResourceChangeTracker;
    033import org.apache.tapestry5.internal.structure.ComponentPageElement;
    034import org.apache.tapestry5.internal.structure.Page;
    035import org.apache.tapestry5.ioc.annotations.ComponentClasses;
    036import org.apache.tapestry5.ioc.annotations.PostInjection;
    037import org.apache.tapestry5.ioc.annotations.Symbol;
    038import org.apache.tapestry5.services.ComponentClassResolver;
    039import org.apache.tapestry5.services.ComponentMessages;
    040import org.apache.tapestry5.services.ComponentTemplates;
    041import org.apache.tapestry5.services.pageload.ComponentRequestSelectorAnalyzer;
    042import org.apache.tapestry5.services.pageload.ComponentResourceSelector;
    043import org.apache.tapestry5.services.pageload.PageCachingReferenceTypeService;
    044import org.apache.tapestry5.services.pageload.PageClassLoaderContext;
    045import org.apache.tapestry5.services.pageload.PageClassLoaderContextManager;
    046import org.apache.tapestry5.services.pageload.ReferenceType;
    047import org.slf4j.Logger;
    048
    049public class PageSourceImpl implements PageSource
    050{
    051    private final ComponentRequestSelectorAnalyzer selectorAnalyzer;
    052
    053    private final PageLoader pageLoader;
    054
    055    private final ComponentDependencyRegistry componentDependencyRegistry;
    056
    057    private final ComponentClassResolver componentClassResolver;
    058
    059    private final PageClassLoaderContextManager pageClassLoaderContextManager;
    060
    061    private final PageCachingReferenceTypeService pageCachingReferenceTypeService;
    062
    063    private final Logger logger;
    064
    065    final private boolean productionMode;
    066
    067    final private boolean multipleClassLoaders;
    068
    069    private static final class CachedPageKey
    070    {
    071        final String pageName;
    072
    073        final ComponentResourceSelector selector;
    074
    075        public CachedPageKey(String pageName, ComponentResourceSelector selector)
    076        {
    077            this.pageName = pageName;
    078            this.selector = selector;
    079        }
    080
    081        public int hashCode()
    082        {
    083            return 37 * pageName.hashCode() + selector.hashCode();
    084        }
    085
    086        public boolean equals(Object obj)
    087        {
    088            if (this == obj)
    089                return true;
    090
    091            if (!(obj instanceof CachedPageKey))
    092                return false;
    093
    094            CachedPageKey other = (CachedPageKey) obj;
    095
    096            return pageName.equals(other.pageName) && selector.equals(other.selector);
    097        }
    098
    099        @Override
    100        public String toString() {
    101            return "CachedPageKey [pageName=" + pageName + ", selector=" + selector + "]";
    102        }
    103
    104
    105    }
    106
    107    private final Map<CachedPageKey, Object> pageCache = CollectionFactory.newConcurrentMap();
    108
    109    public PageSourceImpl(PageLoader pageLoader, ComponentRequestSelectorAnalyzer selectorAnalyzer,
    110            ComponentDependencyRegistry componentDependencyRegistry,
    111            ComponentClassResolver componentClassResolver,
    112            PageClassLoaderContextManager pageClassLoaderContextManager,
    113            PageCachingReferenceTypeService pageCachingReferenceTypeService,
    114            @Symbol(SymbolConstants.PRODUCTION_MODE) boolean productionMode,
    115            @Symbol(SymbolConstants.MULTIPLE_CLASSLOADERS) boolean multipleClassLoaders,
    116            Logger logger)
    117    {
    118        this.pageLoader = pageLoader;
    119        this.selectorAnalyzer = selectorAnalyzer;
    120        this.componentDependencyRegistry = componentDependencyRegistry;
    121        this.componentClassResolver = componentClassResolver;
    122        this.productionMode = productionMode;
    123        this.pageCachingReferenceTypeService = pageCachingReferenceTypeService;
    124        this.multipleClassLoaders = multipleClassLoaders;
    125        this.pageClassLoaderContextManager = pageClassLoaderContextManager;
    126        this.logger = logger;
    127    }
    128
    129    public Page getPage(String canonicalPageName)
    130    {
    131        if (!productionMode)
    132        {
    133            componentDependencyRegistry.disableInvalidations();
    134        }
    135        try
    136        {
    137            return getPage(canonicalPageName, true);
    138        }
    139        finally
    140        {
    141            if (!productionMode)
    142            {
    143                componentDependencyRegistry.enableInvalidations();
    144            }
    145        }
    146    }
    147
    148    public Page getPage(String canonicalPageName, boolean invalidateUnknownContext)
    149    {
    150        ComponentResourceSelector selector = selectorAnalyzer.buildSelectorForRequest();
    151
    152        CachedPageKey key = new CachedPageKey(canonicalPageName, selector);
    153
    154        // The while loop looks superfluous, but it helps to ensure that the Page instance,
    155        // with all of its mutable construction-time state, is properly published to other
    156        // threads (at least, as I understand Brian Goetz's explanation, it should be).
    157
    158        while (true)
    159        {
    160
    161            Page page;
    162            Object object = pageCache.get(key);
    163
    164            page = toPage(object);
    165
    166            if (page != null)
    167            {
    168                return page;
    169            }
    170
    171            final String className = componentClassResolver.resolvePageNameToClassName(canonicalPageName);
    172            if (multipleClassLoaders)
    173            {
    174
    175                // Avoiding problems in PlasticClassPool.createTransformation()
    176                // when the class being loaded has a page superclass
    177                final List<String> pageDependencies = preprocessPageDependencies(className);
    178
    179                for (String pageClassName : pageDependencies)
    180                {
    181                    page = getPage(componentClassResolver.resolvePageClassNameToPageName(pageClassName), false);
    182                }
    183
    184            }
    185
    186            // In rare race conditions, we may see the same page loaded multiple times across
    187            // different threads. The last built one will "evict" the others from the page cache,
    188            // and the earlier ones will be GCed.
    189
    190            page = pageLoader.loadPage(canonicalPageName, selector);
    191
    192            final ReferenceType referenceType = pageCachingReferenceTypeService.get(canonicalPageName);
    193            if (referenceType.equals(ReferenceType.SOFT))
    194            {
    195                pageCache.put(key, new SoftReference<Page>(page));
    196            }
    197            else
    198            {
    199                pageCache.put(key, page);
    200            }
    201
    202            if (!productionMode)
    203            {
    204                final ComponentPageElement rootElement = page.getRootElement();
    205                componentDependencyRegistry.clear(rootElement);
    206                componentDependencyRegistry.register(rootElement);
    207                PageClassLoaderContext context = pageClassLoaderContextManager.get(className);
    208
    209                if (context.isUnknown() && multipleClassLoaders)
    210                {
    211                    this.pageCache.remove(key);
    212                    if (invalidateUnknownContext)
    213                    {
    214                        pageClassLoaderContextManager.invalidateAndFireInvalidationEvents(context);
    215                        preprocessPageDependencies(className);
    216                    }
    217                    context.getClassNames().clear();
    218                    // Avoiding bad invalidations
    219                    return getPage(canonicalPageName, false);
    220                }
    221            }
    222
    223        }
    224
    225
    226    }
    227
    228    private List<String> preprocessPageDependencies(final String className) {
    229        final List<String> pageDependencies = new ArrayList<>();
    230        pageDependencies.addAll(
    231                new ArrayList<String>(componentDependencyRegistry.getDependencies(className, DependencyType.INJECT_PAGE)));
    232        pageDependencies.addAll(
    233                new ArrayList<String>(componentDependencyRegistry.getDependencies(className, DependencyType.SUPERCLASS)));
    234
    235        final Iterator<String> iterator = pageDependencies.iterator();
    236        while (iterator.hasNext())
    237        {
    238            if (!iterator.next().contains(".pages."))
    239            {
    240                iterator.remove();
    241            }
    242        }
    243
    244        preprocessPageClassLoaderContexts(className, pageDependencies);
    245        return pageDependencies;
    246    }
    247
    248    private void preprocessPageClassLoaderContexts(String className, final List<String> pageDependencies) {
    249        for (int i = 0; i < 5; i++)
    250        {
    251            pageClassLoaderContextManager.get(className);
    252            for (String pageClassName : pageDependencies)
    253            {
    254                final PageClassLoaderContext context = pageClassLoaderContextManager.get(pageClassName);
    255                if (i == 1)
    256                {
    257                    try
    258                    {
    259                        context.getClassLoader().loadClass(pageClassName);
    260                    } catch (ClassNotFoundException e) {
    261                        throw new RuntimeException(e);
    262                    }
    263                }
    264            }
    265        }
    266
    267        // TODO: remove
    268//        for (String pageClassName : pageDependencies)
    269//        {
    270//            try
    271//            {
    272//                pageClassLoaderContextManager.get(pageClassName).getClassLoader().loadClass(pageClassName);
    273//            } catch (ClassNotFoundException e)
    274//            {
    275//                throw new RuntimeException(e);
    276//            }
    277//        }
    278    }
    279
    280    @PostInjection
    281    public void setupInvalidation(@ComponentClasses InvalidationEventHub classesHub,
    282                                  @ComponentTemplates InvalidationEventHub templatesHub,
    283                                  @ComponentMessages InvalidationEventHub messagesHub,
    284                                  ResourceChangeTracker resourceChangeTracker)
    285    {
    286        classesHub.addInvalidationCallback(this::listen);
    287        templatesHub.addInvalidationCallback(this::listen);
    288        messagesHub.addInvalidationCallback(this::listen);
    289
    290        // Because Assets can be injected into pages, and Assets are invalidated when
    291        // an Asset's value is changed (partly due to the change, in 5.4, to include the asset's
    292        // checksum as part of the asset URL), then when we notice a change to
    293        // any Resource, it is necessary to discard all page instances.
    294        // From 5.8.3 on, Tapestry tries to only invalidate the components and pages known as
    295        // using the changed resources. If a given resource is changed but not associated with any
    296        // component, then all of them are invalidated.
    297        resourceChangeTracker.addInvalidationCallback(this::listen);
    298    }
    299
    300    private List<String> listen(List<String> resources)
    301    {
    302
    303        if (resources.isEmpty())
    304        {
    305            clearCache();
    306        }
    307        else
    308        {
    309            String pageName;
    310            for (String className : resources)
    311            {
    312                if (componentClassResolver.isPage(className))
    313                {
    314                    pageName = componentClassResolver.resolvePageClassNameToPageName(className);
    315                    final Iterator<Entry<CachedPageKey, Object>> iterator = pageCache.entrySet().iterator();
    316                    while (iterator.hasNext())
    317                    {
    318                        final Entry<CachedPageKey, Object> entry = iterator.next();
    319                        final String entryPageName = entry.getKey().pageName;
    320                        if (entryPageName.equalsIgnoreCase(pageName))
    321                        {
    322                            logger.info("Clearing cached page '{}'", pageName);
    323                            iterator.remove();
    324                        }
    325                    }
    326                }
    327            }
    328        }
    329
    330        return Collections.emptyList();
    331    }
    332
    333    public void clearCache()
    334    {
    335        logger.info("Clearing page cache");
    336        pageCache.clear();
    337    }
    338
    339    public Set<Page> getAllPages()
    340    {
    341        return F.flow(pageCache.values()).map(new Mapper<Object, Page>()
    342        {
    343            public Page map(Object object)
    344            {
    345                return toPage(object);
    346            }
    347        }).removeNulls().toSet();
    348    }
    349
    350    private Page toPage(Object object)
    351    {
    352        Page page;
    353        if (object instanceof SoftReference)
    354        {
    355            @SuppressWarnings("unchecked")
    356            SoftReference<Page> ref = (SoftReference<Page>) object;
    357            page = ref == null ? null : ref.get();
    358        }
    359        else
    360        {
    361            page = (Page) object;
    362        }
    363        return page;
    364    }
    365
    366}