Project page: Lenora Virtual Connect, a one-page showcase of this project.
Download the source: virtual-classroom.zip (Maven project, SQL schema, Dockerfile and compose file, about 45 KB).
1. Abstract
The Virtual Classroom is a web application in which teachers run online courses and students take part in them. It has three modules: Student, Teacher and Administrator. Teachers publish lessons, set assignments with deadlines, grade submissions, schedule live video classes and record attendance. Students enrol in courses, read lessons, join live classes, submit work, see marks and feedback and discuss with the class. The administrator manages accounts, courses, enrolment and announcements.
The system is built in Java with JSP pages for the front end, servlets as controllers, PostgreSQL as the database and Apache Tomcat as the application server. Data access uses a JDBC Type 4 driver. Security is part of the design: salted password hashing, prepared statements, output escaping, request forgery tokens and role-based access control.
2. Introduction and objectives
Classroom teaching has moved partly online, and a small institution needs a simple system that it can host itself and understand completely. The objectives of this project are:
- To provide a single place for course material, assignments, grades, discussion and live classes.
- To give each kind of user only the features they need (role-based modules).
- To keep personal and academic data safe.
- To use an architecture that is simple enough to explain, extend and maintain: a layered, servlet and JSP design without a heavy framework.
- To make installation easy with a container-based deployment.
3. Software requirements
| Item | Choice |
|---|---|
| Operating system | Windows 11, Linux or macOS (Docker deployment runs the same everywhere) |
| Front end tool | HTML5 and CSS with JSP and JSTL (Jakarta Tags 3.0) |
| Back end tool | PostgreSQL 18 |
| Technology | Java 25 (LTS) with Jakarta EE 11 web profile APIs: Servlet 6.1, Pages 4.0, Expression Language 6.0 |
| Application server | Apache Tomcat 11.0.x |
| Database connectivity | JDBC Type 4 (pure Java) driver, pgjdbc 42.7.14, with HikariCP connection pooling |
| Build tool | Apache Maven 3, packaging a WAR file |
| Deployment | Docker and Docker Compose (optional); Jitsi Meet for live video |
| Hardware | Any machine with 2 CPU cores, 4 GB RAM and 2 GB free disk for a classroom-sized deployment |
Note that Jakarta EE uses jakarta.* package names, so imports read jakarta.servlet.http.HttpServlet, not javax.servlet.
4. System analysis
4.1 Actors and use cases
flowchart LR
S(["Student"])
T(["Teacher"])
A(["Administrator"])
S --- s1["Register and sign in"]
S --- s2["Browse catalogue and enrol"]
S --- s3["Read lessons"]
S --- s4["Join live class"]
S --- s5["Submit or resubmit assignment"]
S --- s6["View marks, feedback, attendance"]
S --- s7["Post in course discussion"]
T --- t1["Add lessons and links"]
T --- t2["Create assignments"]
T --- t3["Schedule live classes"]
T --- t4["Grade submissions"]
T --- t5["Mark attendance"]
T --- t7["Post in course discussion"]
A --- a1["Create teacher, student, admin accounts"]
A --- a2["Enable or disable accounts"]
A --- a3["Create courses, assign teachers"]
A --- a4["Enrol students, close courses"]
A --- a5["Post announcements"]
4.2 Functional requirements
| ID | Requirement | Module |
|---|---|---|
| F1 | Students register themselves; teachers and administrators are created by an administrator | Student, Administrator |
| F2 | Sign in and sign out with a role-based landing page | All |
| F3 | Create, assign, close and reopen courses | Administrator |
| F4 | Enrol students (self-service or by the administrator) | Student, Administrator |
| F5 | Lessons with text and an optional web link | Teacher, Student |
| F6 | Assignments with deadline and maximum marks; resubmission allowed until graded; submission closes at the deadline | Teacher, Student |
| F7 | Grading with marks within the maximum and written feedback | Teacher |
| F8 | Scheduled live classes with a private room link | Teacher, Student |
| F9 | Attendance per live class | Teacher, Student |
| F10 | Course discussion board | All |
| F11 | Announcements shown on the student dashboard | Administrator, Student |
| F12 | Disabling an account blocks access immediately | Administrator |
4.3 Non-functional requirements
Security (see section 9), responsive pages that work on a phone, time-zone safe timestamps, and simple installation with one command.
5. System architecture
5.1 Layered architecture
The system is a classic three-tier, MVC-style web application. The browser holds no logic beyond forms; every decision is taken on the server, and the database enforces the data rules again with constraints.
flowchart TB
subgraph CL["Tier 1: Client"]
BR["Web browser: HTML5 forms, CSS, no inline scripts"]
end
subgraph AP["Tier 2: Application server, Apache Tomcat 11 on Java 25"]
direction TB
FL["SecurityFilter mapped to /* : security headers, CSRF token, role check, active-account check"]
subgraph CT["Controller layer: servlets"]
direction LR
HS["HomeServlet /"]
LS["LoginServlet /login"]
RS["RegisterServlet /register"]
OS["LogoutServlet /logout"]
AS["AdminServlet /admin/*"]
TS["TeacherServlet /teacher/*"]
SS["StudentServlet /student/*"]
CS["CourseServlet /course/*"]
end
subgraph VW["View layer: JSP and JSTL under WEB-INF/views"]
direction LR
V1["header.jsp, footer.jsp, error.jsp"]
V2["login, register, course"]
V3["admin/*"]
V4["teacher/*"]
V5["student/*"]
end
subgraph SV["Support classes"]
direction LR
U1["BaseServlet: user, view, redirect, access rules"]
U2["Db: JDBC helper, prepared statements"]
U3["Passwords: PBKDF2"]
U4["Forms: input parsing, URL check"]
end
LI["AppListener: starts pool, creates first admin"]
end
subgraph DT["Tier 3: Data"]
PO["HikariCP connection pool"]
DR["pgjdbc driver, JDBC Type 4"]
PG[("PostgreSQL 18: database classroom, 10 tables")]
end
BR -->|"HTTP request"| FL
FL --> CT
CT -->|"forward with request attributes"| VW
CT --> SV
U2 --> PO --> DR --> PG
LI --> U2
VW -->|"HTML response"| BR
5.2 Deployment and wiring between components
flowchart LR
USER["User's browser"]
subgraph HOST["Docker host: docker compose"]
direction LR
subgraph NET["Compose network, name resolution by service name"]
direction LR
subgraph APPC["Container app: tomcat 11.0 jre25"]
WAR["ROOT.war: classes, JSPs, JSTL, pgjdbc, HikariCP"]
end
subgraph DBC["Container db: postgres 18"]
PGS[("classroom database")]
end
end
VOL[("Volume pgdata")]
INIT["db/schema.sql mounted into docker-entrypoint-initdb.d"]
end
JITSI["Jitsi Meet: meet.jit.si"]
USER -->|"HTTP 8080 mapped to container 8080"| WAR
WAR -->|"jdbc:postgresql://db:5432/classroom, user and password from environment"| PGS
PGS --- VOL
INIT -->|"runs once on first start"| PGS
USER -->|"HTTPS: Join or Start link, room name from database"| JITSI
The wiring is configured entirely through environment variables, so the same WAR runs unchanged on a laptop, a server or a container platform.
| Variable | Meaning | Default |
|---|---|---|
CLASSROOM_DB_URL |
JDBC URL | jdbc:postgresql://localhost:5432/classroom |
CLASSROOM_DB_USER |
Database user | classroom |
CLASSROOM_DB_PASSWORD |
Database password | classroom (change it) |
CLASSROOM_DB_POOL |
Maximum pooled connections | 10 |
CLASSROOM_ADMIN_EMAIL |
First administrator’s email | admin@classroom.local |
CLASSROOM_ADMIN_PASSWORD |
First administrator’s password; a random one is logged once if unset | none |
5.3 Start-up wiring
When Tomcat deploys the WAR, components come up in this order:
- Tomcat reads
WEB-INF/web.xml: session settings (HttpOnly cookie, cookie tracking, 60-minute timeout), JSP encoding (UTF-8) and the error pages for 403, 404 and 500. - Annotation scanning registers the classes marked
@WebListener,@WebFilterand@WebServlet. AppListener.contextInitializedcallsDb.init(), which builds the HikariCP pool from the environment variables; it then checks whether an administrator exists and creates one if not.- The
SecurityFilterand the servlets are ready; Tomcat starts accepting requests on port 8080. - On shutdown,
contextDestroyedcloses the pool.
5.4 Request-handling flow inside the filter
Every request passes through one filter before any servlet runs.
flowchart TD
R["Incoming request"] --> H["Add security headers: nosniff, frame deny, referrer policy, Content-Security-Policy"]
H --> AS{"Path starts with /assets/ ?"}
AS -->|"yes"| PASS["Pass to the next component"]
AS -->|"no"| SE["Get or create session, create CSRF token if missing"]
SE --> PM{"Method is POST ?"}
PM -->|"yes"| TK{"Form token equals session token ?"}
TK -->|"no"| E403["403 Invalid or missing form token"]
TK -->|"yes"| PR
PM -->|"no"| PR{"Path is /admin, /teacher, /student or /course ?"}
PR -->|"no, public page"| PASS
PR -->|"yes"| LG{"User in session ?"}
LG -->|"no"| LOGIN["Redirect to /login"]
LG -->|"yes"| AC{"Account still active in database ?"}
AC -->|"no"| OUT["Invalidate session, redirect to /login"]
AC -->|"yes"| RL{"Role matches the module ?"}
RL -->|"no"| E403B["403 Not allowed for your role"]
RL -->|"yes"| PASS
5.5 URL, servlet, view and table map
This table is the complete wiring of the application: which address is handled by which servlet, which page it shows and which tables it touches.
| URL | Servlet | View (JSP) | Tables used |
|---|---|---|---|
GET / |
HomeServlet | redirect by role | none |
/login |
LoginServlet | login.jsp |
users |
/register |
RegisterServlet | register.jsp |
users |
POST /logout |
LogoutServlet | redirect | none |
GET /admin |
AdminServlet | admin/dashboard.jsp |
users, courses, enrollments |
GET /admin/users, POST /admin/users/create, /admin/users/toggle |
AdminServlet | admin/users.jsp |
users |
GET /admin/courses, POST /admin/courses/create, /assign, /toggle, /enroll |
AdminServlet | admin/courses.jsp |
courses, users, enrollments |
GET /admin/announcements, POST /admin/announcements/create |
AdminServlet | admin/announcements.jsp |
announcements, users |
GET /teacher |
TeacherServlet | teacher/dashboard.jsp |
courses, class_sessions, submissions, assignments |
GET /teacher/submissions?assignment= , POST /teacher/grade |
TeacherServlet | teacher/submissions.jsp |
submissions, assignments, courses, users |
GET /teacher/attendance?session=, POST /teacher/attendance |
TeacherServlet | teacher/attendance.jsp |
attendance, class_sessions, enrollments, users |
POST /teacher/lesson, /assignment, /session |
TeacherServlet | redirect to course page | lessons, assignments, class_sessions |
GET /student |
StudentServlet | student/dashboard.jsp |
enrollments, courses, class_sessions, assignments, submissions, announcements |
GET /student/catalog, POST /student/enroll |
StudentServlet | student/catalog.jsp |
courses, enrollments |
GET /student/assignment?id=, POST /student/submit |
StudentServlet | student/assignment.jsp |
assignments, submissions, enrollments |
GET /course?id=, POST /course/post |
CourseServlet | course.jsp |
all course tables, posts |
5.6 Sequence: sign in
sequenceDiagram
autonumber
actor U as User
participant F as SecurityFilter
participant L as LoginServlet
participant D as Db and Passwords
participant P as PostgreSQL
participant J as login.jsp
U->>F: GET /login
F->>F: create session and CSRF token
F->>L: continue
L->>J: forward
J-->>U: login form with hidden token
U->>F: POST /login with email, password, token
F->>F: token check passes
F->>L: continue
L->>D: find user by lower(email)
D->>P: SELECT with bind parameter
P-->>D: row with password hash
L->>D: verify password against PBKDF2 hash
D-->>L: true
L->>L: changeSessionId, store id, name, role in session
L-->>U: 302 redirect to /admin, /teacher or /student
5.7 Sequence: student submits an assignment
sequenceDiagram
autonumber
actor S as Student
participant F as SecurityFilter
participant T as StudentServlet
participant P as PostgreSQL
participant V as assignment.jsp
S->>F: GET /student/assignment?id=7
F->>F: session user, active, role STUDENT
F->>T: continue
T->>P: assignment joined with enrollment of this student
P-->>T: row, or none if not enrolled
T->>P: this student's existing submission
T->>V: forward with assignment and submission
V-->>S: page with answer box (closed after deadline or once graded)
S->>F: POST /student/submit with answer and token
F->>F: CSRF token check
F->>T: continue
T->>P: is the student enrolled and is the deadline still open
P-->>T: row with closed flag
T->>P: INSERT ... ON CONFLICT DO UPDATE WHERE marks IS NULL
P-->>T: affected rows
T-->>S: 302 redirect back with message Submitted
6. Database design
6.1 Entity-relationship diagram
erDiagram
users ||--o{ courses : "teaches"
users ||--o{ enrollments : "enrols as student"
courses ||--o{ enrollments : "has"
courses ||--o{ lessons : "contains"
courses ||--o{ assignments : "sets"
courses ||--o{ class_sessions : "schedules"
courses ||--o{ posts : "discussion"
assignments ||--o{ submissions : "receives"
users ||--o{ submissions : "submits"
class_sessions ||--o{ attendance : "records"
users ||--o{ attendance : "attends"
users ||--o{ posts : "writes"
users ||--o{ announcements : "publishes"
users {
bigint id PK
text full_name
text email UK
text password_hash
text role
boolean active
timestamptz created_at
}
courses {
bigint id PK
text code UK
text title
text description
bigint teacher_id FK
boolean active
}
enrollments {
bigint course_id PK
bigint student_id PK
timestamptz enrolled_at
}
lessons {
bigint id PK
bigint course_id FK
text title
text body
text resource_url
}
assignments {
bigint id PK
bigint course_id FK
text title
text instructions
timestamptz due_at
integer max_marks
}
submissions {
bigint id PK
bigint assignment_id FK
bigint student_id FK
text answer
timestamptz submitted_at
integer marks
text feedback
timestamptz graded_at
}
class_sessions {
bigint id PK
bigint course_id FK
text title
timestamptz starts_at
integer duration_min
text room
}
attendance {
bigint session_id PK
bigint student_id PK
boolean present
}
posts {
bigint id PK
bigint course_id FK
bigint author_id FK
text body
timestamptz created_at
}
announcements {
bigint id PK
bigint author_id FK
text title
text body
timestamptz created_at
}
6.2 Design decisions
| Decision | Reason |
|---|---|
One users table with a role column and a CHECK constraint |
One login path for all roles; the database refuses an unknown role |
Email unique on lower(email) |
Raj@x.com and raj@x.com are the same person |
Identity columns for keys, TIMESTAMPTZ for times |
Standard SQL keys; correct dates across time zones |
Composite primary keys on enrollments and attendance |
A student cannot be enrolled twice or have two attendance marks for one class |
UNIQUE (assignment_id, student_id) on submissions |
One submission per student per assignment, updated on resubmission |
CHECK (marks >= 0), max_marks > 0, duration between 5 and 480 minutes |
Bad data is refused even if application code has a bug |
ON DELETE CASCADE from courses to their content; ON DELETE SET NULL for the course teacher |
Removing a course removes its material; removing a teacher leaves the course unassigned |
Indexes on foreign keys used in lists, such as (course_id, due_at) |
Fast course pages |
The full DDL is in db/schema.sql in the download.
7. Module design
7.1 Student module
- Dashboard: latest announcements, enrolled courses with teacher, next five live classes with a Join button, assignments still open, recent grades with feedback.
- Catalogue: all active courses; Enrol creates an enrolment, Open course appears once enrolled.
- Course page: lessons, live classes with attendance status, assignments with status (open, submitted, graded, missed) and the discussion.
- Assignment page: instructions, deadline, answer box; after the deadline, or after grading, the answer is read-only.
7.2 Teacher module
- Dashboard: own courses with student counts, upcoming live classes with a Start button, submissions waiting for grading.
- Course page (teacher view): forms to add a lesson, create an assignment, schedule a live class, plus the student roster.
- Submissions page: each answer with fields for marks (limited to the assignment maximum) and feedback.
- Attendance page: a checklist of enrolled students for one live class.
7.3 Administrator module
- Dashboard: counts of students, teachers, active courses and enrolments; newest accounts.
- Users: create an account of any role; enable or disable accounts (an administrator cannot disable themselves).
- Courses: create a course, assign or change its teacher, enrol a student, close or reopen the course.
- Announcements: post messages that students see on their dashboard.
7.4 Live classes
A live class stores a random room name such as vc- followed by about 24 random characters. The Join and Start buttons open https://meet.jit.si/<room> in a new tab. Because the room name is unguessable and shown only to the teacher and enrolled students, outsiders cannot find it. The project therefore delivers video conferencing without running a media server; hosting Jitsi on the institution’s own server is a drop-in change to the link.
8. Implementation highlights
8.1 Database helper
All SQL goes through one small class, so every statement uses bind parameters and every connection is returned to the pool.
public static List<Map<String, Object>> query(String sql, Object... params) {
try (Connection c = pool.getConnection();
PreparedStatement ps = c.prepareStatement(sql)) {
bind(ps, params);
try (ResultSet rs = ps.executeQuery()) {
// each row becomes a Map keyed by column label, readable in JSP as ${row.title}
}
} catch (SQLException e) {
throw new DbException("Query failed: " + e.getMessage(), e);
}
}
8.2 Controller pattern
Each servlet reads the path after its mapping, checks permissions, uses the database, and either forwards to a JSP (GET) or redirects (POST), so refreshing a page never repeats an action.
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
switch (sub(req)) {
case "/grade" -> {
// the submission must belong to a course taught by the signed-in teacher
// marks must be between 0 and the assignment maximum
// UPDATE submissions SET marks = ?, feedback = ?, graded_at = now() WHERE id = ?
redirect(req, resp, "/teacher/submissions?assignment=" + assignment, "Grade saved.");
}
default -> notFound(resp);
}
}
8.3 Safe resubmission in one statement
INSERT INTO submissions (assignment_id, student_id, answer) VALUES (?, ?, ?)
ON CONFLICT (assignment_id, student_id)
DO UPDATE SET answer = EXCLUDED.answer, submitted_at = now()
WHERE submissions.marks IS NULL;
A student can replace an answer until the teacher grades it; after that the statement changes nothing, and the servlet tells the student that the work is locked.
8.4 View layer
JSP pages contain no business logic. They print what the servlet prepared with JSTL, escaping every value:
<c:forEach var="a" items="${assignments}">
<li><c:out value="${a.title}"/>
<span class="meta">due <fmt:formatDate value="${a.due_at}" pattern="dd MMM yyyy, HH:mm"/></span></li>
</c:forEach>
9. Security design
| Threat | Countermeasure |
|---|---|
| Password theft | PBKDF2-HMAC-SHA256, 600,000 iterations, random 16-byte salt per user, constant-time comparison; stored as pbkdf2$iterations$salt$hash |
| SQL injection | Prepared statements with bind parameters everywhere |
| Cross-site scripting | <c:out> on every value; Content-Security-Policy blocks inline script and style |
| Cross-site request forgery | Per-session random token required on every POST |
| Session fixation and hijacking | New session id at login, HttpOnly cookie, cookie-only tracking, POST-only logout that invalidates the session |
| Broken access control | Role check in the filter, then an ownership check in each servlet (teacher owns the course, student is enrolled) |
| Disabled users keeping access | The account is re-checked in the database on every protected request |
| User enumeration | Same error text for unknown email and wrong password; a dummy hash check keeps timing similar |
| Dangerous links | Only http and https addresses are accepted for lesson resources |
| Clickjacking and sniffing | X-Frame-Options: DENY, frame-ancestors 'none', nosniff |
| Default credentials | No fixed admin password: it comes from an environment variable or is generated randomly and logged once |
10. Installation and deployment
10.1 With Docker
unzip virtual-classroom.zip && cd virtual-classroom
docker compose up --build
docker compose logs app | grep -i administrator # first-run admin password
The compose file starts PostgreSQL 18, loads db/schema.sql on first start, waits for the database to be healthy, then starts Tomcat 11 with the application as the root web application. Open http://localhost:8080 and sign in as admin@classroom.local. Change the database password in docker-compose.yml before using it on a shared machine.
10.2 Without Docker
- Install Java 25, Maven, PostgreSQL 18 and Tomcat 11.
- Create a database and user, then run
psql -d classroom -f db/schema.sql. - Set the
CLASSROOM_DB_*environment variables for the Tomcat process. - Run
mvn packageand copytarget/classroom.warinto Tomcat’swebappsfolder.
10.3 Project structure
virtual-classroom/
├── pom.xml Maven build, WAR packaging, dependencies
├── Dockerfile two-stage build: Maven, then Tomcat 11
├── docker-compose.yml app + PostgreSQL 18 + volume
├── db/schema.sql tables, constraints, indexes
├── db/sample-data.sql optional demo rows
└── src/main
├── java/com/mrvitej/classroom
│ ├── util/ Db, DbException, Passwords, Forms
│ └── web/ AppListener, SecurityFilter, BaseServlet, Home, Login, Register,
│ Logout, Admin, Teacher, Student, Course servlets
└── webapp
├── assets/app.css
└── WEB-INF/ web.xml and views/ (all JSP pages)
11. Test cases
These cases cover the main requirements and are suitable for the testing chapter of the report.
| No. | Test | Expected result |
|---|---|---|
| 1 | Register a student with a new email | Account created, redirected to sign in |
| 2 | Register again with the same email in different letter case | Refused: email already registered |
| 3 | Sign in with a wrong password | Generic error; no hint whether the email exists |
| 4 | Student opens /admin |
403 Not allowed |
| 5 | Open /teacher while signed out |
Redirect to /login |
| 6 | Submit any form with the token removed | 403 Invalid or missing form token |
| 7 | Administrator disables a signed-in student | The student’s next click redirects to the login page |
| 8 | Teacher opens a course that belongs to another teacher | 404 |
| 9 | Student submits before the deadline, then resubmits | One submission, answer replaced |
| 10 | Student submits after the deadline | Refused: submissions are closed |
| 11 | Teacher grades with marks above the maximum | Refused with the allowed range |
| 12 | Student resubmits after being graded | Refused: work is locked |
| 13 | Teacher marks attendance, then edits it | One row per student, latest value kept |
| 14 | Add a lesson link starting with javascript: |
Link is not stored |
| 15 | Post a message containing <script> |
Shown as plain text |
12. Limitations and future scope
- Rate limiting and lockout after repeated failed logins.
- Password reset and email verification.
- File upload for assignments, stored outside the web root with size and type limits.
- Embedded video using the Jitsi API or a self-hosted instance, with attendance recorded automatically.
- Automated tests: JUnit for the helpers and integration tests with Testcontainers on PostgreSQL.
- Pagination for long lists, an audit log of administrator actions and notification emails.
- HTTPS behind a reverse proxy, with the session cookie’s
Secureflag turned on, before real use.
13. Conclusion
The Virtual Classroom meets its objectives: three role-based modules, a clear layered architecture, a constrained relational schema and a security design that covers the common web risks. The simple servlet and JSP structure makes every request traceable from the filter to the database and back, which is valuable both for maintenance and for explaining the project in a viva.
Sources: Apache Tomcat 11 downloads, PostgreSQL JDBC driver, PostgreSQL.
Written by Claude, Anthropic’s AI assistant.