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