001// Copyright 2011 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 org.apache.tapestry5.SymbolConstants;
018import org.apache.tapestry5.alerts.AlertManager;
019import org.apache.tapestry5.annotations.*;
020import org.apache.tapestry5.beaneditor.BeanModel;
021import org.apache.tapestry5.beaneditor.Validate;
022import org.apache.tapestry5.corelib.components.Zone;
023import org.apache.tapestry5.func.*;
024import org.apache.tapestry5.internal.PageCatalogTotals;
025import org.apache.tapestry5.internal.services.ComponentInstantiatorSource;
026import org.apache.tapestry5.internal.services.PageSource;
027import org.apache.tapestry5.internal.structure.Page;
028import org.apache.tapestry5.ioc.Messages;
029import org.apache.tapestry5.ioc.OperationTracker;
030import org.apache.tapestry5.ioc.annotations.Inject;
031import org.apache.tapestry5.ioc.annotations.Symbol;
032import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
033import org.apache.tapestry5.ioc.internal.util.InternalUtils;
034import org.apache.tapestry5.services.BeanModelSource;
035import org.apache.tapestry5.services.ComponentClassResolver;
036import org.apache.tapestry5.services.pageload.ComponentResourceSelector;
037
038import java.util.Collection;
039import java.util.List;
040import java.util.Set;
041
042/**
043 * Lists out the currently loaded pages, using a {@link org.apache.tapestry5.corelib.components.Grid}.
044 * Provides an option to force all pages to be loaded. In development mode, includes an option to clear the page cache.
045 */
046@ContentType("text/html")
047@WhitelistAccessOnly
048public class PageCatalog
049{
050
051    @Property
052    private PageCatalogTotals totals;
053
054    @Property
055    @Inject
056    @Symbol(SymbolConstants.PRODUCTION_MODE)
057    private boolean productionMode;
058
059    @Inject
060    private PageSource pageSource;
061
062    @Inject
063    private ComponentResourceSelector selector;
064
065    @Inject
066    private ComponentClassResolver resolver;
067
068    @Inject
069    private AlertManager alertManager;
070
071    @Property
072    private Page page;
073
074    @InjectComponent
075    private Zone pagesZone;
076
077    @Persist
078    private Set<String> failures;
079
080    @Property
081    @Validate("required")
082    @Persist
083    private String pageName;
084
085    @Inject
086    private OperationTracker operationTracker;
087
088    @Inject
089    private ComponentInstantiatorSource componentInstantiatorSource;
090
091    @Inject
092    private BeanModelSource beanModelSource;
093
094    @Inject
095    private Messages messages;
096
097    @Property
098    @Retain
099    private BeanModel<Page> model;
100
101    void pageLoaded()
102    {
103        model = beanModelSource.createDisplayModel(Page.class, messages);
104
105        model.addExpression("selector", "selector.toString()");
106        model.addExpression("assemblyTime", "stats.assemblyTime");
107        model.addExpression("componentCount", "stats.componentCount");
108        model.addExpression("weight", "stats.weight");
109
110        model.reorder("name", "selector", "assemblyTime", "componentCount", "weight");
111    }
112
113    public void onRecomputeTotals()
114    {
115        totals = new PageCatalogTotals();
116
117        Flow<Page> pages = F.flow(getPages());
118
119        totals.loadedPages = pages.count();
120        totals.definedPages = getPageNames().size();
121        totals.uniquePageNames = pages.map(new Mapper<Page, String>()
122        {
123            public String map(Page element)
124            {
125                return element.getName();
126            }
127        }).toSet().size();
128
129        totals.components = pages.reduce(new Reducer<Integer, Page>()
130        {
131            public Integer reduce(Integer accumulator, Page element)
132            {
133                return accumulator + element.getStats().componentCount;
134            }
135        }, 0);
136
137        Set<String> selectorIds = pages.map(new Mapper<Page, String>()
138        {
139            public String map(Page element)
140            {
141                return element.getSelector().toShortString();
142            }
143        }).toSet();
144
145        totals.selectors = InternalUtils.joinSorted(selectorIds);
146    }
147
148    public List<String> getPageNames()
149    {
150        return resolver.getPageNames();
151    }
152
153    public Collection<Page> getPages()
154    {
155        return pageSource.getAllPages();
156    }
157
158    Object onActionFromReloadClasses()
159    {
160        if (productionMode)
161        {
162            alertManager.error("Forcing a class reload is only allowed when executing in development mode.");
163            return null;
164        }
165
166        pageName = null;
167
168        componentInstantiatorSource.forceComponentInvalidation();
169
170        alertManager.info("Forced a component class reload.");
171
172        return pagesZone.getBody();
173    }
174
175    Object onSuccessFromSinglePageLoad()
176    {
177        boolean found = !F.flow(getPages()).filter(new Predicate<Page>()
178        {
179            public boolean accept(Page element)
180            {
181                return element.getName().equals(pageName) && element.getSelector().equals(selector);
182            }
183        }).isEmpty();
184
185        if (found)
186        {
187            alertManager.warn(String.format("Page %s has already been loaded for '%s'.",
188                    pageName, selector.toShortString()));
189            return null;
190        }
191
192        long startTime = System.currentTimeMillis();
193
194
195        // Load the page now (may cause an exception).
196
197        pageSource.getPage(pageName);
198
199
200        alertManager.info(String.format("Loaded page %s for selector '%s' (in %,d ms).", pageName,
201                selector.toShortString(), System.currentTimeMillis() - startTime));
202
203        return pagesZone.getBody();
204    }
205
206    private class PageLoadData
207    {
208        int loadedCount;
209        RuntimeException fail;
210        boolean someFail;
211    }
212
213    Object onActionFromForceLoad()
214    {
215        if (failures == null)
216        {
217            failures = CollectionFactory.newSet();
218        }
219
220        long startTime = System.currentTimeMillis();
221
222        final Collection<Page> initialPages = getPages();
223
224        final PageLoadData data = new PageLoadData();
225
226        for (final String name : resolver.getPageNames())
227        {
228            if (failures.contains(name))
229            {
230                alertManager.warn(String.format("Skipping page %s due to prior load failure.", name));
231                data.someFail = true;
232                continue;
233            }
234
235            operationTracker.run("Loading page " + name, new Runnable()
236            {
237                public void run()
238                {
239                    try
240                    {
241                        Page newPage = pageSource.getPage(name);
242
243                        if (!initialPages.contains(newPage))
244                        {
245                            data.loadedCount++;
246                        }
247                    } catch (RuntimeException ex)
248                    {
249                        alertManager.error(String.format("Page %s failed to load.", name));
250                        failures.add(name);
251
252                        if (data.fail == null)
253                        {
254                            pageName = name;
255                            data.fail = ex;
256                        }
257                    }
258                }
259            });
260
261            if (data.fail != null)
262            {
263                break;
264            }
265        }
266
267        alertManager.info(String.format("Loaded %,d new pages for selector '%s' (in %,d ms).", data.loadedCount,
268                selector.toShortString(), System.currentTimeMillis() - startTime));
269
270        if (data.someFail)
271        {
272            alertManager.warn("Clear the cache to reset the list of failed pages.");
273        }
274
275        if (data.fail != null)
276        {
277            throw data.fail;
278        }
279
280        return pagesZone.getBody();
281    }
282
283    Object onActionFromClearCache()
284    {
285        if (productionMode)
286        {
287            alertManager.error("Clearing the cache is only allowed in development mode.");
288            return null;
289        }
290
291        pageSource.clearCache();
292
293        failures = null;
294
295        alertManager.info("Page cache cleared.");
296
297        return pagesZone.getBody();
298    }
299
300    Object onActionFromRunGC()
301    {
302        if (productionMode)
303        {
304            alertManager.error("Executing a garbage collection is only allowed in development mode.");
305            return null;
306        }
307
308        Runtime runtime = Runtime.getRuntime();
309
310        long initialFreeMemory = runtime.freeMemory();
311
312        runtime.gc();
313
314        long delta = runtime.freeMemory() - initialFreeMemory;
315
316        alertManager.info(String.format("Garbage collection freed %,.2f Kb of memory.",
317                ((double) delta) / 1024.0d));
318
319        return pagesZone.getBody();
320    }
321
322    public String formatElapsed(long millis)
323    {
324        return String.format("%,d ms", millis);
325    }
326}