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