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}