001// Copyright 2011-2013 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.corelib.pages;
016
017import java.util.ArrayList;
018import java.util.Arrays;
019import java.util.Collection;
020import java.util.Collections;
021import java.util.List;
022import java.util.Set;
023
024import org.apache.tapestry5.MarkupWriter;
025import org.apache.tapestry5.SymbolConstants;
026import org.apache.tapestry5.alerts.AlertManager;
027import org.apache.tapestry5.annotations.InjectComponent;
028import org.apache.tapestry5.annotations.Persist;
029import org.apache.tapestry5.annotations.Property;
030import org.apache.tapestry5.annotations.UnknownActivationContextCheck;
031import org.apache.tapestry5.annotations.WhitelistAccessOnly;
032import org.apache.tapestry5.beaneditor.Validate;
033import org.apache.tapestry5.beanmodel.BeanModel;
034import org.apache.tapestry5.beanmodel.services.BeanModelSource;
035import org.apache.tapestry5.commons.Messages;
036import org.apache.tapestry5.commons.services.InvalidationEventHub;
037import org.apache.tapestry5.commons.util.CollectionFactory;
038import org.apache.tapestry5.corelib.components.Zone;
039import org.apache.tapestry5.dom.Element;
040import org.apache.tapestry5.func.F;
041import org.apache.tapestry5.func.Flow;
042import org.apache.tapestry5.func.Mapper;
043import org.apache.tapestry5.func.Predicate;
044import org.apache.tapestry5.func.Reducer;
045import org.apache.tapestry5.http.TapestryHttpSymbolConstants;
046import org.apache.tapestry5.http.services.Request;
047import org.apache.tapestry5.internal.PageCatalogTotals;
048import org.apache.tapestry5.internal.services.ComponentDependencyGraphvizGenerator;
049import org.apache.tapestry5.internal.services.ComponentDependencyRegistry;
050import org.apache.tapestry5.internal.services.ComponentDependencyRegistry.DependencyType;
051import org.apache.tapestry5.internal.services.PageSource;
052import org.apache.tapestry5.internal.services.ReloadHelper;
053import org.apache.tapestry5.internal.structure.ComponentPageElement;
054import org.apache.tapestry5.internal.structure.Page;
055import org.apache.tapestry5.ioc.OperationTracker;
056import org.apache.tapestry5.ioc.annotations.ComponentClasses;
057import org.apache.tapestry5.ioc.annotations.Inject;
058import org.apache.tapestry5.ioc.annotations.Symbol;
059import org.apache.tapestry5.ioc.internal.util.InternalUtils;
060import org.apache.tapestry5.runtime.Component;
061import org.apache.tapestry5.services.ComponentClassResolver;
062import org.apache.tapestry5.services.ajax.AjaxResponseRenderer;
063import org.apache.tapestry5.services.javascript.JavaScriptSupport;
064import org.apache.tapestry5.services.pageload.ComponentResourceSelector;
065import org.apache.tapestry5.services.pageload.PageClassLoaderContextManager;
066
067/**
068 * Lists out the currently loaded pages, using a {@link org.apache.tapestry5.corelib.components.Grid}.
069 * Provides an option to force all pages to be loaded. In development mode, includes an option to clear the page cache.
070 */
071@UnknownActivationContextCheck(false)
072@WhitelistAccessOnly
073public class PageCatalog
074{
075
076    @Property
077    private PageCatalogTotals totals;
078
079    @Property
080    @Inject
081    @Symbol(TapestryHttpSymbolConstants.PRODUCTION_MODE)
082    private boolean productionMode;
083    
084    @Property
085    @Inject
086    @Symbol(SymbolConstants.MULTIPLE_CLASSLOADERS)
087    private boolean multipleClassLoaders;
088
089    @Inject
090    private PageSource pageSource;
091
092    @Inject
093    private ComponentResourceSelector selector;
094
095    @Inject
096    private ComponentClassResolver resolver;
097    
098    @Inject
099    private ComponentDependencyRegistry componentDependencyRegistry;
100
101    @Inject
102    private AlertManager alertManager;
103
104    @Property
105    private Page page;
106
107    @Property
108    private Page selectedPage;
109
110    @Property
111    private String dependency;
112
113    @InjectComponent
114    private Zone pagesZone;
115    
116    @InjectComponent
117    private Zone pageStructureZone;
118
119    @Persist
120    private Set<String> failures;
121
122    @Property
123    @Validate("required")
124    @Persist
125    private String pageName;
126
127    @Inject
128    private OperationTracker operationTracker;
129
130    @Inject
131    private ReloadHelper reloadHelper;
132
133    @Inject
134    private BeanModelSource beanModelSource;
135    
136    @Inject
137    private Messages messages;
138
139    @Property
140    public static BeanModel<Page> model;
141
142    @Inject 
143    private Request request;
144    
145    @Inject
146    @ComponentClasses 
147    private InvalidationEventHub classesInvalidationEventHub;
148    
149    @Inject
150    private JavaScriptSupport javaScriptSupport;
151    
152    @Inject
153    private ComponentDependencyGraphvizGenerator componentDependencyGraphvizGenerator;
154
155    @Inject
156    private ComponentClassResolver componentClassResolver;
157
158    @Inject
159    private AjaxResponseRenderer ajaxResponseRenderer;
160    
161    @Inject
162    private PageClassLoaderContextManager pageClassLoaderContextManager;
163    
164    void pageLoaded()
165    {
166        model = beanModelSource.createDisplayModel(Page.class, messages);
167
168        model.addExpression("selector", "selector.toString()");
169        model.addExpression("assemblyTime", "stats.assemblyTime");
170        model.addExpression("componentCount", "stats.componentCount");
171        model.addExpression("weight", "stats.weight");
172        model.add("clear", null);
173
174        model.reorder("name", "selector", "assemblyTime", "componentCount", "weight");
175    }
176
177    public void onRecomputeTotals()
178    {
179        totals = new PageCatalogTotals();
180
181        Flow<Page> pages = F.flow(getPages());
182
183        totals.loadedPages = pages.count();
184        totals.definedPages = getPageNames().size();
185        totals.uniquePageNames = pages.map(new Mapper<Page, String>()
186        {
187            public String map(Page element)
188            {
189                return element.getName();
190            }
191        }).toSet().size();
192
193        totals.components = pages.reduce(new Reducer<Integer, Page>()
194        {
195            public Integer reduce(Integer accumulator, Page element)
196            {
197                return accumulator + element.getStats().componentCount;
198            }
199        }, 0);
200
201        Set<String> selectorIds = pages.map(new Mapper<Page, String>()
202        {
203            public String map(Page element)
204            {
205                return element.getSelector().toShortString();
206            }
207        }).toSet();
208
209        totals.selectors = InternalUtils.joinSorted(selectorIds);
210    }
211
212    public List<String> getPageNames()
213    {
214        return resolver.getPageNames();
215    }
216
217    public Collection<Page> getPages()
218    {
219        return pageSource.getAllPages();
220    }
221    
222    void onActionFromPreloadPageClassLoaderContexts()
223    {
224        pageClassLoaderContextManager.preload();
225    }
226    
227    Object onClearPage(String className)
228    {
229        final String logicalName = resolver.getLogicalName(className);
230        classesInvalidationEventHub.fireInvalidationEvent(Arrays.asList(className));
231        alertManager.warn(String.format("Page %s (%s) has been cleared from the page cache",
232                className, logicalName));
233        return pagesZone.getBody();
234    }
235    
236    Object onSuccessFromSinglePageLoad()
237    {
238        boolean found = !F.flow(getPages()).filter(new Predicate<Page>()
239        {
240            public boolean accept(Page element)
241            {
242                return element.getName().equals(pageName) && element.getSelector().equals(selector);
243            }
244        }).isEmpty();
245
246        if (found)
247        {
248            alertManager.warn(String.format("Page %s has already been loaded for '%s'.",
249                    pageName, selector.toShortString()));
250            return null;
251        }
252
253        long startTime = System.currentTimeMillis();
254
255
256        // Load the page now (may cause an exception).
257
258        pageSource.getPage(pageName);
259
260
261        alertManager.info(String.format("Loaded page %s for selector '%s' (in %,d ms).", pageName,
262                selector.toShortString(), System.currentTimeMillis() - startTime));
263
264        return pagesZone.getBody();
265    }
266
267    private class PageLoadData
268    {
269        int loadedCount;
270        RuntimeException fail;
271        boolean someFail;
272    }
273
274    Object onActionFromForceLoad()
275    {
276        if (failures == null)
277        {
278            failures = CollectionFactory.newSet();
279        }
280
281        long startTime = System.currentTimeMillis();
282
283        final Collection<Page> initialPages = getPages();
284
285        final PageLoadData data = new PageLoadData();
286
287        for (final String name : resolver.getPageNames())
288        {
289            if (failures.contains(name))
290            {
291                alertManager.warn(String.format("Skipping page %s due to prior load failure.", name));
292                data.someFail = true;
293                continue;
294            }
295
296            operationTracker.run("Loading page " + name, new Runnable()
297            {
298                public void run()
299                {
300                    try
301                    {
302                        Page newPage = pageSource.getPage(name);
303
304                        if (!initialPages.contains(newPage))
305                        {
306                            data.loadedCount++;
307                        }
308                    } catch (RuntimeException ex)
309                    {
310                        alertManager.error(String.format("Page %s failed to load.", name));
311                        failures.add(name);
312
313                        if (data.fail == null)
314                        {
315                            pageName = name;
316                            data.fail = ex;
317                        }
318                    }
319                }
320            });
321
322            if (data.fail != null)
323            {
324                break;
325            }
326        }
327
328        alertManager.info(String.format("Loaded %,d new pages for selector '%s' (in %,d ms).", data.loadedCount,
329                selector.toShortString(), System.currentTimeMillis() - startTime));
330
331        if (data.someFail)
332        {
333            alertManager.warn("Clear the cache to reset the list of failed pages.");
334        }
335
336        if (data.fail != null)
337        {
338            throw data.fail;
339        }
340
341        return pagesZone.getBody();
342    }
343
344    Object onActionFromClearCaches()
345    {
346        reloadHelper.forceReload();
347
348        failures = null;
349
350        return pagesZone.getBody();
351    }
352    
353    Object onActionFromStoreDependencyInformation()
354    {
355        
356        componentDependencyRegistry.writeFile();
357        
358        alertManager.warn(String.format(
359                "Component dependency information written to %s.", 
360                ComponentDependencyRegistry.FILENAME));
361        
362        return pagesZone.getBody();
363        
364    }
365
366    Object onActionFromRunGC()
367    {
368        Runtime runtime = Runtime.getRuntime();
369
370        long initialFreeMemory = runtime.freeMemory();
371
372        runtime.gc();
373
374        long delta = runtime.freeMemory() - initialFreeMemory;
375
376        alertManager.info(String.format("Garbage collection freed %,.2f Kb of memory.",
377                ((double) delta) / 1024.0d));
378
379        return pagesZone.getBody();
380    }
381
382    public String formatElapsed(double millis)
383    {
384        return String.format("%,.3f ms", millis);
385    }
386    
387    public List<String> getDependencies() 
388    {
389        final String selectedPageClassName = getSelectedPageClassName();
390        List<String> dependencies = new ArrayList<>();
391        dependencies.addAll(
392                componentDependencyRegistry.getDependencies(selectedPageClassName, DependencyType.USAGE));
393        dependencies.addAll(
394                componentDependencyRegistry.getDependencies(selectedPageClassName, DependencyType.INJECT_PAGE));
395        dependencies.addAll(
396                componentDependencyRegistry.getDependencies(selectedPageClassName, DependencyType.SUPERCLASS));
397        Collections.sort(dependencies);
398        return dependencies;
399    }
400    
401    public void onPageStructure(String pageName)
402    {
403        selectedPage = pageSource.getPage(pageName);
404        ajaxResponseRenderer.addRender("pageStructureZone", pageStructureZone.getBody());
405    }
406    
407    public String getDisplayLogicalName() 
408    {
409        return getDisplayLogicalName(dependency);
410    }
411
412    public String getPageClassName() 
413    {
414        return getClassName(page);
415    }
416
417    public String getSelectedPageClassName() 
418    {
419        return getClassName(selectedPage);
420    }
421    
422    private String getClassName(Page page) 
423    {
424        return page.getRootComponent().getComponentResources().getComponentModel().getComponentClassName();
425    }
426
427    private String getClassName(Component component) 
428    {
429        return component.getComponentResources().getComponentModel().getComponentClassName();
430    }
431
432    public void onComponentTree(MarkupWriter writer) 
433    {
434        render(selectedPage.getRootElement(), writer);
435    }
436    
437    private void render(ComponentPageElement componentPageElement, MarkupWriter writer) 
438    {
439        final Element li = writer.element("li");
440        final String className = getClassName(componentPageElement.getComponent());
441        final Set<String> embeddedElementIds = componentPageElement.getEmbeddedElementIds();
442        
443        if (componentPageElement.getComponent().getComponentResources().getComponentModel().isPage()) 
444        {
445            li.text(componentPageElement.getPageName());
446        }
447        else {
448            li.text(String.format("%s (%s)", getDisplayLogicalName(className), componentPageElement.getId()));
449        }
450        
451        if (!embeddedElementIds.isEmpty())
452        {
453            writer.element("ul");
454            for (String id : embeddedElementIds)
455            {
456                render(componentPageElement.getEmbeddedElement(id), writer);
457            }
458            writer.end();
459        }
460        
461        writer.end();
462    }
463
464    private String getDisplayLogicalName(final String className) 
465    {
466        final String logicalName = resolver.getLogicalName(className);
467        String displayName = logicalName;
468        if (logicalName == null || logicalName.trim().length() == 0)
469        {
470            if (className.contains(".base."))
471            {
472                displayName = "(base class)";
473            }
474            if (className.contains(".mixins."))
475            {
476                displayName = "(mixin)";
477            }
478        }
479        return displayName;
480    }
481
482    public String getLogicalName(String className) 
483    {
484        return resolver.getLogicalName(className);
485    }
486    
487    public String getGraphvizValue()
488    {
489        return componentDependencyGraphvizGenerator.generate(getClassName(selectedPage));
490    }
491    
492}