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    }
231    
232    Object onClearPage(String className)
233    {
234        final String logicalName = resolver.getLogicalName(className);
235        classesInvalidationEventHub.fireInvalidationEvent(Arrays.asList(className));
236        alertManager.warn(String.format("Page %s (%s) has been cleared from the page cache",
237                className, logicalName));
238        return pagesZone.getBody();
239    }
240    
241    Object onSuccessFromSinglePageLoad()
242    {
243        boolean found = !F.flow(getPages()).filter(new Predicate<Page>()
244        {
245            public boolean accept(Page element)
246            {
247                return element.getName().equals(pageName) && element.getSelector().equals(selector);
248            }
249        }).isEmpty();
250
251        if (found)
252        {
253            alertManager.warn(String.format("Page %s has already been loaded for '%s'.",
254                    pageName, selector.toShortString()));
255            return null;
256        }
257
258        long startTime = System.currentTimeMillis();
259
260
261        // Load the page now (may cause an exception).
262
263        pageSource.getPage(pageName);
264
265
266        alertManager.info(String.format("Loaded page %s for selector '%s' (in %,d ms).", pageName,
267                selector.toShortString(), System.currentTimeMillis() - startTime));
268
269        return pagesZone.getBody();
270    }
271
272    private class PageLoadData
273    {
274        int loadedCount;
275        RuntimeException fail;
276        boolean someFail;
277    }
278
279    Object onActionFromForceLoad()
280    {
281        if (failures == null)
282        {
283            failures = CollectionFactory.newSet();
284        }
285
286        long startTime = System.currentTimeMillis();
287
288        final Collection<Page> initialPages = getPages();
289
290        final PageLoadData data = new PageLoadData();
291
292        for (final String name : resolver.getPageNames())
293        {
294            if (failures.contains(name))
295            {
296                alertManager.warn(String.format("Skipping page %s due to prior load failure.", name));
297                data.someFail = true;
298                continue;
299            }
300
301            operationTracker.run("Loading page " + name, new Runnable()
302            {
303                public void run()
304                {
305                    try
306                    {
307                        Page newPage = pageSource.getPage(name);
308
309                        if (!initialPages.contains(newPage))
310                        {
311                            data.loadedCount++;
312                        }
313                    } catch (RuntimeException ex)
314                    {
315                        alertManager.error(String.format("Page %s failed to load.", name));
316                        failures.add(name);
317
318                        if (data.fail == null)
319                        {
320                            pageName = name;
321                            data.fail = ex;
322                        }
323                    }
324                }
325            });
326
327            if (data.fail != null)
328            {
329                break;
330            }
331        }
332
333        alertManager.info(String.format("Loaded %,d new pages for selector '%s' (in %,d ms).", data.loadedCount,
334                selector.toShortString(), System.currentTimeMillis() - startTime));
335
336        if (data.someFail)
337        {
338            alertManager.warn("Clear the cache to reset the list of failed pages.");
339        }
340
341        if (data.fail != null)
342        {
343            throw data.fail;
344        }
345
346        return pagesZone.getBody();
347    }
348
349    Object onActionFromClearCaches()
350    {
351        reloadHelper.forceReload();
352
353        failures = null;
354
355        return pagesZone.getBody();
356    }
357    
358    Object onActionFromStoreDependencyInformation()
359    {
360        
361        componentDependencyRegistry.writeFile();
362        
363        alertManager.warn(String.format(
364                "Component dependency information written to %s.", 
365                new File(componentDependencyFile).getAbsolutePath()));
366        
367        return pagesZone.getBody();
368        
369    }
370
371    Object onActionFromRunGC()
372    {
373        Runtime runtime = Runtime.getRuntime();
374
375        long initialFreeMemory = runtime.freeMemory();
376
377        runtime.gc();
378
379        long delta = runtime.freeMemory() - initialFreeMemory;
380
381        alertManager.info(String.format("Garbage collection freed %,.2f Kb of memory.",
382                ((double) delta) / 1024.0d));
383
384        return pagesZone.getBody();
385    }
386
387    public String formatElapsed(double millis)
388    {
389        return String.format("%,.3f ms", millis);
390    }
391    
392    public List<String> getDependencies() 
393    {
394        final String selectedPageClassName = getSelectedPageClassName();
395        List<String> dependencies = new ArrayList<>();
396        dependencies.addAll(
397                componentDependencyRegistry.getDependencies(selectedPageClassName, DependencyType.USAGE));
398        dependencies.addAll(
399                componentDependencyRegistry.getDependencies(selectedPageClassName, DependencyType.INJECT_PAGE));
400        dependencies.addAll(
401                componentDependencyRegistry.getDependencies(selectedPageClassName, DependencyType.SUPERCLASS));
402        Collections.sort(dependencies);
403        return dependencies;
404    }
405    
406    public void onPageStructure(String pageName)
407    {
408        selectedPage = pageSource.getPage(pageName);
409        ajaxResponseRenderer.addRender("pageStructureZone", pageStructureZone.getBody());
410    }
411    
412    public String getDisplayLogicalName() 
413    {
414        return getDisplayLogicalName(dependency);
415    }
416
417    public String getPageClassName() 
418    {
419        return getClassName(page);
420    }
421
422    public String getSelectedPageClassName() 
423    {
424        return getClassName(selectedPage);
425    }
426    
427    private String getClassName(Page page) 
428    {
429        return page.getRootComponent().getComponentResources().getComponentModel().getComponentClassName();
430    }
431
432    private String getClassName(Component component) 
433    {
434        return component.getComponentResources().getComponentModel().getComponentClassName();
435    }
436
437    public void onComponentTree(MarkupWriter writer) 
438    {
439        render(selectedPage.getRootElement(), writer);
440    }
441    
442    private void render(ComponentPageElement componentPageElement, MarkupWriter writer) 
443    {
444        final Element li = writer.element("li");
445        final String className = getClassName(componentPageElement.getComponent());
446        final Set<String> embeddedElementIds = componentPageElement.getEmbeddedElementIds();
447        
448        if (componentPageElement.getComponent().getComponentResources().getComponentModel().isPage()) 
449        {
450            li.text(componentPageElement.getPageName());
451        }
452        else {
453            li.text(String.format("%s (%s)", getDisplayLogicalName(className), componentPageElement.getId()));
454        }
455        
456        if (!embeddedElementIds.isEmpty())
457        {
458            writer.element("ul");
459            for (String id : embeddedElementIds)
460            {
461                render(componentPageElement.getEmbeddedElement(id), writer);
462            }
463            writer.end();
464        }
465        
466        writer.end();
467    }
468
469    private String getDisplayLogicalName(final String className) 
470    {
471        final String logicalName = resolver.getLogicalName(className);
472        String displayName = logicalName;
473        if (logicalName == null || logicalName.trim().length() == 0)
474        {
475            if (className.contains(".base."))
476            {
477                displayName = "(base class)";
478            }
479            if (className.contains(".mixins."))
480            {
481                displayName = "(mixin)";
482            }
483        }
484        return displayName;
485    }
486
487    public String getLogicalName(String className) 
488    {
489        return resolver.getLogicalName(className);
490    }
491    
492    public String getGraphvizValue()
493    {
494        return componentDependencyGraphvizGenerator.generate(getClassName(selectedPage));
495    }
496    
497}