Skip to content

Session 1

Basics of Servlet

Updated View as Markdown

A servlet is a Java class that answers HTTP requests inside a container such as Tomcat. This session covers the request and response objects, HTML forms, client information, session tracking and a first database-backed CRUD application.

Objectives

Do not copy. Read for understanding and the viva
  • Complete questions 1 to 5 of the manual: basics of servlet
  • Prepare the deliverable before the lab and finish it during the session
  • Be ready to explain every step in the viva

Questions Covered

Do not copy. Read for understanding and the viva
QuestionRequirementStatus
Q1Write a Servlet Programme to print the current date and time along with the timestampComplete
Q2Create an HTML form with the input of student information using HTTP Protocol and…Complete
Q3Write a servlet program to capture client IPs and display itComplete
Q4Write a servlet program for session management using HTTP Session along with tracking…Complete
Q5Write a CRUD (Create/Save, Read, Edit/Update, Delete) application using servlet…Complete

Preparation

Do not copy. Read for understanding and the viva
  • Create a Dynamic Web Project (Eclipse) or Java Web application (NetBeans) targeting Tomcat 10; note that Tomcat 10 uses the jakarta.servlet package, not javax.servlet.
  • Design the IGNOU database and the Student table on paper first: enrolment number, name, date of birth, email, mobile, address, programme, courses. Question 5 and most later sessions reuse it.
  • Add the MySQL Connector/J jar to the project’s library path.
  • One Maven web project called ServletLab holds all five answers. Every servlet is mapped with @WebServlet, so web.xml only carries the welcome file and the session timeout. All Java files live in src/main/java/ignou/, static pages in src/main/webapp/.

Project Setup

Do not copy. Read for understanding and the viva

Do this once; every question below drops files into the same project.

  1. NetBeans: File, New Project, Java with Maven, Web Application, Next. Project Name ServletLab, Next. Server: Apache Tomcat 10.1 (click Add if it is not listed and browse to the extracted Tomcat folder, as in manual figure 2.24), Java EE Version: Jakarta EE 10 Web, Finish.
  2. Eclipse: File, New, Dynamic Web Project, name ServletLab, Target runtime: Apache Tomcat v10.1, Dynamic web module version 6.0, Finish. Then right-click the project, Configure, Convert to Maven Project.
  3. Replace the generated pom.xml dependencies with the fragment below and save; the IDE downloads the jars.
  4. Put web.xml in src/main/webapp/WEB-INF/.
  5. Run: right-click the project, Run (NetBeans) or Run As, Run on Server (Eclipse). The base URL is http://localhost:8080/ServletLab/.

Lab record: every tab is one file of the answer. Write all of them.

pom.xmlxml
<?xml version="1.0" encoding="UTF-8"?>
<!-- Dependency fragment for a Maven WAR project on Tomcat 10.1 / JDK 17.
     NetBeans: File > New Project > Java with Maven > Web Application creates
     the rest of this file; paste the <dependencies> block in.
     Eclipse: right-click the Dynamic Web Project > Configure > Convert to
     Maven Project, then paste the same block. -->
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>ignou</groupId>
  <artifactId>ServletLab</artifactId>
  <version>1.0</version>
  <packaging>war</packaging>

  <properties>
    <maven.compiler.source>17</maven.compiler.source>
    <maven.compiler.target>17</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <failOnMissingWebXml>false</failOnMissingWebXml>
  </properties>

  <dependencies>
    <!-- Servlet 6.0 API: Tomcat 10.1 already ships it, so scope is provided -->
    <dependency>
      <groupId>jakarta.servlet</groupId>
      <artifactId>jakarta.servlet-api</artifactId>
      <version>6.0.0</version>
      <scope>provided</scope>
    </dependency>
    <!-- JSTL 3.0 (Session 2): API plus the Glassfish implementation, packed in the WAR -->
    <dependency>
      <groupId>jakarta.servlet.jsp.jstl</groupId>
      <artifactId>jakarta.servlet.jsp.jstl-api</artifactId>
      <version>3.0.0</version>
    </dependency>
    <dependency>
      <groupId>org.glassfish.web</groupId>
      <artifactId>jakarta.servlet.jsp.jstl</artifactId>
      <version>3.0.1</version>
    </dependency>
    <!-- MySQL JDBC driver, packed in the WAR -->
    <dependency>
      <groupId>com.mysql</groupId>
      <artifactId>mysql-connector-j</artifactId>
      <version>8.4.0</version>
    </dependency>
  </dependencies>

  <build>
    <finalName>ServletLab</finalName>
  </build>
</project>
web.xmlxml
<?xml version="1.0" encoding="UTF-8"?>
<!-- src/main/webapp/WEB-INF/web.xml
     Servlet URLs are mapped with @WebServlet annotations in each class, so this
     file only holds container-wide settings. -->
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"
         version="6.0">

  <display-name>ServletLab</display-name>

  <welcome-file-list>
    <welcome-file>student-form.html</welcome-file>
  </welcome-file-list>

  <session-config>
    <session-timeout>30</session-timeout>
  </session-config>
</web-app>

Question 1

Problem Statement

Write in lab record

Write a Servlet Programme to print the current date and time along with the timestamp.

Solution

Write in lab record

Steps

  1. Right-click Source Packages, New, Servlet (NetBeans) or New, Servlet (Eclipse). Class name DateTimeServlet, package ignou. Untick “Add information to deployment descriptor” so the annotation does the mapping.
  2. Replace the generated body with the listing.
  3. Run the project and open http://localhost:8080/ServletLab/datetime.
  4. Press F5 a few times: time and timestamp change, the date does not.

Program

DateTimeServlet.javajava
package ignou;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.io.PrintWriter;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Date;

/** Q1: current date, time and timestamp. URL: /datetime */
@WebServlet("/datetime")
public class DateTimeServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        response.setContentType("text/html;charset=UTF-8");

        long millis = System.currentTimeMillis();
        LocalDateTime now = LocalDateTime.now();
        DateTimeFormatter dateFmt = DateTimeFormatter.ofPattern("dd-MM-yyyy");
        DateTimeFormatter timeFmt = DateTimeFormatter.ofPattern("HH:mm:ss");

        try (PrintWriter out = response.getWriter()) {
            out.println("<!DOCTYPE html><html><head><title>Date and Time</title></head><body>");
            out.println("<h2>Current Date and Time</h2>");
            out.println("<p>Date: " + now.format(dateFmt) + "</p>");
            out.println("<p>Time: " + now.format(timeFmt) + "</p>");
            out.println("<p>Full (java.util.Date): " + new Date(millis) + "</p>");
            out.println("<p>Timestamp (milliseconds since 1 Jan 1970 UTC): " + millis + "</p>");
            out.println("<p>SQL Timestamp: " + new Timestamp(millis) + "</p>");
            out.println("</body></html>");
        }
    }
}

Output

Expected browser page (values for the moment of the request; checked by reading the listing, not executed, since no Tomcat is available here):

Current Date and Time
Date: 26-09-2026
Time: 10:42:07
Full (java.util.Date): Sat Sep 26 10:42:07 IST 2026
Timestamp (milliseconds since 1 Jan 1970 UTC): 1790397127342
SQL Timestamp: 2026-09-26 10:42:07.342

Explanation

  • @WebServlet("/datetime") registers the servlet with the container; no servlet and servlet-mapping entries are needed in web.xml. The manual’s examples use web.xml; both work on Tomcat 10.
  • doGet runs once per GET request. response.setContentType must come before getWriter() or the charset is ignored.
  • System.currentTimeMillis() is the timestamp: milliseconds since the Unix epoch. LocalDateTime and DateTimeFormatter (java.time) format the same instant as a readable date and time; java.sql.Timestamp is the form a database column would store.
  • The try with resources closes the PrintWriter, which flushes the buffer to the client.

Question 2

Problem Statement

Write in lab record

Create an HTML form with the input of student information using HTTP Protocol and method, then display the input information using Servlet.

Solution

Write in lab record

Steps

  1. Add student-form.html under src/main/webapp/ (NetBeans: right-click Web Pages, New, HTML File).
  2. Add servlet StudentInfoServlet in package ignou.
  3. Run and open http://localhost:8080/ServletLab/student-form.html (it is also the welcome file, so the bare context URL works).
  4. Fill the form and press Submit. Then change method="post" to method="get" in the HTML, redeploy, submit again and compare the address bar.

Program

Lab record: every tab is one file of the answer. Write all of them.

student-form.htmlhtml
<!DOCTYPE html>
<!-- src/main/webapp/student-form.html : Q2 input form.
     method="post" sends the fields in the request body over HTTP;
     change to method="get" once to see them appear in the URL instead. -->
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Student Information Form</title>
</head>
<body>
  <h2>Student Information</h2>
  <form action="studentInfo" method="post">
    <p><label>Enrolment No: <input type="text" name="enrolmentNo" required></label></p>
    <p><label>Name: <input type="text" name="name" required></label></p>
    <p><label>Date of Birth: <input type="date" name="dob"></label></p>
    <p><label>Email: <input type="email" name="email"></label></p>
    <p><label>Mobile: <input type="tel" name="mobile" pattern="[0-9]{10}"></label></p>
    <p><label>Programme:
      <select name="programme">
        <option>MCA</option>
        <option>BCA</option>
        <option>MSc</option>
      </select></label></p>
    <p>Courses:
      <label><input type="checkbox" name="courses" value="MCS-218"> MCS-218</label>
      <label><input type="checkbox" name="courses" value="MCS-219"> MCS-219</label>
      <label><input type="checkbox" name="courses" value="MCS-220"> MCS-220</label>
      <label><input type="checkbox" name="courses" value="MCS-221"> MCS-221</label>
    </p>
    <p><button type="submit">Submit</button> <button type="reset">Reset</button></p>
  </form>
</body>
</html>
StudentInfoServlet.javajava
package ignou;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.io.PrintWriter;

/** Q2: reads the fields posted by student-form.html and echoes them. URL: /studentInfo */
@WebServlet("/studentInfo")
public class StudentInfoServlet extends HttpServlet {

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        show(request, response);
    }

    /** Also answer GET so the form can be switched to method="get" for comparison. */
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        show(request, response);
    }

    private void show(HttpServletRequest request, HttpServletResponse response)
            throws IOException {
        request.setCharacterEncoding("UTF-8");
        response.setContentType("text/html;charset=UTF-8");

        String[] courses = request.getParameterValues("courses");
        String courseList = courses == null ? "none" : String.join(", ", courses);

        try (PrintWriter out = response.getWriter()) {
            out.println("<!DOCTYPE html><html><head><title>Student Details</title></head><body>");
            out.println("<h2>Submitted Student Information</h2>");
            out.println("<p>HTTP method: " + request.getMethod()
                    + ", protocol: " + request.getProtocol()
                    + ", content type: " + request.getContentType() + "</p>");
            out.println("<table border='1' cellpadding='4'>");
            row(out, "Enrolment No", request.getParameter("enrolmentNo"));
            row(out, "Name", request.getParameter("name"));
            row(out, "Date of Birth", request.getParameter("dob"));
            row(out, "Email", request.getParameter("email"));
            row(out, "Mobile", request.getParameter("mobile"));
            row(out, "Programme", request.getParameter("programme"));
            row(out, "Courses", courseList);
            out.println("</table>");
            out.println("<p><a href='student-form.html'>Back to form</a></p>");
            out.println("</body></html>");
        }
    }

    private static void row(PrintWriter out, String label, String value) {
        out.println("<tr><th align='left'>" + label + "</th><td>" + esc(value) + "</td></tr>");
    }

    /** Escape user input before writing it back into HTML (stops script injection). */
    private static String esc(String s) {
        if (s == null) return "";
        return s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;");
    }
}

Output

Checked by reading, not executed. With POST the address bar shows /ServletLab/studentInfo and the page reads:

Submitted Student Information
HTTP method: POST, protocol: HTTP/1.1, content type: application/x-www-form-urlencoded
Enrolment No   2451001234
Name           Asha Verma
Date of Birth  2001-03-14
Email          asha.verma@example.com
Mobile         9876543210
Programme      MCA
Courses        MCS-218, MCS-220
Back to form

With GET the first line becomes HTTP method: GET, protocol: HTTP/1.1, content type: null and the address bar carries every field: studentInfo?enrolmentNo=2451001234&name=Asha+Verma&....

Explanation

  • The form action="studentInfo" is relative, so the browser resolves it against /ServletLab/; the servlet is mapped to /studentInfo.
  • request.getParameter(name) returns one value; getParameterValues("courses") returns all ticked checkboxes as an array, or null when none is ticked.
  • request.getMethod() and getProtocol() show the HTTP method and version, which is what the question means by “HTTP Protocol and method”. GET puts the fields in the query string (visible, bookmarkable, length-limited); POST puts them in the body, so passwords and long text belong in POST.
  • esc() HTML-escapes the values before they are written back. Without it a name like <script> would run in the browser.
  • setCharacterEncoding("UTF-8") before the first getParameter call makes Hindi or other non-ASCII names decode correctly.

Question 3

Problem Statement

Write in lab record

Write a servlet program to capture client IPs and display it.

Solution

Write in lab record

Steps

  1. Add servlet ClientIpServlet in package ignou.
  2. Run and open http://localhost:8080/ServletLab/clientIp from the same machine, then http://127.0.0.1:8080/ServletLab/clientIp, then from a phone on the same Wi-Fi using the PC’s LAN address (find it with ipconfig or ip addr).

Program

ClientIpServlet.javajava
package ignou;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.io.PrintWriter;

/** Q3: shows the client's IP address and related request details. URL: /clientIp */
@WebServlet("/clientIp")
public class ClientIpServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        response.setContentType("text/html;charset=UTF-8");

        // Behind a proxy or load balancer the real client IP arrives in this header;
        // on a direct localhost connection it is null.
        String forwarded = request.getHeader("X-Forwarded-For");
        String clientIp = forwarded != null ? forwarded.split(",")[0].trim() : request.getRemoteAddr();

        try (PrintWriter out = response.getWriter()) {
            out.println("<!DOCTYPE html><html><head><title>Client IP</title></head><body>");
            out.println("<h2>Client Information</h2>");
            out.println("<p>Client IP address: <b>" + clientIp + "</b></p>");
            out.println("<p>request.getRemoteAddr(): " + request.getRemoteAddr() + "</p>");
            out.println("<p>request.getRemoteHost(): " + request.getRemoteHost() + "</p>");
            out.println("<p>request.getRemotePort(): " + request.getRemotePort() + "</p>");
            out.println("<p>X-Forwarded-For header: " + forwarded + "</p>");
            out.println("<p>Server name and port: " + request.getServerName() + ":" + request.getServerPort() + "</p>");
            out.println("<p>User-Agent: " + request.getHeader("User-Agent") + "</p>");
            out.println("</body></html>");
        }
    }
}

Output

Checked by reading. From the same PC using localhost:

Client Information
Client IP address: 0:0:0:0:0:0:0:1
request.getRemoteAddr(): 0:0:0:0:0:0:0:1
request.getRemoteHost(): 0:0:0:0:0:0:0:1
request.getRemotePort(): 53412
X-Forwarded-For header: null
Server name and port: localhost:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) ... Chrome/129.0

Using 127.0.0.1 in the URL the first two lines show 127.0.0.1; from a phone they show something like 192.168.1.7.

Explanation

  • getRemoteAddr() is the address of the TCP peer that connected to Tomcat. On localhost that is the IPv6 loopback 0:0:0:0:0:0:0:1 on most Windows builds, because the browser prefers IPv6 for localhost.
  • When a reverse proxy or load balancer sits in front of Tomcat, the peer is the proxy, and the original client is in the X-Forwarded-For header (a comma list, first entry is the client). The servlet prefers that header when present. Trust it only when you control the proxy; anyone can send that header.
  • getRemoteHost() returns a host name only if Tomcat’s enableLookups is on; by default it just repeats the IP.
  • getRemotePort() is the client’s ephemeral port, different for every connection.

Question 4

Problem Statement

Write in lab record

Write a servlet program for session management using HTTP Session along with tracking and also use a cookie for session tracking.

Solution

Write in lab record

Steps

  1. Add servlet SessionTrackingServlet in package ignou.
  2. Run and open http://localhost:8080/ServletLab/session. Reload three times and watch “Visits in this session” count up.
  3. Type a name, click “Remember me”. The name appears in the heading and in the visitorName row.
  4. Open the browser dev tools, Application, Cookies: you see JSESSIONID (session cookie, no expiry) and visitorName (expires in 7 days).
  5. Click Logout: the visit count restarts at 1, the name is gone, and the Session ID is new.
  6. Block cookies for localhost in the browser and reload twice: “Session id came from cookie?” turns false, and the “Reload” link now contains ;jsessionid=... because encodeURL switched to URL rewriting.

Program

SessionTrackingServlet.javajava
package ignou;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.Date;

/**
 * Q4: session management with HttpSession plus a cookie that survives the session.
 * URL: /session          shows counters and session details
 * URL: /session?action=logout  invalidates the session and deletes the cookie
 */
@WebServlet("/session")
public class SessionTrackingServlet extends HttpServlet {

    private static final String NAME_COOKIE = "visitorName";

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        if ("logout".equals(request.getParameter("action"))) {
            HttpSession old = request.getSession(false);
            if (old != null) old.invalidate();
            Cookie gone = new Cookie(NAME_COOKIE, "");
            gone.setMaxAge(0);                       // max age 0 tells the browser to delete it
            gone.setPath(request.getContextPath());
            response.addCookie(gone);
            response.sendRedirect(request.getContextPath() + "/session");
            return;
        }

        HttpSession session = request.getSession();  // creates one on the first visit
        Integer visits = (Integer) session.getAttribute("visits");
        visits = visits == null ? 1 : visits + 1;
        session.setAttribute("visits", visits);

        String name = readCookie(request, NAME_COOKIE);

        response.setContentType("text/html;charset=UTF-8");
        try (PrintWriter out = response.getWriter()) {
            out.println("<!DOCTYPE html><html><head><title>Session Tracking</title></head><body>");
            out.println("<h2>Welcome " + (name == null ? "guest" : esc(name)) + "</h2>");
            out.println("<table border='1' cellpadding='4'>");
            out.println("<tr><th align='left'>Session ID</th><td>" + session.getId() + "</td></tr>");
            out.println("<tr><th align='left'>New session?</th><td>" + session.isNew() + "</td></tr>");
            out.println("<tr><th align='left'>Created</th><td>" + new Date(session.getCreationTime()) + "</td></tr>");
            out.println("<tr><th align='left'>Last accessed</th><td>" + new Date(session.getLastAccessedTime()) + "</td></tr>");
            out.println("<tr><th align='left'>Timeout (s)</th><td>" + session.getMaxInactiveInterval() + "</td></tr>");
            out.println("<tr><th align='left'>Visits in this session</th><td>" + visits + "</td></tr>");
            out.println("<tr><th align='left'>Session id came from cookie?</th><td>"
                    + request.isRequestedSessionIdFromCookie() + "</td></tr>");
            out.println("<tr><th align='left'>Session id came from URL?</th><td>"
                    + request.isRequestedSessionIdFromURL() + "</td></tr>");
            out.println("<tr><th align='left'>visitorName cookie</th><td>" + esc(name) + "</td></tr>");
            out.println("</table>");

            out.println("<form method='post'><p>Your name: <input name='name' required> "
                    + "<button type='submit'>Remember me (cookie, 7 days)</button></p></form>");
            // encodeURL appends ;jsessionid=... only when the browser refused the session cookie
            out.println("<p><a href='" + response.encodeURL("session") + "'>Reload (URL rewriting aware)</a> | "
                    + "<a href='session?action=logout'>Logout</a></p>");
            out.println("</body></html>");
        }
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        request.setCharacterEncoding("UTF-8");
        String name = request.getParameter("name");
        Cookie c = new Cookie(NAME_COOKIE, java.net.URLEncoder.encode(name, "UTF-8"));
        c.setMaxAge(7 * 24 * 60 * 60);
        c.setPath(request.getContextPath());
        c.setHttpOnly(true);
        response.addCookie(c);
        request.getSession().setAttribute("name", name);
        response.sendRedirect(request.getContextPath() + "/session");
    }

    private static String readCookie(HttpServletRequest request, String key) throws IOException {
        Cookie[] cookies = request.getCookies();
        if (cookies == null) return null;
        for (Cookie c : cookies) {
            if (key.equals(c.getName())) return java.net.URLDecoder.decode(c.getValue(), "UTF-8");
        }
        return null;
    }

    private static String esc(String s) {
        if (s == null) return "";
        return s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;");
    }
}

Output

Checked by reading. After two reloads and “Remember me” with the name Asha:

Welcome Asha
Session ID                         5F3A0C1E9B7D4E2A8C6F1B0D9E8A7C5B
New session?                       false
Created                            Sat Sep 26 10:50:02 IST 2026
Last accessed                      Sat Sep 26 10:50:41 IST 2026
Timeout (s)                        1800
Visits in this session             3
Session id came from cookie?       true
Session id came from URL?          false
visitorName cookie                 Asha
Your name: [        ] [Remember me (cookie, 7 days)]
Reload (URL rewriting aware) | Logout

Explanation

  • HTTP is stateless. request.getSession() makes Tomcat create an HttpSession object on the server and send its id to the browser in the JSESSIONID cookie; every later request carries the cookie, so Tomcat finds the same object. The visits attribute lives in that object, not in the browser.
  • The session cookie has no expiry, so it dies when the browser closes; the session itself dies after 30 minutes of inactivity (session-timeout in web.xml, shown as 1800 seconds).
  • The visitorName cookie is the second tracking mechanism: it is stored by the browser with setMaxAge(7 days), so it survives browser restarts and even session.invalidate(). Logout deletes it by sending the same cookie with max age 0.
  • response.encodeURL() is the fallback when cookies are refused: it appends ;jsessionid= to links so the id travels in the URL. isRequestedSessionIdFromCookie() and isRequestedSessionIdFromURL() show which route was used.
  • Cookie values may not contain spaces or semicolons, so the name is URL-encoded before storing and decoded when read. setHttpOnly(true) keeps JavaScript from reading it.

Question 5

Problem Statement

Write in lab record

Write a CRUD (Create/Save, Read, Edit/Update, Delete) application using servlet. Create a Database named IGNOU, create a table named Student which must capture the student information (basics, contact, enrollment details along with courses). Make necessary assumptions required.

Solution

Write in lab record

Assumptions

  • Enrolment number is the primary key (IGNOU issues a unique 9 to 12 digit number).
  • Courses are stored in one column as a comma-separated list of course codes. A student registers for a handful of courses, so a separate Course table and join table would add two more screens without teaching anything new for this session. Session 6 (Hibernate) is the place to normalise it.
  • One application login ignou / ignou123 with only SELECT, INSERT, UPDATE and DELETE rights on the IGNOU database.
  • MySQL 8 runs on localhost:3306.

Steps

  1. Start MySQL and run schema.sql in MySQL Workbench (File, Open SQL Script, then the lightning-bolt Execute button) or with mysql -u root -p then source schema.sql. Check with SELECT * FROM Student; that three rows exist.
  2. Add Student.java, StudentDao.java and StudentServlet.java to package ignou. The mysql-connector-j dependency is already in pom.xml.
  3. Run and open http://localhost:8080/ServletLab/students.
  4. Read: the list shows the three seeded rows. Create: click “Add new student”, fill the form, Save. Update: click Edit on a row, change the mobile, Save. Delete: click Delete, confirm.
  5. Verify each step in Workbench with SELECT enrolment_no, name, mobile FROM Student;.

Program

Lab record: every tab is one file of the answer. Write all of them.

schema.sqlsql
-- Q5: IGNOU database and Student table.
-- Run in MySQL Workbench or:  mysql -u root -p < schema.sql
-- One table holds basics, contact and enrolment details; the courses column is a
-- comma-separated list of course codes (assumption: a student registers for at most
-- a handful of courses, so a separate Course table is not needed for this lab).

CREATE DATABASE IF NOT EXISTS IGNOU CHARACTER SET utf8mb4;
USE IGNOU;

DROP TABLE IF EXISTS Student;

CREATE TABLE Student (
    -- basics
    enrolment_no    VARCHAR(12)  NOT NULL,
    name            VARCHAR(80)  NOT NULL,
    dob             DATE         NOT NULL,
    gender          ENUM('M', 'F', 'O') NOT NULL DEFAULT 'M',
    -- contact
    email           VARCHAR(120) NOT NULL,
    mobile          CHAR(10)     NOT NULL,
    address         VARCHAR(200),
    city            VARCHAR(60),
    state           VARCHAR(60),
    pincode         CHAR(6),
    -- enrolment details
    programme       VARCHAR(10)  NOT NULL,          -- MCA, BCA, MSc ...
    semester        TINYINT      NOT NULL DEFAULT 1,
    admission_year  YEAR         NOT NULL,
    study_centre    VARCHAR(10),                    -- e.g. 0710
    courses         VARCHAR(200),                   -- 'MCS-218,MCS-219,MCS-220'
    PRIMARY KEY (enrolment_no),
    UNIQUE KEY uq_student_email (email),
    CONSTRAINT chk_mobile CHECK (mobile REGEXP '^[0-9]{10}$'),
    CONSTRAINT chk_semester CHECK (semester BETWEEN 1 AND 6)
);

-- Application login used by the servlet: create once, grant only what the lab needs.
CREATE USER IF NOT EXISTS 'ignou'@'localhost' IDENTIFIED BY 'ignou123';
GRANT SELECT, INSERT, UPDATE, DELETE ON IGNOU.* TO 'ignou'@'localhost';

INSERT INTO Student (enrolment_no, name, dob, gender, email, mobile, address, city, state, pincode,
                     programme, semester, admission_year, study_centre, courses) VALUES
('2451001234', 'Asha Verma',  '2001-03-14', 'F', 'asha.verma@example.com',  '9876543210', '12 MG Road', 'Jaipur',  'Rajasthan', '302001', 'MCA', 2, 2024, '0710', 'MCS-218,MCS-219,MCS-220,MCS-221'),
('2451001235', 'Rahul Singh', '2000-11-02', 'M', 'rahul.singh@example.com', '9123456780', '4 Park Street', 'Kolkata', 'West Bengal', '700016', 'MCA', 2, 2024, '2801', 'MCS-218,MCS-220,MCS-221'),
('2451001236', 'Meera Nair',  '2002-07-25', 'F', 'meera.nair@example.com',  '9988776655', '9 Beach Road', 'Kochi',   'Kerala', '682001', 'MCA', 1, 2025, '1401', 'MCS-211,MCS-212,MCS-213');

SELECT enrolment_no, name, programme, semester, courses FROM Student;
Student.javajava
package ignou;

/** One row of the Student table. Plain data holder used by the DAO, servlets and JSPs. */
public class Student {
    private String enrolmentNo;
    private String name;
    private String dob;          // yyyy-MM-dd, the format <input type="date"> and MySQL both use
    private String gender;
    private String email;
    private String mobile;
    private String address;
    private String city;
    private String state;
    private String pincode;
    private String programme;
    private int semester;
    private int admissionYear;
    private String studyCentre;
    private String courses;      // comma-separated course codes

    public String getEnrolmentNo() { return enrolmentNo; }
    public void setEnrolmentNo(String enrolmentNo) { this.enrolmentNo = enrolmentNo; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public String getDob() { return dob; }
    public void setDob(String dob) { this.dob = dob; }
    public String getGender() { return gender; }
    public void setGender(String gender) { this.gender = gender; }
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
    public String getMobile() { return mobile; }
    public void setMobile(String mobile) { this.mobile = mobile; }
    public String getAddress() { return address; }
    public void setAddress(String address) { this.address = address; }
    public String getCity() { return city; }
    public void setCity(String city) { this.city = city; }
    public String getState() { return state; }
    public void setState(String state) { this.state = state; }
    public String getPincode() { return pincode; }
    public void setPincode(String pincode) { this.pincode = pincode; }
    public String getProgramme() { return programme; }
    public void setProgramme(String programme) { this.programme = programme; }
    public int getSemester() { return semester; }
    public void setSemester(int semester) { this.semester = semester; }
    public int getAdmissionYear() { return admissionYear; }
    public void setAdmissionYear(int admissionYear) { this.admissionYear = admissionYear; }
    public String getStudyCentre() { return studyCentre; }
    public void setStudyCentre(String studyCentre) { this.studyCentre = studyCentre; }
    public String getCourses() { return courses; }
    public void setCourses(String courses) { this.courses = courses; }
}
StudentDao.javajava
package ignou;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

/**
 * Data access for the Student table (IGNOU database). Every query uses a
 * PreparedStatement, so values are sent separately from the SQL text and a
 * quote in a name cannot break or hijack the statement.
 */
public class StudentDao {

    private static final String URL =
            "jdbc:mysql://localhost:3306/IGNOU?useSSL=false&serverTimezone=Asia/Kolkata";
    private static final String USER = "ignou";
    private static final String PASSWORD = "ignou123";

    private static final String COLUMNS =
            "enrolment_no, name, dob, gender, email, mobile, address, city, state, pincode, "
          + "programme, semester, admission_year, study_centre, courses";

    // Connector/J 8 registers itself through META-INF/services; no Class.forName needed.
    private Connection connect() throws SQLException {
        return DriverManager.getConnection(URL, USER, PASSWORD);
    }

    public List<Student> findAll() throws SQLException {
        List<Student> list = new ArrayList<>();
        String sql = "SELECT " + COLUMNS + " FROM Student ORDER BY enrolment_no";
        try (Connection con = connect();
             PreparedStatement ps = con.prepareStatement(sql);
             ResultSet rs = ps.executeQuery()) {
            while (rs.next()) list.add(map(rs));
        }
        return list;
    }

    public Student findById(String enrolmentNo) throws SQLException {
        String sql = "SELECT " + COLUMNS + " FROM Student WHERE enrolment_no = ?";
        try (Connection con = connect();
             PreparedStatement ps = con.prepareStatement(sql)) {
            ps.setString(1, enrolmentNo);
            try (ResultSet rs = ps.executeQuery()) {
                return rs.next() ? map(rs) : null;
            }
        }
    }

    public int insert(Student s) throws SQLException {
        String sql = "INSERT INTO Student (" + COLUMNS + ") VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)";
        try (Connection con = connect();
             PreparedStatement ps = con.prepareStatement(sql)) {
            ps.setString(1, s.getEnrolmentNo());
            bind(ps, 2, s);
            return ps.executeUpdate();
        }
    }

    public int update(Student s) throws SQLException {
        String sql = "UPDATE Student SET name=?, dob=?, gender=?, email=?, mobile=?, address=?, "
                   + "city=?, state=?, pincode=?, programme=?, semester=?, admission_year=?, "
                   + "study_centre=?, courses=? WHERE enrolment_no=?";
        try (Connection con = connect();
             PreparedStatement ps = con.prepareStatement(sql)) {
            bind(ps, 1, s);
            ps.setString(15, s.getEnrolmentNo());
            return ps.executeUpdate();
        }
    }

    public int delete(String enrolmentNo) throws SQLException {
        try (Connection con = connect();
             PreparedStatement ps = con.prepareStatement("DELETE FROM Student WHERE enrolment_no = ?")) {
            ps.setString(1, enrolmentNo);
            return ps.executeUpdate();
        }
    }

    /** Binds the 14 non-key columns starting at parameter index {@code from}. */
    private static void bind(PreparedStatement ps, int from, Student s) throws SQLException {
        ps.setString(from, s.getName());
        ps.setString(from + 1, s.getDob());
        ps.setString(from + 2, s.getGender());
        ps.setString(from + 3, s.getEmail());
        ps.setString(from + 4, s.getMobile());
        ps.setString(from + 5, s.getAddress());
        ps.setString(from + 6, s.getCity());
        ps.setString(from + 7, s.getState());
        ps.setString(from + 8, s.getPincode());
        ps.setString(from + 9, s.getProgramme());
        ps.setInt(from + 10, s.getSemester());
        ps.setInt(from + 11, s.getAdmissionYear());
        ps.setString(from + 12, s.getStudyCentre());
        ps.setString(from + 13, s.getCourses());
    }

    private static Student map(ResultSet rs) throws SQLException {
        Student s = new Student();
        s.setEnrolmentNo(rs.getString("enrolment_no"));
        s.setName(rs.getString("name"));
        s.setDob(rs.getString("dob"));
        s.setGender(rs.getString("gender"));
        s.setEmail(rs.getString("email"));
        s.setMobile(rs.getString("mobile"));
        s.setAddress(rs.getString("address"));
        s.setCity(rs.getString("city"));
        s.setState(rs.getString("state"));
        s.setPincode(rs.getString("pincode"));
        s.setProgramme(rs.getString("programme"));
        s.setSemester(rs.getInt("semester"));
        s.setAdmissionYear(rs.getInt("admission_year"));
        s.setStudyCentre(rs.getString("study_centre"));
        s.setCourses(rs.getString("courses"));
        return s;
    }
}
StudentServlet.javajava
package ignou;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.io.PrintWriter;
import java.sql.SQLException;
import java.util.List;

/**
 * Q5: CRUD front controller for the Student table.
 *   GET  /students               list
 *   GET  /students?action=new    empty form
 *   GET  /students?action=edit&id=E  form filled from the database
 *   POST /students  action=insert | update | delete
 */
@WebServlet("/students")
public class StudentServlet extends HttpServlet {

    private final StudentDao dao = new StudentDao();

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        String action = request.getParameter("action");
        try {
            if ("new".equals(action)) {
                renderForm(response, new Student(), false);
            } else if ("edit".equals(action)) {
                Student s = dao.findById(request.getParameter("id"));
                if (s == null) {
                    response.sendError(HttpServletResponse.SC_NOT_FOUND, "No such student");
                    return;
                }
                renderForm(response, s, true);
            } else {
                renderList(response, dao.findAll(), request.getParameter("msg"));
            }
        } catch (SQLException e) {
            throw new ServletException("Database error: " + e.getMessage(), e);
        }
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        request.setCharacterEncoding("UTF-8");
        String action = request.getParameter("action");
        String msg;
        try {
            switch (action == null ? "" : action) {
                case "insert" -> { dao.insert(fromRequest(request)); msg = "Student added"; }
                case "update" -> { dao.update(fromRequest(request)); msg = "Student updated"; }
                case "delete" -> { dao.delete(request.getParameter("id")); msg = "Student deleted"; }
                default -> msg = "Unknown action";
            }
        } catch (SQLException e) {
            msg = "Database error: " + e.getMessage();   // e.g. duplicate enrolment number
        }
        // Redirect after POST so a browser refresh does not repeat the insert.
        response.sendRedirect("students?msg=" + java.net.URLEncoder.encode(msg, "UTF-8"));
    }

    private static Student fromRequest(HttpServletRequest r) {
        Student s = new Student();
        s.setEnrolmentNo(r.getParameter("enrolmentNo").trim());
        s.setName(r.getParameter("name").trim());
        s.setDob(r.getParameter("dob"));
        s.setGender(r.getParameter("gender"));
        s.setEmail(r.getParameter("email").trim());
        s.setMobile(r.getParameter("mobile").trim());
        s.setAddress(r.getParameter("address"));
        s.setCity(r.getParameter("city"));
        s.setState(r.getParameter("state"));
        s.setPincode(r.getParameter("pincode"));
        s.setProgramme(r.getParameter("programme"));
        s.setSemester(Integer.parseInt(r.getParameter("semester")));
        s.setAdmissionYear(Integer.parseInt(r.getParameter("admissionYear")));
        s.setStudyCentre(r.getParameter("studyCentre"));
        String[] courses = r.getParameterValues("courses");
        s.setCourses(courses == null ? "" : String.join(",", courses));
        return s;
    }

    private void renderList(HttpServletResponse response, List<Student> list, String msg) throws IOException {
        response.setContentType("text/html;charset=UTF-8");
        try (PrintWriter out = response.getWriter()) {
            out.println("<!DOCTYPE html><html><head><title>Students</title></head><body>");
            out.println("<h2>IGNOU Students (" + list.size() + ")</h2>");
            if (msg != null) out.println("<p style='color:green'>" + esc(msg) + "</p>");
            out.println("<p><a href='students?action=new'>Add new student</a></p>");
            out.println("<table border='1' cellpadding='4'><tr><th>Enrolment</th><th>Name</th><th>DOB</th>"
                    + "<th>Email</th><th>Mobile</th><th>Programme</th><th>Sem</th><th>Courses</th><th>Actions</th></tr>");
            for (Student s : list) {
                out.println("<tr><td>" + esc(s.getEnrolmentNo()) + "</td><td>" + esc(s.getName()) + "</td><td>"
                        + s.getDob() + "</td><td>" + esc(s.getEmail()) + "</td><td>" + esc(s.getMobile())
                        + "</td><td>" + esc(s.getProgramme()) + "</td><td>" + s.getSemester() + "</td><td>"
                        + esc(s.getCourses()) + "</td><td>"
                        + "<a href='students?action=edit&amp;id=" + esc(s.getEnrolmentNo()) + "'>Edit</a> "
                        + "<form method='post' style='display:inline' onsubmit='return confirm(\"Delete?\")'>"
                        + "<input type='hidden' name='action' value='delete'>"
                        + "<input type='hidden' name='id' value='" + esc(s.getEnrolmentNo()) + "'>"
                        + "<button type='submit'>Delete</button></form></td></tr>");
            }
            out.println("</table></body></html>");
        }
    }

    private void renderForm(HttpServletResponse response, Student s, boolean editing) throws IOException {
        response.setContentType("text/html;charset=UTF-8");
        String[] allCourses = {"MCS-218", "MCS-219", "MCS-220", "MCS-221", "MCSL-222", "MCSL-223"};
        String chosen = s.getCourses() == null ? "" : "," + s.getCourses() + ",";
        try (PrintWriter out = response.getWriter()) {
            out.println("<!DOCTYPE html><html><head><title>Student Form</title></head><body>");
            out.println("<h2>" + (editing ? "Edit" : "Add") + " Student</h2>");
            out.println("<form method='post' action='students'>");
            out.println("<input type='hidden' name='action' value='" + (editing ? "update" : "insert") + "'>");
            field(out, "Enrolment No", "enrolmentNo", s.getEnrolmentNo(), editing ? "readonly" : "required");
            field(out, "Name", "name", s.getName(), "required");
            out.println("<p><label>Date of Birth: <input type='date' name='dob' value='" + esc(s.getDob()) + "' required></label></p>");
            out.println("<p>Gender: <label><input type='radio' name='gender' value='M' " + ("F".equals(s.getGender()) || "O".equals(s.getGender()) ? "" : "checked") + "> Male</label> "
                    + "<label><input type='radio' name='gender' value='F' " + ("F".equals(s.getGender()) ? "checked" : "") + "> Female</label> "
                    + "<label><input type='radio' name='gender' value='O' " + ("O".equals(s.getGender()) ? "checked" : "") + "> Other</label></p>");
            field(out, "Email", "email", s.getEmail(), "type='email' required");
            field(out, "Mobile", "mobile", s.getMobile(), "pattern='[0-9]{10}' required");
            field(out, "Address", "address", s.getAddress(), "");
            field(out, "City", "city", s.getCity(), "");
            field(out, "State", "state", s.getState(), "");
            field(out, "Pincode", "pincode", s.getPincode(), "pattern='[0-9]{6}'");
            field(out, "Programme", "programme", s.getProgramme() == null ? "MCA" : s.getProgramme(), "required");
            field(out, "Semester", "semester", s.getSemester() == 0 ? "1" : String.valueOf(s.getSemester()), "type='number' min='1' max='6'");
            field(out, "Admission Year", "admissionYear", s.getAdmissionYear() == 0 ? "2024" : String.valueOf(s.getAdmissionYear()), "type='number' min='2000' max='2099'");
            field(out, "Study Centre", "studyCentre", s.getStudyCentre(), "");
            out.println("<p>Courses:");
            for (String c : allCourses) {
                out.println("<label><input type='checkbox' name='courses' value='" + c + "' "
                        + (chosen.contains("," + c + ",") ? "checked" : "") + "> " + c + "</label>");
            }
            out.println("</p><p><button type='submit'>Save</button> <a href='students'>Cancel</a></p>");
            out.println("</form></body></html>");
        }
    }

    private static void field(PrintWriter out, String label, String name, String value, String extra) {
        out.println("<p><label>" + label + ": <input name='" + name + "' value='" + esc(value) + "' " + extra + "></label></p>");
    }

    private static String esc(String s) {
        if (s == null) return "";
        return s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
                .replace("\"", "&quot;").replace("'", "&#39;");
    }
}

Output

Checked by reading; the SQL was checked against MySQL 8 syntax. After adding a fourth student the list page reads:

IGNOU Students (4)
Student added
Add new student
Enrolment    Name         DOB         Email                     Mobile      Programme  Sem  Courses                          Actions
2451001234   Asha Verma   2001-03-14  asha.verma@example.com    9876543210  MCA        2    MCS-218,MCS-219,MCS-220,MCS-221  Edit [Delete]
2451001235   Rahul Singh  2000-11-02  rahul.singh@example.com   9123456780  MCA        2    MCS-218,MCS-220,MCS-221          Edit [Delete]
2451001236   Meera Nair   2002-07-25  meera.nair@example.com    9988776655  MCA        1    MCS-211,MCS-212,MCS-213          Edit [Delete]
2451001237   Vikram Rao   2001-08-30  vikram.rao@example.com    9012345678  MCA        2    MCS-218,MCSL-222                 Edit [Delete]

Submitting the add form a second time with the same enrolment number shows, in green text at the top of the list, Database error: Duplicate entry '2451001237' for key 'student.PRIMARY'. After Delete the count drops to 3 and the message reads Student deleted.

Explanation

  • Three layers: Student (data), StudentDao (SQL), StudentServlet (HTTP and HTML). The servlet never builds SQL; the DAO never touches the request. That is the split every later session (Spring, Hibernate) formalises.
  • PreparedStatement sends the SQL text with ? placeholders first and the values separately, so a name containing a quote cannot change the statement (SQL injection). setString, setInt also handle quoting and type conversion.
  • Every DAO method opens and closes its own connection through try with resources. That is fine for a lab; a real application uses a connection pool (Tomcat’s JNDI DataSource).
  • The servlet is a front controller: the action parameter selects list, new, edit, insert, update or delete. Reads are GET; writes are POST, and every POST ends in sendRedirect back to the list (Post-Redirect-Get), so pressing F5 on the list page never re-inserts a row.
  • Delete is a tiny POST form rather than a link, because browsers and crawlers prefetch links; a GET that deletes data is a classic bug.
  • The CHECK constraints in schema.sql catch bad mobile numbers and semesters even if someone bypasses the HTML pattern attributes; the database is the last line of validation.

Viva Questions

Do not copy. Read for understanding and the viva
  • Q: What is the servlet life cycle? A: The container loads the class, calls init() once, calls service() (which dispatches to doGet, doPost) for every request on a pooled thread, and calls destroy() once at undeploy.
  • Q: Why jakarta.servlet and not javax.servlet? A: Tomcat 10 implements Jakarta EE 9+, where every package was renamed from javax.* to jakarta.*. Code with javax.servlet imports compiles but Tomcat 10 never calls it.
  • Q: Difference between @WebServlet and web.xml mapping? A: Same effect; the annotation keeps the mapping next to the code, web.xml lets you change it without recompiling and is needed for container-wide settings like session timeout and error pages.
  • Q: GET versus POST? A: GET carries parameters in the URL, is idempotent and cacheable; POST carries them in the body and is used for anything that changes state.
  • Q: How does Tomcat know which session belongs to which browser? A: By the JSESSIONID cookie, or by ;jsessionid= in the URL when cookies are off.
  • Q: Where is session data stored, browser or server? A: On the server, in the HttpSession object; the browser only holds the id.
  • Q: Why PreparedStatement instead of Statement? A: Parameters are bound, not concatenated, so user input cannot alter the SQL; the driver can also cache the compiled statement.
  • Q: What does Post-Redirect-Get solve? A: A browser refresh after a POST re-sends the form; redirecting to a GET page after the write prevents duplicate inserts.

Common Mistakes

Do not copy. Read for understanding and the viva
  • Importing javax.servlet.*: the project compiles against an old jar but Tomcat 10 returns 404 for the servlet.
  • Calling getWriter() before setContentType(): the charset header is ignored and non-ASCII text appears garbled.
  • Forgetting mysql-connector-j in WEB-INF/lib (or pom.xml): No suitable driver found for jdbc:mysql://... at run time.
  • Leaving the MySQL server stopped or the IGNOU schema uncreated: Communications link failure or Unknown database 'ignou'.
  • Deleting through a GET link, or forgetting to redirect after POST, so a refresh repeats the write.
  • Writing user input straight into HTML without escaping, which lets a <script> in the name field run in every viewer’s browser.

Session Summary

Write in lab record
  • Project ServletLab on Tomcat 10.1 with pom.xml (jakarta.servlet-api, JSTL, mysql-connector-j) and web.xml
  • Question 1: DateTimeServlet at /datetime printing date, time and millisecond timestamp
  • Question 2: student-form.html posting to StudentInfoServlet at /studentInfo, showing method and protocol
  • Question 3: ClientIpServlet at /clientIp with getRemoteAddr and X-Forwarded-For
  • Question 4: SessionTrackingServlet at /session with HttpSession visit counter, visitorName cookie, logout and URL rewriting
  • Question 5: schema.sql (IGNOU database, Student table), Student, StudentDao with PreparedStatement, StudentServlet CRUD at /students
Navigation

Type to search…

↑↓ navigate↵ selectEsc close