+answer.received=Answer received:
data.collected=Data collected <SPAN ID="elapsed">0m 0s</SPAN> ago at {0}.
destination=Destination
error.page=Error Page
frank=Frank
gps.off=GPS off
gps.read=GPS Read
+invalid.response=Frank requested information from the OC Transpo server, but received an unexpected response.
m=m
+maybe.server.problem=This may indicate a problem with the server that was contacted, but it is also possible that you've uncovered a bug in Frank.
remain=Remain
+request.made=Request made:
route=Route
s=s
unexpected.error=Unexpected Error
+unexpected.exception=An unexpected exception has been raised. This probably indicates a bug in Frank.
+url.contacted=URL contacted:
+answer.received=Answer received:
data.collected=Data collected <SPAN ID="elapsed">0m 0s</SPAN> ago at {0}.
destination=Destination
error.page=Error Page
frank=Frank
gps.off=GPS off
gps.read=GPS Read
+invalid.response=Frank requested information from the OC Transpo server, but received an unexpected response.
m=m
+maybe.server.problem=This may indicate a problem with the server that was contacted, but it is also possible that you've uncovered a bug in Frank.
remain=Remain
+request.made=Request made:
route=Route
s=s
unexpected.error=Unexpected Error
+unexpected.exception=An unexpected exception has been raised. This probably indicates a bug in Frank.
+url.contacted=URL contacted:
--- /dev/null
+package net.jaekl.frank;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.io.PrintWriter;
+import java.util.Locale;
+
+import net.jaekl.qd.http.InvalidResponseException;
+
+public class ErrorHandler {
+ static final String JAVASCRIPT =
+ "<SCRIPT>\n" +
+ " var show_hide = function() {\n" +
+ " var theDiv = document.getElementById('details');\n" +
+ " var theButton = document.getElementById('details_btn');\n" +
+ " if (theDiv.style.display === 'block' || theDiv.style.display === '') {\n" +
+ " theDiv.style.display = 'none';\n" +
+ " theButton.value = 'Show details';\n" +
+ " }\n" +
+ " else {\n" +
+ " theDiv.style.display = 'block';\n" +
+ " theButton.value = 'Hide details';\n" +
+ " }\n" +
+ "}\n" +
+ "</SCRIPT>";
+
+ void writeScript(PrintWriter pw) {
+ pw.println(JAVASCRIPT);
+ }
+
+ void explain(PrintWriter pw, Throwable t, FrankBundle bundle) {
+ Throwable cause = t;
+ if (t instanceof FrankException) {
+ if (null != t.getCause()) {
+ cause = t.getCause();
+ }
+ }
+
+ pw.println("<P>");
+ if (cause instanceof InvalidResponseException) {
+ InvalidResponseException ire = (InvalidResponseException)cause;
+
+ pw.println(bundle.get(FrankBundle.INVALID_RESPONSE));
+ pw.println("<P ID=\"errtable\">");
+ pw.println(" <TABLE>");
+ pw.println(" <TR><TD CLASS=\"head\">" + bundle.get(FrankBundle.URL_CONTACTED)
+ + "</TD><TD>" + ire.getUrl() + "</TD></TR>");
+ pw.println(" <TR CLASS=\"alt\"><TD CLASS=\"head\">" + bundle.get(FrankBundle.REQUEST_MADE)
+ + "</TD><TD>" + ire.getMethod() + "</TD></TR>");
+ pw.println(" <TR><TD CLASS=\"head\">" + bundle.get(FrankBundle.ANSWER_RECEIVED)
+ + "</TD><TD>" + ire.getResponse() + "</TD></TR>");
+ pw.println(" </TABLE>");
+ pw.println("</P>");
+ pw.println("<P>" + bundle.get(FrankBundle.MAYBE_SERVER_PROBLEM) + "</P>");
+ }
+ else {
+ pw.println(bundle.get(FrankBundle.UNEXPECTED_EXCEPTION));
+ }
+ pw.println("</P>");
+ }
+
+ void writeErrorPage(PrintWriter pw, Throwable t, Locale locale) {
+ Style style = new Style();
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ PrintStream ps = new PrintStream(baos);
+ FrankBundle bundle = FrankBundle.getInst(locale);
+
+ pw.println("<HTML><HEAD>");
+ pw.println("<TITLE>" +
+ bundle.get(FrankBundle.FRANK) + ": " +
+ bundle.get(FrankBundle.ERROR_PAGE) +
+ "</TITLE>");
+ style.writeStyle(pw);
+ writeScript(pw);
+ pw.println("</HEAD>");
+
+ pw.println("<BODY>");
+ pw.println("<TABLE ID=\"errhead\" WIDTH=\"100%\"><TR><TD>" +
+ bundle.get(FrankBundle.FRANK) + ": " +
+ bundle.get(FrankBundle.UNEXPECTED_ERROR) +
+ "</TD></TR></TABLE>");
+
+ explain(pw, t, bundle);
+
+ // Note that, if we cared about security, we would log this stack trace to a
+ // server log, and only report a cross-reference to the log file back to the
+ // end user's browser, to avoid potentially exposing internal info that we
+ // don't want to share.
+ // At least at this point, we don't care (that much), and trade off a
+ // potential information leak in favour of reducing our code complexity
+ // and the administrator's workload.
+
+ pw.println("<P><INPUT TYPE=\"button\" ID=\"details_btn\" VALUE=\"Show details\" ONCLICK=\"show_hide();\"/></P>");
+ pw.println("<P>");
+ pw.println("<DIV ID=\"details\" STYLE=\"display: none;\"><PRE>");
+
+ t.printStackTrace(ps);
+ String stackTrace = baos.toString();
+ pw.println(stackTrace);
+
+ pw.println("</PRE>\n</P>\n</DIV>");
+ pw.println("<P>Click <A HREF=\"/\">here</A> to return to the main page.</P>");
+ pw.println("</BODY></HTML>");
+ }
+}
import net.jaekl.qd.QDBundleFactory;
public class FrankBundle {
+ public static final String ANSWER_RECEIVED = "answer.received";
public static final String DATA_COLLECTED = "data.collected";
public static final String DESTINATION = "destination";
public static final String ERROR_PAGE = "error.page";
public static final String FRANK = "frank";
public static final String GPS_OFF = "gps.off";
public static final String GPS_READ = "gps.read";
+ public static final String INVALID_RESPONSE = "invalid.response";
+ public static final String MAYBE_SERVER_PROBLEM = "maybe.server.problem";
public static final String MINUTES = "m"; // suffix (abbreviated) for minutes
public static final String REMAIN = "remain";
+ public static final String REQUEST_MADE = "request.made";
public static final String ROUTE = "route";
public static final String SECONDS = "s";
public static final String UNEXPECTED_ERROR = "unexpected.error";
+ public static final String UNEXPECTED_EXCEPTION = "unexpected.exception";
+ public static final String URL_CONTACTED = "url.contacted";
final static String BUNDLE_NAME = "frank";
import net.jaekl.frank.octranspo.Trip;
public class Schedule {
- Locale m_locale;
- FrankBundle m_bundle;
DateFormat m_hourMinFmt;
DateFormat m_hourMinSecFmt;
+ FrankBundle m_bundle;
+ Locale m_locale;
+ Style m_style;
public Schedule(Locale locale) {
m_locale = locale;
+ m_style = new Style();
m_bundle = FrankBundle.getInst(locale);
m_hourMinFmt = new SimpleDateFormat("hh:mma", locale);
m_hourMinSecFmt = new SimpleDateFormat("hh:mm:ssa", locale);
String mapUrl(double latitude, double longitude) {
return "http://www.openstreetmap.org/?mlat=" + latitude + "&mlon=" + longitude + "&zoom=15";
}
-
- void writeStyle(PrintWriter pw) {
- pw.println("<STYLE>");
- pw.println(" body {background-color: #F0F0C0; font-size: 1.5em; }");
- pw.println(" #trips {border-collapse: collapse; font-size: 1.5em; }");
- pw.println(" #trips td, #trips th {border: 1px solid #600000; padding: 3px 3px 3px 3px; text-align: center;}");
- pw.println(" #trips th {background-color: #800000; color: #FFFFFF; }");
- pw.println(" #trips tr.ghost td {background-color: #C0C0C0;}");
- pw.println("</STYLE>");
- }
-
+
// Countdown timer that updates time remaining until each bus is expected.
void writeScript(PrintWriter pw, String remainArray, int remainCount) {
String min = trans(FrankBundle.MINUTES);
pw.println("<HTML>");
pw.println("<HEAD>");
pw.println("<TITLE>" + title + "</TITLE>");
- writeStyle(pw);
+ new Style().writeStyle(pw);
pw.println("</HEAD>");
}
--- /dev/null
+package net.jaekl.frank;
+
+import java.io.PrintWriter;
+
+public class Style {
+ static final String CSS =
+ "<STYLE>\n" +
+ " body {background-color: #F0F0C0; font-size: 1em; }\n" +
+ " #errhead td {background-color: #D00000; color: #FFFFFF; font-size: 2em; padding: 3px 3px 3px 3px; text-align: left; }\n" +
+ " #errtable table { border: 3px solid #D00000; border-collapse: collapse; background-color: #E0E080; }\n" +
+ " #errtable table, tr, td { padding: 3px; }\n" +
+ " #errtable tr.alt { background-color: #D0D070; }\n" +
+ " #errtable td.head {text-align: right; background-color: #D00000; color: #FFFFFF; font-weight: bold; }\n" +
+ " #trips {border-collapse: collapse; font-size: 1.5em; }\n" +
+ " #trips td, #trips th {border: 1px solid #600000; padding: 3px 3px 3px 3px; text-align: center;}\n" +
+ " #trips th {background-color: #800000; color: #FFFFFF; }\n" +
+ " #trips tr.ghost td {background-color: #C0C0C0;}\n" +
+ "</STYLE>";
+
+ public void writeStyle(PrintWriter pw) {
+ pw.println(CSS);
+ }
+}
package net.jaekl.frank;
-import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
-import java.io.PrintStream;
import java.io.PrintWriter;
import java.util.Locale;
static final String ROUTE = "route";
static final String LANG = "lang";
+ ErrorHandler m_errorHandler;
+
+ public ViewSchedule() {
+ m_errorHandler = new ErrorHandler();
+ }
+
int getParamInt(HttpServletRequest req, String paramName) {
String valueStr = getParamString(req, paramName);
try {
}
}
catch (Throwable t) {
- writeErrorPage(pw, t, locale);
+ m_errorHandler.writeErrorPage(pw, t, locale);
}
}
-
- void writeErrorPage(PrintWriter pw, Throwable t, Locale locale) {
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
- PrintStream ps = new PrintStream(baos);
- FrankBundle bundle = FrankBundle.getInst(locale);
-
- pw.println("<HTML><HEAD><TITLE>" +
- bundle.get(FrankBundle.FRANK) + ": " +
- bundle.get(FrankBundle.ERROR_PAGE) +
- "</TITLE></HEAD>");
- pw.println("<BODY><H1>" +
- bundle.get(FrankBundle.FRANK) + ": " +
- bundle.get(FrankBundle.UNEXPECTED_ERROR) +
- "</H1><P><PRE>");
-
- // Note that, if we cared about security, we would log this stack trace to a
- // server log, and only report a cross-reference to the log file back to the
- // end user's browser, to avoid potentially exposing internal info that we
- // don't want to share.
- // At least at this point, we don't care (that much), and trade off a
- // potential information leak in favour of reducing our code complexity
- // and the administrator's workload.
- t.printStackTrace(ps);
- String stackTrace = baos.toString();
- pw.println(stackTrace);
-
- pw.println("</PRE></P></BODY>");
- pw.println("</HTML>");
- }
-
-
}
public String getUrl() { return m_url; }
public String getMethod() { return m_method; }
+ public String getResponse() { return m_response; }
@Override
public String toString() {
--- /dev/null
+package net.jaekl.frank;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.sql.SQLException;
+import java.util.Locale;
+
+import net.jaekl.qd.QDException;
+import net.jaekl.qd.http.InvalidResponseException;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+public class ErrorHandlerTest {
+ static final String GATEWAY = "http://www.jaekl.net/api";
+ static final String METHOD = "SampleApiMethod";
+ static final String RESPONSE = "Required parameter not specified.";
+
+ ByteArrayOutputStream m_baos;
+ PrintWriter m_pw;
+
+ @Before
+ public void setUp() {
+ m_baos = new ByteArrayOutputStream();
+ m_pw = new PrintWriter(m_baos);
+ }
+
+ @After
+ public void tearDown() throws IOException {
+ m_pw.close();
+ }
+
+ @Test
+ public void testWriteErrorPage_basicBehaviourWithVariousExceptions() {
+ ErrorHandler eh = new ErrorHandler();
+
+ Throwable[] throwables = {
+ new NullPointerException(),
+ new QDException(),
+ new SQLException(),
+ new InvalidResponseException(new NullPointerException(), GATEWAY, METHOD, RESPONSE)
+ };
+
+ for (Throwable t : throwables) {
+ m_baos.reset();
+ eh.writeErrorPage(m_pw, t, Locale.CANADA);
+ m_pw.flush();
+
+ String actual = m_baos.toString();
+ Assert.assertTrue(actual.contains("<TITLE>Frank: Error Page</TITLE>"));
+ Assert.assertTrue(actual.contains(t.toString()));
+ }
+ }
+
+ @Test
+ public void testExplain_unexpectedException() {
+ Locale[] locales = { Locale.CANADA, Locale.FRANCE, Locale.JAPAN, Locale.CHINA};
+
+ ErrorHandler eh = new ErrorHandler();
+
+ for (Locale locale : locales) {
+ FrankBundle bundle = FrankBundle.getInst(locale);
+
+ m_baos.reset();
+ eh.writeErrorPage(m_pw, new NullPointerException(), Locale.CANADA);
+ m_pw.flush();
+
+ String actual = m_baos.toString();
+ Assert.assertTrue(actual.contains(bundle.get(FrankBundle.UNEXPECTED_EXCEPTION)));
+ }
+ }
+
+ @Test
+ public void testExplain_wrappedInvalidResponseException() {
+ Locale[] locales = { Locale.CANADA, Locale.FRANCE, Locale.JAPAN, Locale.CHINA};
+
+ ErrorHandler eh = new ErrorHandler();
+ InvalidResponseException ire = new InvalidResponseException(null, GATEWAY, METHOD, RESPONSE);
+ FrankException fe = new FrankException(ire);
+
+ for (Locale locale : locales) {
+ FrankBundle bundle = FrankBundle.getInst(locale);
+
+ m_baos.reset();
+ eh.writeErrorPage(m_pw, fe, Locale.CANADA);
+ m_pw.flush();
+
+ String actual = m_baos.toString();
+
+ Assert.assertTrue(actual.contains(bundle.get(FrankBundle.INVALID_RESPONSE)));
+
+ Assert.assertTrue(actual.contains(bundle.get(FrankBundle.URL_CONTACTED)));
+ Assert.assertTrue(actual.contains(bundle.get(FrankBundle.REQUEST_MADE)));
+ Assert.assertTrue(actual.contains(bundle.get(FrankBundle.ANSWER_RECEIVED)));
+
+ Assert.assertTrue(actual.contains(bundle.get(FrankBundle.MAYBE_SERVER_PROBLEM)));
+ }
+ }
+}
import org.junit.Test;
public class ScheduleTest {
- static final String EXPECTED_STYLE = "<STYLE>\n" +
- " body {background-color: #F0F0C0; font-size: 1.5em; }\n" +
- " #trips {border-collapse: collapse; font-size: 1.5em; }\n" +
- " #trips td, #trips th {border: 1px solid #600000; padding: 3px 3px 3px 3px; text-align: center;}\n" +
- " #trips th {background-color: #800000; color: #FFFFFF; }\n" +
- " #trips tr.ghost td {background-color: #C0C0C0;}\n" +
- "</STYLE>\n";
static final String TITLE_PREFIX = "<HTML>\n<HEAD>\n<TITLE>";
- static final String TITLE_SUFFIX = "</TITLE>\n" + EXPECTED_STYLE + "</HEAD>\n";
+ static final String TITLE_SUFFIX = "</TITLE>\n" + Style.CSS + "\n</HEAD>\n";
ByteArrayOutputStream m_baos;
}
}
- // Confirm that writeStyle's output does not vary with the locale
- @Test
- public void test_writeStyle() {
- String actual;
-
- Locale[] locales = { Locale.CANADA, Locale.CANADA_FRENCH, Locale.JAPAN };
-
- for (Locale locale : locales) {
- m_baos.reset();
-
- Schedule schedule = new Schedule(locale);
- schedule.writeStyle(m_pw);
- m_pw.flush();
-
- actual = m_baos.toString();
- Assert.assertEquals(EXPECTED_STYLE, actual);
- }
- }
-
@Test
public void test_writeHeader() {
Locale[] locales = { Locale.CANADA, Locale.CANADA_FRENCH, Locale.JAPAN };
String actual = m_baos.toString();
- Assert.assertTrue(actual.contains(EXPECTED_STYLE));
+ Assert.assertTrue(actual.contains(Style.CSS));
String expectedTitle = TITLE_PREFIX + "Frank: " + stopName + " (" + stopNo + ")" + TITLE_SUFFIX;
Assert.assertTrue(actual.contains(expectedTitle));
// Some rudimentary validation of the result.
// Should really go through more permutations, and examine them more closely, here.
- Assert.assertTrue(actual.contains(EXPECTED_STYLE));
+ Assert.assertTrue(actual.contains(Style.CSS));
String expectedTitle = TITLE_PREFIX + "Frank: " + stopName + " (" + stopNo + ")" + TITLE_SUFFIX;
Assert.assertTrue(actual.contains(expectedTitle));
--- /dev/null
+package net.jaekl.frank;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.PrintWriter;
+
+import junit.framework.Assert;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class StyleTest {
+ ByteArrayOutputStream m_baos;
+ PrintWriter m_pw;
+
+ @Before
+ public void setUp() {
+ m_baos = new ByteArrayOutputStream();
+ m_pw = new PrintWriter(m_baos);
+ }
+
+ @After
+ public void tearDown() throws IOException {
+ if (null != m_baos) {
+ m_baos.close();
+ }
+ }
+
+ // Should develop something here that is a bit smarter about what
+ // it's looking for, and less brittle than a simple compare-against-master...
+ @Test
+ public void test_writeStyle() {
+ String actual;
+
+ m_baos.reset();
+
+ Style style = new Style();
+ style.writeStyle(m_pw);
+ m_pw.flush();
+
+ actual = m_baos.toString();
+ Assert.assertEquals(Style.CSS + "\n", actual);
+ }
+}
package net.jaekl.frank;
-import java.io.ByteArrayOutputStream;
-import java.io.PrintWriter;
-import java.sql.SQLException;
import java.util.HashMap;
-import java.util.Locale;
-import net.jaekl.qd.QDException;
import net.jaekl.qd.http.HttpServletRequestMock;
import org.junit.Assert;
value = vs.getParamString(reqMock, "notPresent");
Assert.assertEquals(null, value);
}
-
- @Test
- public void testWriteErrorPage() {
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
- PrintWriter pw = new PrintWriter(baos);
-
- ViewSchedule vs = new ViewSchedule();
-
- Throwable[] throwables = {
- new NullPointerException(),
- new QDException(),
- new SQLException()
- };
-
- for (Throwable t : throwables) {
- baos.reset();
- vs.writeErrorPage(pw, t, Locale.CANADA); // TODO: test translations
- pw.flush();
-
- String actual = baos.toString();
- Assert.assertTrue(actual.contains("<TITLE>Frank: Error Page</TITLE>"));
- Assert.assertTrue(actual.contains(t.toString()));
- }
- }
-
}