[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]

[or-cvs] [metrics-web/master] Split relay-search.html into a servlet and a JSP.



Author: Karsten Loesing <karsten.loesing@xxxxxxx>
Date: Sun, 17 Oct 2010 10:02:30 +0200
Subject: Split relay-search.html into a servlet and a JSP.
Commit: ed3900af506e649dd10124daed59509f0c62dd1a

---
 .../torproject/ernie/web/RelaySearchServlet.java   |  341 +++++++-------------
 web/WEB-INF/relay-search.jsp                       |   96 ++++++
 2 files changed, 210 insertions(+), 227 deletions(-)
 create mode 100644 web/WEB-INF/relay-search.jsp

diff --git a/src/org/torproject/ernie/web/RelaySearchServlet.java b/src/org/torproject/ernie/web/RelaySearchServlet.java
index c938bba..9724f5b 100644
--- a/src/org/torproject/ernie/web/RelaySearchServlet.java
+++ b/src/org/torproject/ernie/web/RelaySearchServlet.java
@@ -9,7 +9,6 @@ import java.text.*;
 import java.util.*;
 import java.util.regex.*;
 
-import org.apache.commons.codec.*;
 import org.apache.commons.codec.binary.*;
 
 /**
@@ -33,35 +32,30 @@ import org.apache.commons.codec.binary.*;
  */
 public class RelaySearchServlet extends HttpServlet {
 
-  private static Pattern alphaNumDotDashSpacePattern =
+  private Pattern alphaNumDotDashSpacePattern =
       Pattern.compile("[A-Za-z0-9\\.\\- ]+");
 
-  private static Pattern numPattern = Pattern.compile("[0-9]+");
+  private Pattern numPattern = Pattern.compile("[0-9]+");
 
-  private static Pattern hexPattern = Pattern.compile("[A-Fa-f0-9]+");
+  private Pattern hexPattern = Pattern.compile("[A-Fa-f0-9]+");
 
-  private static Pattern alphaNumPattern =
-      Pattern.compile("[A-Za-z0-9]+");
+  private Pattern alphaNumPattern = Pattern.compile("[A-Za-z0-9]+");
 
-  private static SimpleDateFormat dayFormat =
-      new SimpleDateFormat("yyyy-MM-dd");
+  private SimpleDateFormat dayFormat = new SimpleDateFormat("yyyy-MM-dd");
 
-  private static SimpleDateFormat monthFormat =
-      new SimpleDateFormat("yyyy-MM");
+  private SimpleDateFormat monthFormat = new SimpleDateFormat("yyyy-MM");
 
-  private static SimpleDateFormat dateTimeFormat =
+  private SimpleDateFormat dateTimeFormat =
       new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
 
-  static {
-    dayFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
-    monthFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
-    dateTimeFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
-  }
-
   private Connection conn = null;
 
   public void init() {
 
+    dayFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+    monthFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+    dateTimeFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+
     /* Try to load the database driver. */
     try {
       Class.forName("org.postgresql.Driver");
@@ -83,136 +77,28 @@ public class RelaySearchServlet extends HttpServlet {
     }
   }
 
-  private void writeHeader(PrintWriter out) throws IOException {
-    out.println("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 "
-          + "Transitional//EN\">\n"
-        + "<html>\n"
-        + "  <head>\n"
-        + "    <title>Tor Metrics Portal: Relay Search</title>\n"
-        + "    <meta http-equiv=\"content-type\" content=\"text/html; "
-          + "charset=ISO-8859-1\">\n"
-        + "    <link href=\"/css/stylesheet-ltr.css\" type=\"text/css\" "
-          + "rel=\"stylesheet\">\n"
-        + "    <link href=\"/images/favicon.ico\" "
-          + "type=\"image/x-icon\" rel=\"shortcut icon\">\n"
-        + "  </head>\n"
-        + "  <body>\n"
-        + "    <div class=\"center\">\n"
-        + "      <table class=\"banner\" border=\"0\" cellpadding=\"0\" "
-          + "cellspacing=\"0\" summary=\"\">\n"
-        + "        <tr>\n"
-        + "          <td class=\"banner-left\"><a "
-          + "href=\"/index.html\"><img src=\"/images/top-left.png\" "
-          + "alt=\"Click to go to home page\" width=\"193\" "
-          + "height=\"79\"></a></td>\n"
-        + "          <td class=\"banner-middle\">\n"
-        + "            <a href=\"/\">Home</a>\n"
-        + "            <a href=\"graphs.html\">Graphs</a>\n"
-        + "            <a href=\"research.html\">Research</a>\n"
-        + "            <a href=\"status.html\">Status</a>\n"
-        + "            <br>\n"
-        + "            <font size=\"2\">\n"
-        + "              <a href=\"exonerator.html\">ExoneraTor</a>\n"
-        + "              <a class=\"current\">Relay Search</a>\n"
-        + "              <a href=\"consensus-health.html\">Consensus "
-          + "Health</a>\n"
-        + "            </font>\n"
-        + "          </td>\n"
-        + "          <td class=\"banner-right\"></td>\n"
-        + "        </tr>\n"
-        + "      </table>\n"
-        + "      <div class=\"main-column\" style=\"margin:5; "
-          + "Padding:0;\">\n"
-        + "        <h2>Relay Search</h2>\n");
-  }
-
-  private void writeFooter(PrintWriter out) throws IOException {
-    out.println("        <br>\n"
-        + "      </div>\n"
-        + "    </div>\n"
-        + "    <div class=\"bottom\" id=\"bottom\">\n"
-        + "      <p>This material is supported in part by the National "
-          + "Science Foundation under Grant No. CNS-0959138. Any "
-          + "opinions, finding, and conclusions or recommendations "
-          + "expressed in this material are those of the author(s) and "
-          + "do not necessarily reflect the views of the National "
-          + "Science Foundation.</p>\n"
-        + "      <p>\"Tor\" and the \"Onion Logo\" are <a "
-          + "href=\"https://www.torproject.org/docs/trademark-faq.html.en\";>"
-          + "registered trademarks</a> of The Tor Project, Inc.</p>\n"
-        + "      <p>Data on this site is freely available under a <a "
-          + "href=\"http://creativecommons.org/publicdomain/zero/1.0/\";>"
-          + "CC0 no copyright declaration</a>: To the extent possible "
-          + "under law, the Tor Project has waived all copyright and "
-          + "related or neighboring rights in the data. Graphs are "
-          + "licensed under a <a "
-          + "href=\"http://creativecommons.org/licenses/by/3.0/us/\";>"
-          + "Creative Commons Attribution 3.0 United States "
-          + "License</a>.</p>\n"
-        + "    </div>\n"
-        + "  </body>\n"
-        + "</html>");
-    out.close();
-  }
-
-  public final String CONSENSUS_DIRECTORY =
-      "/srv/metrics.torproject.org/ernie/directory-archive/consensus";
-
   public void doGet(HttpServletRequest request,
       HttpServletResponse response) throws IOException,
       ServletException {
 
-    /* Measure how long it takes to process this request. */
-    long started = System.currentTimeMillis();
-
-    /* Get print writer and start writing response. We're wrapping the
-     * PrintWriter, because we want it to auto-flush as soon as we have
-     * written a line. */
-    PrintWriter out = new PrintWriter(response.getWriter(), true);
-    writeHeader(out);
-
-    /* Check if we have a database connection. */
+    /* Check if we have a database connection. If not, there's nothing we
+     * can do here. */
     if (conn == null) {
-      out.println("<p><font color=\"red\"><b>Warning: </b></font>This "
-          + "server doesn't have any relay lists available. If this "
-          + "problem persists, please "
-          + "<a href=\"mailto:tor-assistants@xxxxxxxxxxxxx\";>let us "
-          + "know</a>!</p>\n");
-      writeFooter(out);
+      request.setAttribute("noDbConn", "No database connection.");
+      request.getRequestDispatcher("WEB-INF/relay-search.jsp").forward(
+          request, response);
       return;
     }
 
-    /* Read search parameter, if any. */
-    String searchParameter = request.getParameter("search");
-    if (searchParameter == null) {
-      searchParameter = "";
-    }
+    /* We should be able to answer this request. Show the search form. */
+    request.setAttribute("showForm", "Show search form.");
 
-    /* Write search form. */
-    out.print("        <p>Search for a relay in the relay descriptor "
-          + "archive by typing (part of) a <b>nickname</b>, "
-          + "<b>fingerprint</b>, or <b>IP address</b> and optionally up "
-          + "to three <b>months (yyyy-mm)</b> or <b>days "
-          + "(yyyy-mm-dd)</b> in the following search field and "
-          + "clicking Search. The search will stop after 30 hits or, "
-          + "unless you provide a month or a day, after parsing the last "
-          + "30 days of relay lists.</p><br>\n"
-        + "        <form action=\"relay-search.html\">\n"
-        + "          <table>\n"
-        + "            <tr>\n"
-        + "              <td><input type=\"text\" name=\"search\""
-          + (searchParameter.length() > 0 ? " value=\"" + searchParameter
-          + "\"" : "") + "></td>\n"
-        + "              <td><input type=\"submit\" value=\"Search\">"
-          + "</td>\n"
-        + "            </tr>\n"
-        + "          </table>\n"
-        + "        </form>\n"
-        + "        <br>\n");
-
-    /* No search parameter? We're done here. */
-    if (searchParameter.length() == 0) {
-      writeFooter(out);
+    /* Read search parameter. If we don't have a search parameter, we're
+     * done here. */
+    String searchParameter = request.getParameter("search");
+    if (searchParameter == null || searchParameter.length() == 0) {
+      request.getRequestDispatcher("WEB-INF/relay-search.jsp").forward(
+          request, response);
       return;
     }
 
@@ -371,20 +257,15 @@ public class RelaySearchServlet extends HttpServlet {
       validQuery = false;
     }
 
-    /* If the query is invalid, print out a general warning. */
+    /* If the query is invalid, stop here. */
     if (!validQuery) {
-      out.write("        <p>Sorry, I didn't understand your query. "
-          + "Please provide a nickname (e.g., \"gabelmoo\"), at least "
-          + "the first 8 hex characters of a fingerprint (e.g., "
-          + "\"F2044413\"), or at least the first two octets of an IPv4 "
-          + "address in dotted-decimal notation (e.g., \"80.190\"). You "
-          + "can also provide at most three months or days in ISO 8601 "
-          + "format (e.g., \"2010-09\" or \"2010-09-17\").</p>\n");
-      writeFooter(out);
+      request.setAttribute("invalidQuery", "Query is invalid.");
+      request.getRequestDispatcher("WEB-INF/relay-search.jsp").
+          forward(request, response);
       return;
     }
 
-    /* Print out what we're searching for. */
+    /* Prepare a string that says what we're searching for. */
     List<String> recognizedSearchTerms = new ArrayList<String>();
     if (searchNickname.length() > 0) {
       recognizedSearchTerms.add("nickname <b>" + searchNickname + "</b>");
@@ -408,45 +289,48 @@ public class RelaySearchServlet extends HttpServlet {
     for (String searchTerm : searchDays) {
       recognizedIntervals.add("on <b>" + searchTerm + "</b>");
     }
-    out.write("        <p>Searching for relays with ");
+    StringBuilder searchNoticeBuilder = new StringBuilder();
+    searchNoticeBuilder.append("Searching for relays with ");
     if (recognizedSearchTerms.size() == 1) {
-      out.write(recognizedSearchTerms.get(0));
+      searchNoticeBuilder.append(recognizedSearchTerms.get(0));
     } else if (recognizedSearchTerms.size() == 2) {
-      out.write(recognizedSearchTerms.get(0) + " and "
+      searchNoticeBuilder.append(recognizedSearchTerms.get(0) + " and "
           + recognizedSearchTerms.get(1));
     } else {
       for (int i = 0; i < recognizedSearchTerms.size() - 1; i++) {
-        out.write(recognizedSearchTerms.get(i) + ", ");
+        searchNoticeBuilder.append(recognizedSearchTerms.get(i) + ", ");
       }
-      out.write("and " + recognizedSearchTerms.get(
+      searchNoticeBuilder.append("and " + recognizedSearchTerms.get(
           recognizedSearchTerms.size() - 1));
     }
     if (recognizedIntervals.size() == 1) {
-      out.write(" running " + recognizedIntervals.get(0));
+      searchNoticeBuilder.append(" running "
+          + recognizedIntervals.get(0));
     } else if (recognizedIntervals.size() == 2) {
-      out.write(" running " + recognizedIntervals.get(0) + " and/or "
-          + recognizedIntervals.get(1));
+      searchNoticeBuilder.append(" running " + recognizedIntervals.get(0)
+          + " and/or " + recognizedIntervals.get(1));
     } else if (recognizedIntervals.size() > 2) {
-      out.write(" running ");
+      searchNoticeBuilder.append(" running ");
       for (int i = 0; i < recognizedIntervals.size() - 1; i++) {
-        out.write(recognizedIntervals.get(i) + ", ");
+      searchNoticeBuilder.append(recognizedIntervals.get(i) + ", ");
       }
-      out.write("and/or " + recognizedIntervals.get(
+      searchNoticeBuilder.append("and/or " + recognizedIntervals.get(
           recognizedIntervals.size() - 1));
     }
-    out.write(" ...</p>\n");
-    out.flush();
+    searchNoticeBuilder.append(" ...");
+    String searchNotice = searchNoticeBuilder.toString();
+    request.setAttribute("searchNotice", searchNotice);
 
-    /* Search relays in the database. */
-    StringBuilder query = new StringBuilder("SELECT validafter, rawdesc "
+    /* Prepare the query string. */
+    StringBuilder queryBuilder = new StringBuilder();
+    queryBuilder.append("SELECT validafter, descriptor, rawdesc "
         + "FROM statusentry WHERE ");
-    boolean addAnd = false;
     if (searchDayTimestamps.size() > 0 ||
         searchMonthTimestamps.size() > 0) {
       boolean addOr = false;
-      query.append("(");
+      queryBuilder.append("(");
       for (long searchTimestamp : searchDayTimestamps) {
-        query.append((addOr ? "OR " : "") + "(validafter >= '"
+        queryBuilder.append((addOr ? "OR " : "") + "(validafter >= '"
             + dateTimeFormat.format(searchTimestamp) + "' AND "
             + "validafter < '" + dateTimeFormat.format(searchTimestamp
             + 24L * 60L * 60L * 1000L) + "') ");
@@ -457,40 +341,49 @@ public class RelaySearchServlet extends HttpServlet {
             TimeZone.getTimeZone("UTC"));
         firstOfNextMonth.setTimeInMillis(searchTimestamp);
         firstOfNextMonth.add(Calendar.MONTH, 1);
-        query.append((addOr ? "OR " : "") + "(validafter >= '"
+        queryBuilder.append((addOr ? "OR " : "") + "(validafter >= '"
             + dateTimeFormat.format(searchTimestamp) + "' AND "
             + "validafter < '" + dateTimeFormat.format(
             firstOfNextMonth.getTimeInMillis()) + "') ");
         addOr = true;
       }
-      query.append(") ");
+      queryBuilder.append(") ");
     } else {
-      query.append("validafter >= '" + dateTimeFormat.format(
-          started - 30L * 24L * 60L * 60L * 1000L) + "' ");
+      queryBuilder.append("validafter >= '" + dateTimeFormat.format(
+          System.currentTimeMillis() - 30L * 24L * 60L * 60L * 1000L)
+          + "' ");
     }
     if (searchNickname.length() > 0) {
-      query.append("AND LOWER(nickname) LIKE '"
+      queryBuilder.append("AND LOWER(nickname) LIKE '"
           + searchNickname.toLowerCase() + "%' ");
     }
     if (searchFingerprint.length() > 0) {
-      query.append("AND fingerprint LIKE '"
+      queryBuilder.append("AND fingerprint LIKE '"
           + searchFingerprint.toLowerCase() + "%' ");
     }
     if (searchIPAddress.length() > 0) {
-      query.append("AND address LIKE '" + searchIPAddress + "%' ");
+      queryBuilder.append("AND address LIKE '" + searchIPAddress + "%' ");
     }
     for (String search : searchFingerprintOrNickname) {
-      query.append("AND (LOWER(nickname) LIKE '" + search.toLowerCase()
-          + "%' OR fingerprint LIKE '" + search.toLowerCase() + "%') ");
+      queryBuilder.append("AND (LOWER(nickname) LIKE '"
+          + search.toLowerCase() + "%' OR fingerprint LIKE '"
+          + search.toLowerCase() + "%') ");
     }
-    query.append("ORDER BY validafter DESC, fingerprint LIMIT 31");
-    out.println("<!-- " + query.toString() + " -->");
-    int matches = 0;
+    queryBuilder.append("ORDER BY validafter DESC, fingerprint LIMIT 31");
+    String query = queryBuilder.toString();
+    request.setAttribute("query", query);
+
+    /* Actually execute the query. */
     long startedQuery = System.currentTimeMillis();
+    SortedMap<String, SortedSet<String>> foundDescriptors =
+        new TreeMap<String, SortedSet<String>>();
+    Map<String, String> rawValidAfterLines =
+        new HashMap<String, String>();
+    Map<String, String> rawStatusEntries = new HashMap<String, String>();
+    int matches = 0;
     try {
       Statement statement = conn.createStatement();
-      ResultSet rs = statement.executeQuery(query.toString());
-      String lastValidAfter = null;
+      ResultSet rs = statement.executeQuery(query);
       while (rs.next()) {
         matches++;
         if (matches > 30) {
@@ -498,72 +391,66 @@ public class RelaySearchServlet extends HttpServlet {
         }
         String validAfter = rs.getTimestamp(1).toString().
             substring(0, 19);
-        if (!validAfter.equals(lastValidAfter)) {
-          out.println("        <br><tt>valid-after "
+        String descriptor = rs.getString(2);
+        if (!foundDescriptors.containsKey(validAfter)) {
+          foundDescriptors.put(validAfter, new TreeSet<String>());
+        }
+        foundDescriptors.get(validAfter).add(descriptor);
+        if (!rawValidAfterLines.containsKey(validAfter)) {
+          rawValidAfterLines.put(validAfter, "<tt>valid-after "
               + "<a href=\"consensus?valid-after="
               + validAfter.replaceAll(":", "-").replaceAll(" ", "-")
               + "\" target=\"_blank\">" + validAfter + "</a></tt><br>");
-          lastValidAfter = validAfter;
-          out.flush();
         }
-        byte[] rawStatusEntry = rs.getBytes(2);
-        try {
-          String statusEntryLines = new String(rawStatusEntry,
-              "US-ASCII");
+        if (!rawStatusEntries.containsKey(descriptor)) {
+          byte[] rawStatusEntry = rs.getBytes(3);
+          String statusEntryLines = null;
+          try {
+            statusEntryLines = new String(rawStatusEntry, "US-ASCII");
+          } catch (UnsupportedEncodingException e) {
+            /* This shouldn't happen, because we know that ASCII is
+             * supported. */
+          }
+          StringBuilder rawStatusEntryBuilder = new StringBuilder();
           String[] lines = statusEntryLines.split("\n");
           for (String line : lines) {
             if (line.startsWith("r ")) {
               String[] parts = line.split(" ");
-              String descriptor = String.format("%040x",
+              String descriptorBase64 = String.format("%040x",
                   new BigInteger(1, Base64.decodeBase64(parts[3]
                   + "==")));
-              out.println("    <tt>r " + parts[1] + " " + parts[2] + " "
-                  + "<a href=\"descriptor.html?desc-id=" + descriptor
-                  + "\" target=\"_blank\">" + parts[3] + "</a> "
-                  + parts[4] + " " + parts[5] + " " + parts[6] + " "
-                  + parts[7] + " " + parts[8] + "</tt><br>");
+              rawStatusEntryBuilder.append("<tt>r " + parts[1] + " "
+                  + parts[2] + " <a href=\"descriptor.html?desc-id="
+                  + descriptorBase64 + "\" target=\"_blank\">" + parts[3]
+                  + "</a> " + parts[4] + " " + parts[5] + " " + parts[6]
+                  + " " + parts[7] + " " + parts[8] + "</tt><br>");
             } else {
-              out.println("    <tt>" + line + "</tt><br>");
+              rawStatusEntryBuilder.append("<tt>" + line + "</tt><br>");
             }
+            rawStatusEntries.put(descriptor,
+                rawStatusEntryBuilder.toString());
           }
-          out.println("    <br>");
-          out.flush();
-        } catch (UnsupportedEncodingException e) {
-          /* This shouldn't happen, because we know that ASCII is
-           * supported. */
         }
       }
       statement.close();
     } catch (SQLException e) {
-      out.println("<p><font color=\"red\"><b>Warning: </b></font>We "
-          + "experienced an unknown database problem while running the "
-          + "search. The query was '" + query + "'. If this problem "
-          + "persists, please "
-          + "<a href=\"mailto:tor-assistants@xxxxxxxxxxxxx\";>let us "
-          + "know</a>!</p>\n");
-      writeFooter(out);
-      return;
-    }
 
-    /* Display total search time on the results page. */
-    long searchTime = System.currentTimeMillis() - started;
-    long queryTime = System.currentTimeMillis() - startedQuery;
-    out.write("        <br><p>Found " + (matches > 30 ? "more than 30"
-        : "" + matches) + " relays " + (matches > 30 ?
-        "(displaying only the first 30 hits) " : "") + "in "
-        + String.format("%d.%03d", searchTime / 1000, searchTime % 1000)
-        + " seconds.</p>\n");
-    if (searchTime > 10L * 1000L) {
-      out.write("        <p>In theory, search time should not exceed "
-          + "10 seconds. The query was '" + query + "'. If this or "
-          + "similar searches remain slow, please "
-          + "<a href=\"mailto:tor-assistants@xxxxxxxxxxxxx\";>let us "
-          + "know</a>!</p>\n");
+      /* Tell the user we have a database problem. */
+      request.setAttribute("dbProblem", "Database problem.");
+      request.getRequestDispatcher("WEB-INF/relay-search.jsp").forward(
+          request, response);
+      return;
     }
-
-    /* Finish writing response. */
-    writeFooter(out);
-    return;
+    request.setAttribute("queryTime", System.currentTimeMillis()
+        - startedQuery);
+    request.setAttribute("foundDescriptors", foundDescriptors);
+    request.setAttribute("rawValidAfterLines", rawValidAfterLines);
+    request.setAttribute("rawStatusEntries", rawStatusEntries);
+    request.setAttribute("matches", matches);
+
+    /* We're done. Let the JSP do the rest. */
+    request.getRequestDispatcher("WEB-INF/relay-search.jsp").forward(
+        request, response);
   }
 }
 
diff --git a/web/WEB-INF/relay-search.jsp b/web/WEB-INF/relay-search.jsp
new file mode 100644
index 0000000..c160a43
--- /dev/null
+++ b/web/WEB-INF/relay-search.jsp
@@ -0,0 +1,96 @@
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"; %>
+<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"; %>
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
+<html>
+<head>
+  <title>Tor Metrics Portal: Relay Search</title>
+  <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1">
+  <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet">
+  <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon">
+</head>
+<body>
+  <div class="center">
+    <%@ include file="banner.jsp"%>
+    <div class="main-column">
+      <c:if test="${not empty noDbConn}">
+        <p><font color="red"><b>Warning: </b></font>This server doesn't
+        have any relay lists available. If this problem persists, please
+        <a href="mailto:tor-assistants@xxxxxxxxxxxxx";>let us
+        know</a>!</p>
+      </c:if>
+      <c:if test="${not empty showForm}">
+        <h2>Tor Metrics Portal: Relay Search</h2>
+        <p>Search for a relay in the relay descriptor archive by typing
+        (part of) a <b>nickname</b>, <b>fingerprint</b>, or <b>IP
+        address</b> and optionally up to three <b>months (yyyy-mm)</b> or
+        <b>days (yyyy-mm-dd)</b> in the following search field and
+        clicking Search. The search will stop after 30 hits or, unless you
+        provide a month or a day, after parsing the last 30 days of relay
+        lists.</p>
+        <br>
+        <form action="relay-search.html">
+          <table>
+            <tr>
+              <td><input type="text" name="search"
+                         value="<c:out value="${param.search}"/>"></td>
+              <td><input type="submit" value="Search"></td>
+            </tr>
+          </table>
+        </form>
+        <br>
+      </c:if>
+      <c:if test="${not empty invalidQuery}">
+        <p>Sorry, I didn't understand your query. Please provide a
+        nickname (e.g., "gabelmoo"), at least the first 8 hex characters
+        of a fingerprint (e.g., "F2044413"), or at least the first two
+        octets of an IPv4 address in dotted-decimal notation (e.g.,
+        "80.190"). You can also provide at most three months or days in
+        ISO 8601 format (e.g., "2010-09" or "2010-09-17").</p>
+      </c:if>
+      <c:if test="${not empty searchNotice}">
+        <p>${searchNotice}</p>
+      </c:if>
+      <c:if test="${not empty query}">
+        <!-- ${query} -->
+      </c:if>
+
+      <c:if test="${not empty dbProblem}">
+        <p><font color="red"><b>Warning: </b></font>We experienced an
+        unknown database problem while running the search. The query was
+        '${query}'. If this problem persists, please
+        <a href="mailto:tor-assistants@xxxxxxxxxxxxx";>let us
+        know</a>!</p>
+      </c:if>
+      <c:if test="${not empty queryTime}">
+        <c:forEach var="consensus" items="${foundDescriptors}">
+          ${rawValidAfterLines[consensus.key]}
+          <c:forEach var="statusentry" items="${consensus.value}">
+            ${rawStatusEntries[statusentry]}
+          </c:forEach>
+          <br>
+        </c:forEach>
+        <p>Found
+        <c:choose>
+          <c:when test="${matches > 30}">
+            more than 30 relays (displaying only the first 30 hits)
+          </c:when>
+          <c:otherwise>
+            ${matches} relays
+          </c:otherwise>
+        </c:choose>
+        in <fmt:formatNumber value="${queryTime / 1000}" pattern="#.###"/>
+        seconds.</p>
+        <c:if test="${queryTime > 10000}">
+          <p>In theory, search time should not exceed 10 seconds. The
+          query was '${query}'. If this or similar searches remain slow,
+          please <a href="mailto:tor-assistants@xxxxxxxxxxxxx";>let us
+          know</a>!</p>
+        </c:if>
+      </c:if>
+  </div>
+</div>
+<div class="bottom" id="bottom">
+  <%@ include file="footer.jsp"%>
+</div>
+</body>
+</html>
-- 
1.7.1