001// Licensed under the Apache License, Version 2.0 (the "License");
002// you may not use this file except in compliance with the License.
003// You may obtain a copy of the License at
004//
005//     http://www.apache.org/licenses/LICENSE-2.0
006//
007// Unless required by applicable law or agreed to in writing, software
008// distributed under the License is distributed on an "AS IS" BASIS,
009// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
010// See the License for the specific language governing permissions and
011// limitations under the License.
012
013package org.apache.tapestry5.corelib.pages;
014
015import org.apache.tapestry5.ComponentResources;
016import org.apache.tapestry5.EventContext;
017import org.apache.tapestry5.alerts.AlertManager;
018import org.apache.tapestry5.annotations.ContentType;
019import org.apache.tapestry5.annotations.Import;
020import org.apache.tapestry5.annotations.Property;
021import org.apache.tapestry5.annotations.UnknownActivationContextCheck;
022import org.apache.tapestry5.beanmodel.services.*;
023import org.apache.tapestry5.commons.services.InvalidationEventHub;
024import org.apache.tapestry5.commons.util.CollectionFactory;
025import org.apache.tapestry5.corelib.base.AbstractInternalPage;
026import org.apache.tapestry5.func.F;
027import org.apache.tapestry5.func.Mapper;
028import org.apache.tapestry5.http.Link;
029import org.apache.tapestry5.http.TapestryHttpSymbolConstants;
030import org.apache.tapestry5.http.services.BaseURLSource;
031import org.apache.tapestry5.http.services.RequestGlobals;
032import org.apache.tapestry5.http.services.Session;
033import org.apache.tapestry5.internal.InternalConstants;
034import org.apache.tapestry5.internal.TapestryInternalUtils;
035import org.apache.tapestry5.internal.services.PageActivationContextCollector;
036import org.apache.tapestry5.internal.services.ReloadHelper;
037import org.apache.tapestry5.ioc.annotations.ComponentClasses;
038import org.apache.tapestry5.ioc.annotations.Inject;
039import org.apache.tapestry5.ioc.annotations.Symbol;
040import org.apache.tapestry5.ioc.internal.util.InternalUtils;
041import org.apache.tapestry5.services.ExceptionReporter;
042import org.apache.tapestry5.services.PageRenderLinkSource;
043import org.apache.tapestry5.services.URLEncoder;
044
045import java.net.MalformedURLException;
046import java.net.URL;
047import java.util.Arrays;
048import java.util.Collections;
049import java.util.List;
050import java.util.regex.Pattern;
051
052/**
053 * Responsible for reporting runtime exceptions. This page is quite verbose and is usually overridden in a production
054 * application. When {@link org.apache.tapestry5.http.TapestryHttpSymbolConstants#PRODUCTION_MODE} is "true", it is very abbreviated.
055 *
056 * @see org.apache.tapestry5.corelib.components.ExceptionDisplay
057 */
058@UnknownActivationContextCheck(false)
059@ContentType("text/html")
060@Import(stylesheet = "ExceptionReport.css")
061public class ExceptionReport extends AbstractInternalPage implements ExceptionReporter
062{
063    private static final String PATH_SEPARATOR_PROPERTY = "path.separator";
064
065    // Match anything ending in .(something?)path.
066
067    private static final Pattern PATH_RECOGNIZER = Pattern.compile("\\..*path$");
068
069    @Property
070    private String attributeName;
071
072    @Inject
073    @Symbol(TapestryHttpSymbolConstants.PRODUCTION_MODE)
074    @Property(write = false)
075    private boolean productionMode;
076
077    @Inject
078    @Symbol(TapestryHttpSymbolConstants.TAPESTRY_VERSION)
079    @Property(write = false)
080    private String tapestryVersion;
081
082    @Inject
083    @Symbol(TapestryHttpSymbolConstants.APPLICATION_VERSION)
084    @Property(write = false)
085    private String applicationVersion;
086
087    @Property(write = false)
088    private Throwable rootException;
089
090    @Property
091    private String propertyName;
092
093    @Inject
094    private RequestGlobals requestGlobals;
095
096    @Inject
097    private AlertManager alertManager;
098
099    @Inject
100    private PageActivationContextCollector pageActivationContextCollector;
101
102    @Inject
103    private PageRenderLinkSource linkSource;
104
105    @Inject
106    private BaseURLSource baseURLSource;
107
108    @Inject
109    private ReloadHelper reloadHelper;
110
111    @Inject
112    private URLEncoder urlEncoder;
113
114    @Property
115    private String rootURL;
116
117    @Property
118    private ThreadInfo thread;
119
120    @Inject
121    private ComponentResources resources;
122    
123    @Inject
124    @ComponentClasses 
125    private InvalidationEventHub classesInvalidationHub;
126
127    private String failurePage;
128
129    /**
130     * A link the user may press to perform an action (e.g., "Reload page").
131     */
132    public static class ActionLink
133    {
134        public final String uri, label;
135
136
137        public ActionLink(String uri, String label)
138        {
139            this.uri = uri;
140            this.label = label;
141        }
142    }
143
144    @Property
145    private ActionLink actionLink;
146
147    public class ThreadInfo implements Comparable<ThreadInfo>
148    {
149        public final String className, name, state, flags;
150
151        public final ThreadGroup group;
152
153        public ThreadInfo(String className, String name, String state, String flags, ThreadGroup group)
154        {
155            this.className = className;
156            this.name = name;
157            this.state = state;
158            this.flags = flags;
159            this.group = group;
160        }
161
162        @Override
163        public int compareTo(ThreadInfo o)
164        {
165            return name.compareTo(o.name);
166        }
167    }
168
169    private final String pathSeparator = System.getProperty(PATH_SEPARATOR_PROPERTY);
170
171    /**
172     * Returns true for normal, non-XHR requests. Links (to the failure page, or to root page) are only
173     * presented if showActions is true.
174     */
175    public boolean isShowActions()
176    {
177        return !request.isXHR();
178    }
179
180    /**
181     * Returns true in development mode; enables the "with reload" actions.
182     */
183    public boolean isShowReload()
184    {
185        return !productionMode;
186    }
187
188    public void reportException(Throwable exception)
189    {
190        rootException = exception;
191
192        rootURL = baseURLSource.getBaseURL(request.isSecure());
193
194        // Capture this now ... before the gears are shifted around to make ExceptionReport the active page.
195        failurePage = (request.getAttribute(InternalConstants.ACTIVE_PAGE_LOADED) == null)
196                ? null
197                : requestGlobals.getActivePageName();
198    }
199
200    private static void add(List<ActionLink> links, Link link, String format, Object... arguments)
201    {
202        String label = String.format(format, arguments);
203        links.add(new ActionLink(link.toURI(), label));
204    }
205
206    public List<ActionLink> getActionLinks()
207    {
208        List<ActionLink> links = CollectionFactory.newList();
209
210        if (failurePage != null)
211        {
212
213            try
214            {
215
216                Object[] pac = pageActivationContextCollector.collectPageActivationContext(failurePage);
217
218                add(links,
219                        linkSource.createPageRenderLinkWithContext(failurePage, pac),
220                        "Go to page <strong>%s</strong>", failurePage);
221
222                if (!productionMode)
223                {
224                    add(links,
225                            resources.createEventLink("reloadFirst", pac).addParameter("loadPage",
226                                    urlEncoder.encode(failurePage)),
227                            "Go to page <strong>%s</strong> (with reload)", failurePage);
228                }
229
230            } catch (Throwable t)
231            {
232                // Ignore.
233            }
234        }
235
236        links.add(new ActionLink(rootURL,
237                String.format("Go to <strong>%s</strong>", rootURL)));
238
239
240        if (!productionMode)
241        {
242            add(links,
243                    resources.createEventLink("reloadFirst"),
244                    "Go to <strong>%s</strong> (with reload)", rootURL);
245        }
246
247        return links;
248    }
249
250
251    Object onReloadFirst(EventContext reloadContext)
252    {
253
254        classesInvalidationHub.fireInvalidationEvent(Collections.emptyList());
255        reloadHelper.forceReload();
256
257        return linkSource.createPageRenderLinkWithContext(urlEncoder.decode(request.getParameter("loadPage")), reloadContext);
258    }
259
260    Object onReloadRoot() throws MalformedURLException
261    {
262        reloadHelper.forceReload();
263
264        return new URL(baseURLSource.getBaseURL(request.isSecure()));
265    }
266
267
268    public boolean getHasSession()
269    {
270        return request.getSession(false) != null;
271    }
272
273    public Session getSession()
274    {
275        return request.getSession(false);
276    }
277
278    public Object getAttributeValue()
279    {
280        return getSession().getAttribute(attributeName);
281    }
282
283    /**
284     * Returns a <em>sorted</em> list of system property names.
285     */
286    public List<String> getSystemProperties()
287    {
288        return InternalUtils.sortedKeys(System.getProperties());
289    }
290
291    public String getPropertyValue()
292    {
293        return System.getProperty(propertyName);
294    }
295
296    public boolean isComplexProperty()
297    {
298        return PATH_RECOGNIZER.matcher(propertyName).find() && getPropertyValue().contains(pathSeparator);
299    }
300
301    public String[] getComplexPropertyValue()
302    {
303        // Neither : nor ; is a regexp character.
304
305        return getPropertyValue().split(pathSeparator);
306    }
307
308    public List<ThreadInfo> getThreads()
309    {
310        return F.flow(TapestryInternalUtils.getAllThreads()).map(new Mapper<Thread, ThreadInfo>()
311        {
312            @Override
313            public ThreadInfo map(Thread t)
314            {
315                List<String> flags = CollectionFactory.newList();
316
317                if (t.isDaemon())
318                {
319                    flags.add("daemon");
320                }
321                if (!t.isAlive())
322                {
323                    flags.add("NOT alive");
324                }
325                if (t.isInterrupted())
326                {
327                    flags.add("interrupted");
328                }
329
330                if (t.getPriority() != Thread.NORM_PRIORITY)
331                {
332                    flags.add("priority " + t.getPriority());
333                }
334
335                return new ThreadInfo(Thread.currentThread() == t ? "active-thread" : "",
336                        t.getName(),
337                        t.getState().name(),
338                        InternalUtils.join(flags),
339                        t.getThreadGroup());
340            }
341        }).sort().toList();
342    }
343}