做读者档案、联系方式、借阅资格功能
This commit is contained in:
@@ -27,10 +27,11 @@ Implemented scaffold tables:
|
|||||||
- `book_categories`: category names and descriptions for catalog grouping.
|
- `book_categories`: category names and descriptions for catalog grouping.
|
||||||
- `books`: book information, category reference, inventory counts, and catalog
|
- `books`: book information, category reference, inventory counts, and catalog
|
||||||
status.
|
status.
|
||||||
|
- `readers`: reader profiles, optional login-account linkage, borrowing
|
||||||
|
eligibility, contact information, and management status.
|
||||||
|
|
||||||
Planned module tables:
|
Planned module tables:
|
||||||
|
|
||||||
- `readers`: reader profiles, borrowing eligibility, contact information.
|
|
||||||
- `borrow_records`: book-reader borrowing, return, renew, and overdue data.
|
- `borrow_records`: book-reader borrowing, return, renew, and overdue data.
|
||||||
|
|
||||||
Record new schema changes in `src/main/resources/db/schema.sql` and update this
|
Record new schema changes in `src/main/resources/db/schema.sql` and update this
|
||||||
@@ -78,6 +79,11 @@ spec with exact table names, key columns, and DAO/service contracts.
|
|||||||
- `role_permissions.permission_code` must reference `permissions.code`.
|
- `role_permissions.permission_code` must reference `permissions.code`.
|
||||||
- `books.status` must match `BookStatus` enum codes: `available`,
|
- `books.status` must match `BookStatus` enum codes: `available`,
|
||||||
`unavailable`, and `archived`.
|
`unavailable`, and `archived`.
|
||||||
|
- `readers.user_id` may reference `users.id` when a reader profile is linked to
|
||||||
|
a login account.
|
||||||
|
- `readers.status` must match `ReaderStatus` enum codes: `active`,
|
||||||
|
`suspended`, and `inactive`.
|
||||||
|
- `readers.max_borrow_count` must stay between 1 and 50.
|
||||||
|
|
||||||
## Scenario: Book Catalog And Management Slice
|
## Scenario: Book Catalog And Management Slice
|
||||||
|
|
||||||
@@ -187,6 +193,115 @@ books/form.jsp -> JDBC -> INSERT INTO books using request parameters
|
|||||||
books/form.jsp -> BookManagementServlet -> BookService -> BookDao -> books
|
books/form.jsp -> BookManagementServlet -> BookService -> BookDao -> books
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Scenario: Reader Information Management Slice
|
||||||
|
|
||||||
|
### 1. Scope / Trigger
|
||||||
|
|
||||||
|
- Trigger: the reader profile foundation was implemented after login,
|
||||||
|
permissions, catalog search, and book management.
|
||||||
|
- Schema path: `src/main/resources/db/schema.sql`.
|
||||||
|
- JSP paths: `WEB-INF/jsp/readers/manage.jsp` and `readers/form.jsp`.
|
||||||
|
|
||||||
|
### 2. Signatures
|
||||||
|
|
||||||
|
- DAO signatures: `ReaderDao.search(ReaderSearchCriteria criteria)`,
|
||||||
|
`findById(long id)`, `findByIdentifier(String identifier)`,
|
||||||
|
`findByUserId(long userId)`, `create(Reader reader)`,
|
||||||
|
`update(Reader reader)`, and `deactivate(long id)`.
|
||||||
|
- Entity/search signatures: `Reader` fields are `id`, `identifier`,
|
||||||
|
nullable `userId`, nullable `username`, `fullName`, `phone`, `email`,
|
||||||
|
`status`, `maxBorrowCount`, `createdAt`, and `updatedAt`;
|
||||||
|
`ReaderSearchCriteria` fields are `identifier`, `name`, `contact`, and
|
||||||
|
`statusCode`.
|
||||||
|
- Status signature: `ReaderStatus` enum codes are `active`, `suspended`, and
|
||||||
|
`inactive`.
|
||||||
|
- Service signatures: `ReaderService.searchReaders(ReaderSearchCriteria)`,
|
||||||
|
`findReader(long id)`, `createReader(AuthenticatedUser actor, Reader reader)`,
|
||||||
|
`updateReader(AuthenticatedUser actor, Reader reader)`, and
|
||||||
|
`deactivateReader(AuthenticatedUser actor, long id)`, all returning
|
||||||
|
`ServiceResult<T>`.
|
||||||
|
- Management routes: `GET /readers`, `GET /readers/new`,
|
||||||
|
`GET /readers/edit?id=...`, `POST /readers`, `POST /readers/update`, and
|
||||||
|
`POST /readers/delete`.
|
||||||
|
- Protected permission: `/readers*` requires `MANAGE_READERS`.
|
||||||
|
- DB signature: `readers(id, reader_identifier, user_id, full_name, phone,
|
||||||
|
email, status, max_borrow_count, created_at, updated_at)`, with unique keys
|
||||||
|
on `reader_identifier` and nullable `user_id`, indexes on name/contact/status,
|
||||||
|
a foreign key to `users(id)`, and checks for supported status values and
|
||||||
|
borrow-limit range.
|
||||||
|
|
||||||
|
### 3. Contracts
|
||||||
|
|
||||||
|
- `readers.reader_identifier` is the unique user-facing reader ID.
|
||||||
|
- `readers.user_id` is optional; when present, only one reader profile may link
|
||||||
|
to the same login account.
|
||||||
|
- `readers.status` stores the Java `ReaderStatus` code exactly. Soft-delete
|
||||||
|
behavior deactivates the profile by setting status to `inactive`.
|
||||||
|
- Reader searches support partial identifier, full-name, and phone/email
|
||||||
|
contact matching plus exact status filtering.
|
||||||
|
- Servlet controllers parse request fields and set JSP attributes such as
|
||||||
|
`criteria`, `readers`, `reader`, `statuses`, `formValues`, `errors`,
|
||||||
|
`errorMessage`, and `successMessage`.
|
||||||
|
- JSP pages render JavaBean properties only; they must not call DAOs or embed
|
||||||
|
SQL.
|
||||||
|
- Navigation should expose reader management to administrator/librarian users,
|
||||||
|
matching the current `MANAGE_READERS` policy.
|
||||||
|
|
||||||
|
### 4. Validation & Error Matrix
|
||||||
|
|
||||||
|
- Missing identifier or full name -> return to `readers/form.jsp` with field
|
||||||
|
errors.
|
||||||
|
- Missing both phone and email -> return a field error on `phone`.
|
||||||
|
- Invalid phone or email format -> return field errors on `phone` or `email`.
|
||||||
|
- Unsupported status -> return a field error on `status`.
|
||||||
|
- Max borrow count outside 1 to 50 -> return a field error on
|
||||||
|
`maxBorrowCount`.
|
||||||
|
- Non-positive linked `user_id` -> return a field error on `userId`.
|
||||||
|
- Duplicate `reader_identifier` -> return a field error on `identifier`.
|
||||||
|
- Duplicate linked `user_id` -> return a field error on `userId`.
|
||||||
|
- Reader or unauthenticated actor attempts write -> HTTP 403 through
|
||||||
|
authorization filter or service denial.
|
||||||
|
- DAO failure during list/search/write -> log server-side details and return
|
||||||
|
`Reader service is temporarily unavailable. Please try again later.`
|
||||||
|
- Successful create/update/deactivate -> redirect to `/readers` with a short
|
||||||
|
flash message.
|
||||||
|
|
||||||
|
### 5. Good/Base/Bad Cases
|
||||||
|
|
||||||
|
- Good: a librarian creates `RD-1003`, searches by email, edits the borrow
|
||||||
|
limit, and can later deactivate the profile without deleting future borrowing
|
||||||
|
history.
|
||||||
|
- Base: `/readers` with no filters lists reader records ordered by full name
|
||||||
|
and reader identifier.
|
||||||
|
- Bad: normal readers access `/readers`, a JSP opens JDBC, or deleting a reader
|
||||||
|
profile removes rows that future borrow records need to reference.
|
||||||
|
|
||||||
|
### 6. Tests Required
|
||||||
|
|
||||||
|
- Run `ReaderServiceCheck` or equivalent assertions for invalid contact,
|
||||||
|
invalid borrow limit, duplicate identifiers, duplicate linked users, reader
|
||||||
|
write denial, successful librarian CRUD-like operations, search, deactivate,
|
||||||
|
and DAO failure fallback.
|
||||||
|
- Run `PermissionPolicyCheck` to confirm librarians have `MANAGE_READERS` and
|
||||||
|
readers do not.
|
||||||
|
- Scan reader JSPs for scriptlets and SQL/JDBC references.
|
||||||
|
- When Maven/Tomcat dependencies are installed, run `mvn clean package` to
|
||||||
|
compile Servlets and package JSP resources.
|
||||||
|
|
||||||
|
### 7. Wrong vs Correct
|
||||||
|
|
||||||
|
#### Wrong
|
||||||
|
|
||||||
|
```text
|
||||||
|
readers/form.jsp -> JDBC -> DELETE FROM readers using request parameters
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Correct
|
||||||
|
|
||||||
|
```text
|
||||||
|
readers/form.jsp -> ReaderManagementServlet -> ReaderService -> ReaderDao -> readers.status = inactive
|
||||||
|
```
|
||||||
|
|
||||||
## Scenario: Login And Permission Scaffold Schema
|
## Scenario: Login And Permission Scaffold Schema
|
||||||
|
|
||||||
### 1. Scope / Trigger
|
### 1. Scope / Trigger
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{"file": ".trellis/spec/backend/index.md", "reason": "Backend checklist and architecture expectations for verifying the reader-management slice."}
|
||||||
|
{"file": ".trellis/spec/backend/database-guidelines.md", "reason": "Schema, DAO, service, permission, and test expectations to check against."}
|
||||||
|
{"file": ".trellis/spec/backend/error-handling.md", "reason": "Verify controller/service/DAO failures are handled consistently."}
|
||||||
|
{"file": ".trellis/spec/backend/logging-guidelines.md", "reason": "Verify server-side failure logging and protected workflow logging expectations."}
|
||||||
|
{"file": ".trellis/spec/backend/quality-guidelines.md", "reason": "Backend quality gate and review checklist."}
|
||||||
|
{"file": ".trellis/spec/frontend/index.md", "reason": "Frontend checklist for JSP/CSS verification."}
|
||||||
|
{"file": ".trellis/spec/frontend/component-guidelines.md", "reason": "Verify reader JSP forms/tables follow local component conventions."}
|
||||||
|
{"file": ".trellis/spec/frontend/state-management.md", "reason": "Verify request/session/form state handling remains server-rendered."}
|
||||||
|
{"file": ".trellis/spec/frontend/type-safety.md", "reason": "Verify JSP/Servlet validation and display contracts."}
|
||||||
|
{"file": ".trellis/spec/frontend/quality-guidelines.md", "reason": "Frontend JSP/static quality checks."}
|
||||||
|
{"file": ".trellis/tasks/archive/2026-04/00-bootstrap-guidelines/research/project-requirements.md", "reason": "Verify implemented reader management satisfies the original project requirement."}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{"file": ".trellis/spec/backend/index.md", "reason": "Backend pre-development checklist and layered architecture overview for JSP/Servlet/MySQL work."}
|
||||||
|
{"file": ".trellis/spec/backend/directory-structure.md", "reason": "Backend package layout and responsibility boundaries for controller, service, DAO, entity, filter, and util classes."}
|
||||||
|
{"file": ".trellis/spec/backend/database-guidelines.md", "reason": "Schema, DAO, query, and existing book-module scenario conventions relevant to adding readers."}
|
||||||
|
{"file": ".trellis/spec/backend/error-handling.md", "reason": "Service/DAO/controller error-handling conventions for user-facing failures and logging."}
|
||||||
|
{"file": ".trellis/spec/backend/logging-guidelines.md", "reason": "Logging boundaries for protected workflows and DAO/service failures."}
|
||||||
|
{"file": ".trellis/spec/backend/quality-guidelines.md", "reason": "Backend checks and review expectations for layered feature work."}
|
||||||
|
{"file": ".trellis/spec/frontend/index.md", "reason": "Frontend pre-development checklist for JSP/CSS work."}
|
||||||
|
{"file": ".trellis/spec/frontend/directory-structure.md", "reason": "JSP and static asset placement conventions for reader-management pages."}
|
||||||
|
{"file": ".trellis/spec/frontend/component-guidelines.md", "reason": "JSP fragment, form, table, and reusable UI conventions."}
|
||||||
|
{"file": ".trellis/spec/frontend/state-management.md", "reason": "Server-rendered request/session/form state conventions."}
|
||||||
|
{"file": ".trellis/spec/frontend/type-safety.md", "reason": "JSP/Servlet validation and JavaBean display contracts."}
|
||||||
|
{"file": ".trellis/spec/frontend/quality-guidelines.md", "reason": "Frontend quality checks for JSP pages and static assets."}
|
||||||
|
{"file": ".trellis/tasks/archive/2026-04/00-bootstrap-guidelines/research/project-requirements.md", "reason": "Original project requirements listing reader information management as a core module."}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
# brainstorm: 继续完成程序
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Continue the MZH Library Management system from the current scaffold by implementing the next focused business slice after login/permission and book catalog/management: reader information management.
|
||||||
|
|
||||||
|
## What I already know
|
||||||
|
|
||||||
|
* User asked to "继续完成程序" without specifying the next target module.
|
||||||
|
* The project is a Java 11 Maven WAR application using JSP, Servlet, Tomcat, MySQL, and JDBC DAOs.
|
||||||
|
* Current code includes login/logout, authentication and authorization filters, role-aware dashboard/home pages, book catalog search, and administrator/librarian book management.
|
||||||
|
* The previous completed task explicitly left reader profile management, borrowing/returning/renewing/overdue handling, and full reports/statistics out of scope.
|
||||||
|
* Project requirements list these remaining core modules:
|
||||||
|
* Reader information management for profiles, borrowing eligibility, and contact information.
|
||||||
|
* Borrowing and return management for borrow, return, renew, overdue handling, and automatic collection status updates.
|
||||||
|
* Book search/statistics for borrowing rankings, inventory reports, and overdue reports.
|
||||||
|
* System maintenance/logs for key operation logs, backup support, and exception tracing.
|
||||||
|
* Existing permissions already include `manage_readers`, `manage_borrowing`, `view_reports`, `view_system_logs`, and `borrow_books`.
|
||||||
|
* Existing database schema already includes users, roles, permissions, system logs, book categories, and books.
|
||||||
|
* User selected option 1: reader information management.
|
||||||
|
|
||||||
|
## Assumptions (temporary)
|
||||||
|
|
||||||
|
* The next slice should stay consistent with the existing layered structure: JSP/CSS presentation -> Servlet controller -> Service -> DAO -> MySQL.
|
||||||
|
* The work should remain small enough to implement and verify in one Trellis task.
|
||||||
|
* The next module should build on the completed book and permission foundation instead of redesigning the application.
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
* None currently blocking. Default scope is administrator/librarian reader profile management.
|
||||||
|
|
||||||
|
## Requirements (evolving)
|
||||||
|
|
||||||
|
* Preserve existing login, role permission, dashboard, catalog, and book-management behavior.
|
||||||
|
* Implement reader information management for reader profiles, contact information, borrowing eligibility, and borrowing limits.
|
||||||
|
* Add a `readers` table to `src/main/resources/db/schema.sql`, linked to reader users when applicable.
|
||||||
|
* Support listing/searching readers by reader identifier, name, phone/email, and status.
|
||||||
|
* Provide administrator/librarian reader-management actions for creating, editing, and deleting or deactivating reader profiles.
|
||||||
|
* Track eligibility/status fields needed for later borrowing work, such as active/suspended status and max borrow count.
|
||||||
|
* Link reader management from the existing dashboard or role workspace for users with `MANAGE_READERS`.
|
||||||
|
* Protect reader-management routes with `MANAGE_READERS`; normal readers must not access management screens.
|
||||||
|
* Add or update database schema, DAO, service, servlet, JSP, and checks for the selected slice.
|
||||||
|
* Keep protected workflows guarded by the existing permission model.
|
||||||
|
* Add focused checks for service rules that can run without Tomcat.
|
||||||
|
|
||||||
|
## Acceptance Criteria (evolving)
|
||||||
|
|
||||||
|
* [x] Administrators and librarians can reach reader management pages and create, edit, and delete or deactivate reader profiles.
|
||||||
|
* [x] Reader profiles persist and load through DAO/service layers, not directly from JSPs.
|
||||||
|
* [x] Reader search supports reader identifier, name, contact fields, and status.
|
||||||
|
* [x] Readers cannot reach reader-management write or list actions.
|
||||||
|
* [x] Invalid identifier, name, contact, status, and borrow-limit values return clear user-facing errors.
|
||||||
|
* [x] Existing completed workflows continue to compile and pass checks.
|
||||||
|
* [x] Local compile/test checks pass or blockers are documented.
|
||||||
|
|
||||||
|
## Definition of Done (team quality bar)
|
||||||
|
|
||||||
|
* Tests added/updated where appropriate.
|
||||||
|
* Lint / typecheck / compile checks green where available.
|
||||||
|
* Docs/notes updated if behavior changes.
|
||||||
|
* Rollout/rollback considered if risky.
|
||||||
|
|
||||||
|
## Out of Scope (explicit)
|
||||||
|
|
||||||
|
* Replacing JSP/Servlet with another framework.
|
||||||
|
* Production deployment automation.
|
||||||
|
* Large visual redesign unrelated to the selected workflow.
|
||||||
|
* Implementing every remaining module in one task.
|
||||||
|
* Borrowing, returning, renewing, overdue handling, and borrowing statistics.
|
||||||
|
* Full reader self-service profile editing.
|
||||||
|
|
||||||
|
## Technical Approach
|
||||||
|
|
||||||
|
Use the same layered pattern established by the book module:
|
||||||
|
|
||||||
|
* `readers` schema and demo reader data in `src/main/resources/db/schema.sql`.
|
||||||
|
* Reader entity/search criteria, DAO, JDBC DAO, service interface, and service implementation.
|
||||||
|
* A `ReaderManagementServlet` mapped to `/readers`, `/readers/new`, `/readers/edit`, `/readers/update`, and `/readers/delete` or equivalent.
|
||||||
|
* JSPs under `WEB-INF/jsp/readers/` for list/search and form workflows.
|
||||||
|
* Service-level checks for validation, permission denial, duplicate identifiers, DAO failure fallback, and successful CRUD-like operations.
|
||||||
|
|
||||||
|
## Decision (ADR-lite)
|
||||||
|
|
||||||
|
**Context**: The project already has authentication, permissions, catalog search, and book management. Borrowing workflows need reader profile and eligibility data before they can be implemented cleanly.
|
||||||
|
|
||||||
|
**Decision**: Implement reader information management as the next focused slice.
|
||||||
|
|
||||||
|
**Consequences**: This adds the reader data foundation needed for future borrow/return/renew modules while keeping borrowing workflows and reports out of scope for this task.
|
||||||
|
|
||||||
|
## Technical Notes
|
||||||
|
|
||||||
|
* Current task directory: `.trellis/tasks/04-27-continue-program`.
|
||||||
|
* Relevant previous task: `.trellis/tasks/archive/2026-04/04-27-continue-improve-program/prd.md`.
|
||||||
|
* Project requirements source: `.trellis/tasks/archive/2026-04/00-bootstrap-guidelines/research/project-requirements.md`.
|
||||||
|
* Spec indexes available:
|
||||||
|
* `.trellis/spec/backend/index.md`
|
||||||
|
* `.trellis/spec/frontend/index.md`
|
||||||
|
* File inventory inspected:
|
||||||
|
* `README.md`
|
||||||
|
* `pom.xml`
|
||||||
|
* `src/main/resources/db/schema.sql`
|
||||||
|
* `src/main/webapp/WEB-INF/web.xml`
|
||||||
|
* `src/main/webapp/WEB-INF/jsp/dashboard.jsp`
|
||||||
|
* `src/main/webapp/WEB-INF/jsp/role-home.jsp`
|
||||||
|
* `src/main/java/com/mzh/library/filter/AuthorizationFilter.java`
|
||||||
|
* `src/main/java/com/mzh/library/entity/Permission.java`
|
||||||
|
* Existing book/auth service and check classes under `src/main/java` and `src/test/java`.
|
||||||
|
* Reader-management implementation should reuse:
|
||||||
|
* `Permission.MANAGE_READERS`
|
||||||
|
* Existing service result and DAO exception patterns
|
||||||
|
* Existing book-module controller/JSP/check structure as the closest local pattern
|
||||||
|
* Final verification notes:
|
||||||
|
* `trellis-implement` completed the reader-management slice across schema, DAO, service, servlet, JSP, CSS, README, spec, and checks.
|
||||||
|
* `trellis-check` fixed a phone-validation gap and added service assertions for missing contact, symbol-only phone, and DAO fallback on write paths.
|
||||||
|
* Available fallback Java compile and service checks passed.
|
||||||
|
* `mvn clean package` remains blocked because Maven is not installed in this environment and local servlet/JSTL jars were not available.
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"id": "continue-program",
|
||||||
|
"name": "continue-program",
|
||||||
|
"title": "brainstorm: 继续完成程序",
|
||||||
|
"description": "",
|
||||||
|
"status": "in_progress",
|
||||||
|
"dev_type": null,
|
||||||
|
"scope": null,
|
||||||
|
"package": null,
|
||||||
|
"priority": "P2",
|
||||||
|
"creator": "Zzzz",
|
||||||
|
"assignee": "Zzzz",
|
||||||
|
"createdAt": "2026-04-27",
|
||||||
|
"completedAt": null,
|
||||||
|
"branch": null,
|
||||||
|
"base_branch": "master",
|
||||||
|
"worktree_path": null,
|
||||||
|
"commit": null,
|
||||||
|
"pr_url": null,
|
||||||
|
"subtasks": [],
|
||||||
|
"children": [],
|
||||||
|
"parent": null,
|
||||||
|
"relatedFiles": [],
|
||||||
|
"notes": "",
|
||||||
|
"meta": {}
|
||||||
|
}
|
||||||
@@ -22,4 +22,4 @@ mvn clean package
|
|||||||
|
|
||||||
5. Deploy `target/library-management.war` to Tomcat.
|
5. Deploy `target/library-management.war` to Tomcat.
|
||||||
|
|
||||||
The login flow is intentionally limited to the first scaffold slice. It supports administrator, librarian, and reader roles, stores only a safe authenticated-user snapshot in the HTTP session, and keeps authentication work in Servlet -> Service -> DAO boundaries.
|
The implemented scaffold slices now cover login/permission checks, catalog and book management, and reader profile management. Authentication stores only a safe authenticated-user snapshot in the HTTP session, and business workflows stay in Servlet -> Service -> DAO boundaries.
|
||||||
|
|||||||
@@ -0,0 +1,375 @@
|
|||||||
|
package com.mzh.library.controller;
|
||||||
|
|
||||||
|
import com.mzh.library.dao.impl.JdbcReaderDao;
|
||||||
|
import com.mzh.library.entity.AuthenticatedUser;
|
||||||
|
import com.mzh.library.entity.Reader;
|
||||||
|
import com.mzh.library.entity.ReaderSearchCriteria;
|
||||||
|
import com.mzh.library.entity.ReaderStatus;
|
||||||
|
import com.mzh.library.service.ReaderService;
|
||||||
|
import com.mzh.library.service.ServiceResult;
|
||||||
|
import com.mzh.library.service.impl.ReaderServiceImpl;
|
||||||
|
import com.mzh.library.util.SessionAttributes;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import javax.servlet.ServletException;
|
||||||
|
import javax.servlet.http.HttpServlet;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import javax.servlet.http.HttpSession;
|
||||||
|
|
||||||
|
public class ReaderManagementServlet extends HttpServlet {
|
||||||
|
private static final String MANAGE_JSP = "/WEB-INF/jsp/readers/manage.jsp";
|
||||||
|
private static final String FORM_JSP = "/WEB-INF/jsp/readers/form.jsp";
|
||||||
|
private static final String UNAUTHORIZED_JSP = "/WEB-INF/jsp/auth/unauthorized.jsp";
|
||||||
|
private static final String FLASH_SUCCESS = "flashSuccess";
|
||||||
|
private static final String FLASH_ERROR = "flashError";
|
||||||
|
|
||||||
|
private ReaderService readerService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init() {
|
||||||
|
this.readerService = new ReaderServiceImpl(new JdbcReaderDao());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
|
||||||
|
String path = request.getServletPath();
|
||||||
|
if ("/readers/new".equals(path)) {
|
||||||
|
renderForm(request, response, "Create reader", "/readers", defaultReader(), Collections.emptyMap(),
|
||||||
|
Collections.emptyMap(), null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ("/readers/edit".equals(path)) {
|
||||||
|
showEditForm(request, response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!"/readers".equals(path)) {
|
||||||
|
response.sendError(HttpServletResponse.SC_NOT_FOUND);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showManagementList(request, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
|
||||||
|
String path = request.getServletPath();
|
||||||
|
if ("/readers".equals(path)) {
|
||||||
|
createReader(request, response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ("/readers/update".equals(path)) {
|
||||||
|
updateReader(request, response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ("/readers/delete".equals(path)) {
|
||||||
|
deactivateReader(request, response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
response.sendError(HttpServletResponse.SC_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showManagementList(HttpServletRequest request, HttpServletResponse response)
|
||||||
|
throws ServletException, IOException {
|
||||||
|
ReaderSearchCriteria criteria = searchCriteria(request);
|
||||||
|
request.setAttribute("criteria", criteria);
|
||||||
|
request.setAttribute("statuses", ReaderStatus.values());
|
||||||
|
applyFlash(request);
|
||||||
|
|
||||||
|
ServiceResult<List<Reader>> searchResult = readerService.searchReaders(criteria);
|
||||||
|
request.setAttribute("readers", searchResult.isSuccessful()
|
||||||
|
? searchResult.getData()
|
||||||
|
: Collections.emptyList());
|
||||||
|
if (!searchResult.isSuccessful()) {
|
||||||
|
request.setAttribute("errorMessage", searchResult.getMessage());
|
||||||
|
request.setAttribute("errors", searchResult.getErrors());
|
||||||
|
}
|
||||||
|
|
||||||
|
request.getRequestDispatcher(MANAGE_JSP).forward(request, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showEditForm(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
|
||||||
|
long id = requiredLong(request.getParameter("id"), -1L);
|
||||||
|
ServiceResult<Optional<Reader>> result = readerService.findReader(id);
|
||||||
|
if (!result.isSuccessful() || !result.getData().isPresent()) {
|
||||||
|
flashError(request, result.isSuccessful() ? "Reader profile was not found." : result.getMessage());
|
||||||
|
response.sendRedirect(request.getContextPath() + "/readers");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderForm(request, response, "Edit reader", "/readers/update", result.getData().get(),
|
||||||
|
Collections.emptyMap(), Collections.emptyMap(), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createReader(HttpServletRequest request, HttpServletResponse response)
|
||||||
|
throws ServletException, IOException {
|
||||||
|
ReaderForm form = readReaderForm(request, false);
|
||||||
|
if (!form.getErrors().isEmpty()) {
|
||||||
|
renderForm(request, response, "Create reader", "/readers", form.getReader(), form.getValues(),
|
||||||
|
form.getErrors(), "Please correct the highlighted reader fields.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ServiceResult<Long> result = readerService.createReader(currentUser(request), form.getReader());
|
||||||
|
if (!result.isSuccessful()) {
|
||||||
|
handleFormFailure(request, response, "Create reader", "/readers", form, result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
flashSuccess(request, result.getMessage());
|
||||||
|
response.sendRedirect(request.getContextPath() + "/readers");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateReader(HttpServletRequest request, HttpServletResponse response)
|
||||||
|
throws ServletException, IOException {
|
||||||
|
ReaderForm form = readReaderForm(request, true);
|
||||||
|
if (!form.getErrors().isEmpty()) {
|
||||||
|
renderForm(request, response, "Edit reader", "/readers/update", form.getReader(), form.getValues(),
|
||||||
|
form.getErrors(), "Please correct the highlighted reader fields.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ServiceResult<Void> result = readerService.updateReader(currentUser(request), form.getReader());
|
||||||
|
if (!result.isSuccessful()) {
|
||||||
|
handleFormFailure(request, response, "Edit reader", "/readers/update", form, result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
flashSuccess(request, result.getMessage());
|
||||||
|
response.sendRedirect(request.getContextPath() + "/readers");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deactivateReader(HttpServletRequest request, HttpServletResponse response)
|
||||||
|
throws IOException, ServletException {
|
||||||
|
long id = requiredLong(request.getParameter("id"), -1L);
|
||||||
|
ServiceResult<Void> result = readerService.deactivateReader(currentUser(request), id);
|
||||||
|
if (isPermissionDenied(result)) {
|
||||||
|
forwardDenied(request, response, result.getMessage());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (result.isSuccessful()) {
|
||||||
|
flashSuccess(request, result.getMessage());
|
||||||
|
} else {
|
||||||
|
flashError(request, result.getMessage());
|
||||||
|
}
|
||||||
|
response.sendRedirect(request.getContextPath() + "/readers");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleFormFailure(HttpServletRequest request, HttpServletResponse response, String title,
|
||||||
|
String action, ReaderForm form, ServiceResult<?> result)
|
||||||
|
throws ServletException, IOException {
|
||||||
|
if (isPermissionDenied(result)) {
|
||||||
|
forwardDenied(request, response, result.getMessage());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
renderForm(request, response, title, action, form.getReader(), form.getValues(), result.getErrors(),
|
||||||
|
result.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void renderForm(HttpServletRequest request, HttpServletResponse response, String title, String action,
|
||||||
|
Reader reader, Map<String, String> formValues, Map<String, String> errors,
|
||||||
|
String errorMessage)
|
||||||
|
throws ServletException, IOException {
|
||||||
|
request.setAttribute("statuses", ReaderStatus.values());
|
||||||
|
request.setAttribute("formTitle", title);
|
||||||
|
request.setAttribute("formAction", action);
|
||||||
|
request.setAttribute("reader", reader);
|
||||||
|
request.setAttribute("formValues", formValues);
|
||||||
|
request.setAttribute("errors", errors);
|
||||||
|
if (errorMessage != null && !errorMessage.isEmpty()) {
|
||||||
|
request.setAttribute("errorMessage", errorMessage);
|
||||||
|
}
|
||||||
|
request.getRequestDispatcher(FORM_JSP).forward(request, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ReaderForm readReaderForm(HttpServletRequest request, boolean requireId) {
|
||||||
|
Map<String, String> values = formValues(request);
|
||||||
|
Map<String, String> errors = new LinkedHashMap<>();
|
||||||
|
Reader reader = new Reader();
|
||||||
|
|
||||||
|
if (requireId) {
|
||||||
|
reader.setId(parseLong(values.get("id"), "id", "Select a valid reader.", errors));
|
||||||
|
}
|
||||||
|
reader.setIdentifier(values.get("identifier"));
|
||||||
|
reader.setUserId(optionalPositiveLong(values.get("userId"), "userId",
|
||||||
|
"Enter a valid linked account ID.", errors));
|
||||||
|
reader.setFullName(values.get("fullName"));
|
||||||
|
reader.setPhone(values.get("phone"));
|
||||||
|
reader.setEmail(values.get("email"));
|
||||||
|
reader.setMaxBorrowCount(parseInt(values.get("maxBorrowCount"), "maxBorrowCount",
|
||||||
|
"Enter a valid max borrow count.", errors));
|
||||||
|
|
||||||
|
try {
|
||||||
|
reader.setStatus(ReaderStatus.fromCode(values.get("status")));
|
||||||
|
} catch (IllegalArgumentException ex) {
|
||||||
|
errors.put("status", "Select a status.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ReaderForm(reader, values, errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, String> formValues(HttpServletRequest request) {
|
||||||
|
Map<String, String> values = new LinkedHashMap<>();
|
||||||
|
values.put("id", trim(request.getParameter("id")));
|
||||||
|
values.put("identifier", trim(request.getParameter("identifier")));
|
||||||
|
values.put("userId", trim(request.getParameter("userId")));
|
||||||
|
values.put("fullName", trim(request.getParameter("fullName")));
|
||||||
|
values.put("phone", trim(request.getParameter("phone")));
|
||||||
|
values.put("email", trim(request.getParameter("email")));
|
||||||
|
values.put("status", trim(request.getParameter("status")));
|
||||||
|
values.put("maxBorrowCount", trim(request.getParameter("maxBorrowCount")));
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ReaderSearchCriteria searchCriteria(HttpServletRequest request) {
|
||||||
|
return new ReaderSearchCriteria(
|
||||||
|
request.getParameter("identifier"),
|
||||||
|
request.getParameter("name"),
|
||||||
|
request.getParameter("contact"),
|
||||||
|
request.getParameter("status")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Reader defaultReader() {
|
||||||
|
Reader reader = new Reader();
|
||||||
|
reader.setStatus(ReaderStatus.ACTIVE);
|
||||||
|
reader.setMaxBorrowCount(5);
|
||||||
|
return reader;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long optionalPositiveLong(String value, String field, String message, Map<String, String> errors) {
|
||||||
|
String trimmed = trim(value);
|
||||||
|
if (trimmed.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
long parsed = Long.parseLong(trimmed);
|
||||||
|
if (parsed <= 0) {
|
||||||
|
errors.put(field, message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
} catch (NumberFormatException ex) {
|
||||||
|
errors.put(field, message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private long parseLong(String value, String field, String message, Map<String, String> errors) {
|
||||||
|
String trimmed = trim(value);
|
||||||
|
if (trimmed.isEmpty()) {
|
||||||
|
errors.put(field, message);
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
long parsed = Long.parseLong(trimmed);
|
||||||
|
if (parsed <= 0) {
|
||||||
|
errors.put(field, message);
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
} catch (NumberFormatException ex) {
|
||||||
|
errors.put(field, message);
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int parseInt(String value, String field, String message, Map<String, String> errors) {
|
||||||
|
String trimmed = trim(value);
|
||||||
|
if (trimmed.isEmpty()) {
|
||||||
|
errors.put(field, message);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Integer.parseInt(trimmed);
|
||||||
|
} catch (NumberFormatException ex) {
|
||||||
|
errors.put(field, message);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private long requiredLong(String value, long fallback) {
|
||||||
|
try {
|
||||||
|
long parsed = Long.parseLong(trim(value));
|
||||||
|
return parsed > 0 ? parsed : fallback;
|
||||||
|
} catch (NumberFormatException ex) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isPermissionDenied(ServiceResult<?> result) {
|
||||||
|
return !result.isSuccessful() && "You do not have permission to manage readers.".equals(result.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void forwardDenied(HttpServletRequest request, HttpServletResponse response, String message)
|
||||||
|
throws ServletException, IOException {
|
||||||
|
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
|
||||||
|
request.setAttribute("errorMessage", message);
|
||||||
|
request.getRequestDispatcher(UNAUTHORIZED_JSP).forward(request, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
private AuthenticatedUser currentUser(HttpServletRequest request) {
|
||||||
|
HttpSession session = request.getSession(false);
|
||||||
|
Object value = session == null ? null : session.getAttribute(SessionAttributes.AUTHENTICATED_USER);
|
||||||
|
return value instanceof AuthenticatedUser ? (AuthenticatedUser) value : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyFlash(HttpServletRequest request) {
|
||||||
|
HttpSession session = request.getSession(false);
|
||||||
|
if (session == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
moveFlash(session, request, FLASH_SUCCESS, "successMessage");
|
||||||
|
moveFlash(session, request, FLASH_ERROR, "errorMessage");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void moveFlash(HttpSession session, HttpServletRequest request, String sessionKey, String requestKey) {
|
||||||
|
Object value = session.getAttribute(sessionKey);
|
||||||
|
if (value != null) {
|
||||||
|
request.setAttribute(requestKey, value);
|
||||||
|
session.removeAttribute(sessionKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void flashSuccess(HttpServletRequest request, String message) {
|
||||||
|
request.getSession().setAttribute(FLASH_SUCCESS, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void flashError(HttpServletRequest request, String message) {
|
||||||
|
request.getSession().setAttribute(FLASH_ERROR, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String trim(String value) {
|
||||||
|
return value == null ? "" : value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class ReaderForm {
|
||||||
|
private final Reader reader;
|
||||||
|
private final Map<String, String> values;
|
||||||
|
private final Map<String, String> errors;
|
||||||
|
|
||||||
|
private ReaderForm(Reader reader, Map<String, String> values, Map<String, String> errors) {
|
||||||
|
this.reader = reader;
|
||||||
|
this.values = values;
|
||||||
|
this.errors = errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Reader getReader() {
|
||||||
|
return reader;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, String> getValues() {
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, String> getErrors() {
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.mzh.library.dao;
|
||||||
|
|
||||||
|
import com.mzh.library.entity.Reader;
|
||||||
|
import com.mzh.library.entity.ReaderSearchCriteria;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public interface ReaderDao {
|
||||||
|
List<Reader> search(ReaderSearchCriteria criteria);
|
||||||
|
|
||||||
|
Optional<Reader> findById(long id);
|
||||||
|
|
||||||
|
Optional<Reader> findByIdentifier(String identifier);
|
||||||
|
|
||||||
|
Optional<Reader> findByUserId(long userId);
|
||||||
|
|
||||||
|
long create(Reader reader);
|
||||||
|
|
||||||
|
boolean update(Reader reader);
|
||||||
|
|
||||||
|
boolean deactivate(long id);
|
||||||
|
}
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
package com.mzh.library.dao.impl;
|
||||||
|
|
||||||
|
import com.mzh.library.dao.ReaderDao;
|
||||||
|
import com.mzh.library.entity.Reader;
|
||||||
|
import com.mzh.library.entity.ReaderSearchCriteria;
|
||||||
|
import com.mzh.library.entity.ReaderStatus;
|
||||||
|
import com.mzh.library.exception.DaoException;
|
||||||
|
import com.mzh.library.util.JdbcUtil;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.sql.Statement;
|
||||||
|
import java.sql.Timestamp;
|
||||||
|
import java.sql.Types;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public class JdbcReaderDao implements ReaderDao {
|
||||||
|
private static final String READER_COLUMNS = ""
|
||||||
|
+ "r.id, r.reader_identifier, r.user_id, u.username, r.full_name, r.phone, r.email, "
|
||||||
|
+ "r.status, r.max_borrow_count, r.created_at, r.updated_at ";
|
||||||
|
|
||||||
|
private static final String READER_FROM = ""
|
||||||
|
+ "FROM readers r "
|
||||||
|
+ "LEFT JOIN users u ON u.id = r.user_id ";
|
||||||
|
|
||||||
|
private static final String FIND_BY_ID = "SELECT " + READER_COLUMNS + READER_FROM + "WHERE r.id = ?";
|
||||||
|
|
||||||
|
private static final String FIND_BY_IDENTIFIER = "SELECT " + READER_COLUMNS + READER_FROM
|
||||||
|
+ "WHERE r.reader_identifier = ?";
|
||||||
|
|
||||||
|
private static final String FIND_BY_USER_ID = "SELECT " + READER_COLUMNS + READER_FROM
|
||||||
|
+ "WHERE r.user_id = ?";
|
||||||
|
|
||||||
|
private static final String CREATE = ""
|
||||||
|
+ "INSERT INTO readers "
|
||||||
|
+ "(reader_identifier, user_id, full_name, phone, email, status, max_borrow_count) "
|
||||||
|
+ "VALUES (?, ?, ?, ?, ?, ?, ?)";
|
||||||
|
|
||||||
|
private static final String UPDATE = ""
|
||||||
|
+ "UPDATE readers "
|
||||||
|
+ "SET reader_identifier = ?, user_id = ?, full_name = ?, phone = ?, email = ?, "
|
||||||
|
+ "status = ?, max_borrow_count = ? "
|
||||||
|
+ "WHERE id = ?";
|
||||||
|
|
||||||
|
private static final String DEACTIVATE = "UPDATE readers SET status = ? WHERE id = ?";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Reader> search(ReaderSearchCriteria criteria) {
|
||||||
|
List<Object> parameters = new ArrayList<>();
|
||||||
|
StringBuilder sql = new StringBuilder("SELECT ")
|
||||||
|
.append(READER_COLUMNS)
|
||||||
|
.append(READER_FROM)
|
||||||
|
.append("WHERE 1 = 1 ");
|
||||||
|
|
||||||
|
appendLike(sql, parameters, "r.reader_identifier", criteria.getIdentifier());
|
||||||
|
appendLike(sql, parameters, "r.full_name", criteria.getName());
|
||||||
|
appendContact(sql, parameters, criteria.getContact());
|
||||||
|
if (criteria.getStatusCode() != null && !criteria.getStatusCode().isEmpty()) {
|
||||||
|
sql.append("AND r.status = ? ");
|
||||||
|
parameters.add(criteria.getStatusCode());
|
||||||
|
}
|
||||||
|
sql.append("ORDER BY r.full_name, r.reader_identifier");
|
||||||
|
|
||||||
|
try (Connection connection = JdbcUtil.getConnection();
|
||||||
|
PreparedStatement statement = connection.prepareStatement(sql.toString())) {
|
||||||
|
bind(statement, parameters);
|
||||||
|
try (ResultSet resultSet = statement.executeQuery()) {
|
||||||
|
List<Reader> readers = new ArrayList<>();
|
||||||
|
while (resultSet.next()) {
|
||||||
|
readers.add(mapReader(resultSet));
|
||||||
|
}
|
||||||
|
return readers;
|
||||||
|
}
|
||||||
|
} catch (SQLException | IllegalArgumentException ex) {
|
||||||
|
throw new DaoException("Unable to search readers", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<Reader> findById(long id) {
|
||||||
|
try (Connection connection = JdbcUtil.getConnection();
|
||||||
|
PreparedStatement statement = connection.prepareStatement(FIND_BY_ID)) {
|
||||||
|
statement.setLong(1, id);
|
||||||
|
try (ResultSet resultSet = statement.executeQuery()) {
|
||||||
|
return resultSet.next() ? Optional.of(mapReader(resultSet)) : Optional.empty();
|
||||||
|
}
|
||||||
|
} catch (SQLException | IllegalArgumentException ex) {
|
||||||
|
throw new DaoException("Unable to load reader by id", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<Reader> findByIdentifier(String identifier) {
|
||||||
|
try (Connection connection = JdbcUtil.getConnection();
|
||||||
|
PreparedStatement statement = connection.prepareStatement(FIND_BY_IDENTIFIER)) {
|
||||||
|
statement.setString(1, identifier);
|
||||||
|
try (ResultSet resultSet = statement.executeQuery()) {
|
||||||
|
return resultSet.next() ? Optional.of(mapReader(resultSet)) : Optional.empty();
|
||||||
|
}
|
||||||
|
} catch (SQLException | IllegalArgumentException ex) {
|
||||||
|
throw new DaoException("Unable to load reader by identifier", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<Reader> findByUserId(long userId) {
|
||||||
|
try (Connection connection = JdbcUtil.getConnection();
|
||||||
|
PreparedStatement statement = connection.prepareStatement(FIND_BY_USER_ID)) {
|
||||||
|
statement.setLong(1, userId);
|
||||||
|
try (ResultSet resultSet = statement.executeQuery()) {
|
||||||
|
return resultSet.next() ? Optional.of(mapReader(resultSet)) : Optional.empty();
|
||||||
|
}
|
||||||
|
} catch (SQLException | IllegalArgumentException ex) {
|
||||||
|
throw new DaoException("Unable to load reader by user id", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long create(Reader reader) {
|
||||||
|
try (Connection connection = JdbcUtil.getConnection();
|
||||||
|
PreparedStatement statement = connection.prepareStatement(CREATE, Statement.RETURN_GENERATED_KEYS)) {
|
||||||
|
bindReader(statement, reader);
|
||||||
|
statement.executeUpdate();
|
||||||
|
|
||||||
|
try (ResultSet generatedKeys = statement.getGeneratedKeys()) {
|
||||||
|
if (generatedKeys.next()) {
|
||||||
|
return generatedKeys.getLong(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new DaoException("Unable to read generated reader id", null);
|
||||||
|
} catch (SQLException ex) {
|
||||||
|
throw new DaoException("Unable to create reader", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean update(Reader reader) {
|
||||||
|
try (Connection connection = JdbcUtil.getConnection();
|
||||||
|
PreparedStatement statement = connection.prepareStatement(UPDATE)) {
|
||||||
|
bindReader(statement, reader);
|
||||||
|
statement.setLong(8, reader.getId());
|
||||||
|
return statement.executeUpdate() == 1;
|
||||||
|
} catch (SQLException ex) {
|
||||||
|
throw new DaoException("Unable to update reader", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean deactivate(long id) {
|
||||||
|
try (Connection connection = JdbcUtil.getConnection();
|
||||||
|
PreparedStatement statement = connection.prepareStatement(DEACTIVATE)) {
|
||||||
|
statement.setString(1, ReaderStatus.INACTIVE.getCode());
|
||||||
|
statement.setLong(2, id);
|
||||||
|
return statement.executeUpdate() == 1;
|
||||||
|
} catch (SQLException ex) {
|
||||||
|
throw new DaoException("Unable to deactivate reader", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendLike(StringBuilder sql, List<Object> parameters, String column, String value) {
|
||||||
|
if (value == null || value.trim().isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sql.append("AND ").append(column).append(" LIKE ? ");
|
||||||
|
parameters.add("%" + value.trim() + "%");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendContact(StringBuilder sql, List<Object> parameters, String value) {
|
||||||
|
if (value == null || value.trim().isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sql.append("AND (r.phone LIKE ? OR r.email LIKE ?) ");
|
||||||
|
String filter = "%" + value.trim() + "%";
|
||||||
|
parameters.add(filter);
|
||||||
|
parameters.add(filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void bind(PreparedStatement statement, List<Object> parameters) throws SQLException {
|
||||||
|
for (int i = 0; i < parameters.size(); i++) {
|
||||||
|
Object value = parameters.get(i);
|
||||||
|
statement.setString(i + 1, value.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void bindReader(PreparedStatement statement, Reader reader) throws SQLException {
|
||||||
|
statement.setString(1, reader.getIdentifier());
|
||||||
|
if (reader.getUserId() == null) {
|
||||||
|
statement.setNull(2, Types.BIGINT);
|
||||||
|
} else {
|
||||||
|
statement.setLong(2, reader.getUserId());
|
||||||
|
}
|
||||||
|
statement.setString(3, reader.getFullName());
|
||||||
|
statement.setString(4, reader.getPhone());
|
||||||
|
statement.setString(5, reader.getEmail());
|
||||||
|
statement.setString(6, reader.getStatus().getCode());
|
||||||
|
statement.setInt(7, reader.getMaxBorrowCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Reader mapReader(ResultSet resultSet) throws SQLException {
|
||||||
|
Reader reader = new Reader();
|
||||||
|
reader.setId(resultSet.getLong("id"));
|
||||||
|
reader.setIdentifier(resultSet.getString("reader_identifier"));
|
||||||
|
long userId = resultSet.getLong("user_id");
|
||||||
|
reader.setUserId(resultSet.wasNull() ? null : userId);
|
||||||
|
reader.setUsername(resultSet.getString("username"));
|
||||||
|
reader.setFullName(resultSet.getString("full_name"));
|
||||||
|
reader.setPhone(resultSet.getString("phone"));
|
||||||
|
reader.setEmail(resultSet.getString("email"));
|
||||||
|
reader.setStatus(ReaderStatus.fromCode(resultSet.getString("status")));
|
||||||
|
reader.setMaxBorrowCount(resultSet.getInt("max_borrow_count"));
|
||||||
|
reader.setCreatedAt(toLocalDateTime(resultSet.getTimestamp("created_at")));
|
||||||
|
reader.setUpdatedAt(toLocalDateTime(resultSet.getTimestamp("updated_at")));
|
||||||
|
return reader;
|
||||||
|
}
|
||||||
|
|
||||||
|
private LocalDateTime toLocalDateTime(Timestamp timestamp) {
|
||||||
|
return timestamp == null ? null : timestamp.toLocalDateTime();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package com.mzh.library.entity;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
public class Reader {
|
||||||
|
private long id;
|
||||||
|
private String identifier;
|
||||||
|
private Long userId;
|
||||||
|
private String username;
|
||||||
|
private String fullName;
|
||||||
|
private String phone;
|
||||||
|
private String email;
|
||||||
|
private ReaderStatus status;
|
||||||
|
private int maxBorrowCount;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
|
||||||
|
public long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getIdentifier() {
|
||||||
|
return identifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIdentifier(String identifier) {
|
||||||
|
this.identifier = identifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getUserId() {
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUserId(Long userId) {
|
||||||
|
this.userId = userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUsername() {
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUsername(String username) {
|
||||||
|
this.username = username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFullName() {
|
||||||
|
return fullName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFullName(String fullName) {
|
||||||
|
this.fullName = fullName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPhone() {
|
||||||
|
return phone;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPhone(String phone) {
|
||||||
|
this.phone = phone;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getEmail() {
|
||||||
|
return email;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEmail(String email) {
|
||||||
|
this.email = email;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReaderStatus getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStatus(ReaderStatus status) {
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMaxBorrowCount() {
|
||||||
|
return maxBorrowCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMaxBorrowCount(int maxBorrowCount) {
|
||||||
|
this.maxBorrowCount = maxBorrowCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getCreatedAt() {
|
||||||
|
return createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreatedAt(LocalDateTime createdAt) {
|
||||||
|
this.createdAt = createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getUpdatedAt() {
|
||||||
|
return updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUpdatedAt(LocalDateTime updatedAt) {
|
||||||
|
this.updatedAt = updatedAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package com.mzh.library.entity;
|
||||||
|
|
||||||
|
public class ReaderSearchCriteria {
|
||||||
|
private String identifier;
|
||||||
|
private String name;
|
||||||
|
private String contact;
|
||||||
|
private String statusCode;
|
||||||
|
|
||||||
|
public ReaderSearchCriteria() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReaderSearchCriteria(String identifier, String name, String contact, String statusCode) {
|
||||||
|
this.identifier = trim(identifier);
|
||||||
|
this.name = trim(name);
|
||||||
|
this.contact = trim(contact);
|
||||||
|
this.statusCode = trim(statusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getIdentifier() {
|
||||||
|
return identifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIdentifier(String identifier) {
|
||||||
|
this.identifier = trim(identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setName(String name) {
|
||||||
|
this.name = trim(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getContact() {
|
||||||
|
return contact;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setContact(String contact) {
|
||||||
|
this.contact = trim(contact);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getStatusCode() {
|
||||||
|
return statusCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStatusCode(String statusCode) {
|
||||||
|
this.statusCode = trim(statusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String trim(String value) {
|
||||||
|
return value == null ? "" : value.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package com.mzh.library.entity;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
public enum ReaderStatus {
|
||||||
|
ACTIVE("active", "Active"),
|
||||||
|
SUSPENDED("suspended", "Suspended"),
|
||||||
|
INACTIVE("inactive", "Inactive");
|
||||||
|
|
||||||
|
private final String code;
|
||||||
|
private final String displayName;
|
||||||
|
|
||||||
|
ReaderStatus(String code, String displayName) {
|
||||||
|
this.code = code;
|
||||||
|
this.displayName = displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCode() {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDisplayName() {
|
||||||
|
return displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ReaderStatus fromCode(String code) {
|
||||||
|
if (code == null || code.trim().isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("Reader status is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
String normalized = code.trim().toLowerCase(Locale.ROOT);
|
||||||
|
for (ReaderStatus status : values()) {
|
||||||
|
if (status.code.equals(normalized)) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IllegalArgumentException("Unsupported reader status: " + code);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,6 +26,7 @@ public class AuthorizationFilter implements Filter {
|
|||||||
private static final String UNAUTHORIZED_JSP = "/WEB-INF/jsp/auth/unauthorized.jsp";
|
private static final String UNAUTHORIZED_JSP = "/WEB-INF/jsp/auth/unauthorized.jsp";
|
||||||
private static final List<PathRule> RULES = Arrays.asList(
|
private static final List<PathRule> RULES = Arrays.asList(
|
||||||
new PathRule("/books", Permission.MANAGE_BOOKS),
|
new PathRule("/books", Permission.MANAGE_BOOKS),
|
||||||
|
new PathRule("/readers", Permission.MANAGE_READERS),
|
||||||
new PathRule("/catalog", Permission.VIEW_CATALOG),
|
new PathRule("/catalog", Permission.VIEW_CATALOG),
|
||||||
new PathRule("/admin", Permission.MANAGE_USERS),
|
new PathRule("/admin", Permission.MANAGE_USERS),
|
||||||
new PathRule("/librarian", Permission.MANAGE_BORROWING),
|
new PathRule("/librarian", Permission.MANAGE_BORROWING),
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.mzh.library.service;
|
||||||
|
|
||||||
|
import com.mzh.library.entity.AuthenticatedUser;
|
||||||
|
import com.mzh.library.entity.Reader;
|
||||||
|
import com.mzh.library.entity.ReaderSearchCriteria;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public interface ReaderService {
|
||||||
|
ServiceResult<List<Reader>> searchReaders(ReaderSearchCriteria criteria);
|
||||||
|
|
||||||
|
ServiceResult<Optional<Reader>> findReader(long id);
|
||||||
|
|
||||||
|
ServiceResult<Long> createReader(AuthenticatedUser actor, Reader reader);
|
||||||
|
|
||||||
|
ServiceResult<Void> updateReader(AuthenticatedUser actor, Reader reader);
|
||||||
|
|
||||||
|
ServiceResult<Void> deactivateReader(AuthenticatedUser actor, long id);
|
||||||
|
}
|
||||||
@@ -0,0 +1,245 @@
|
|||||||
|
package com.mzh.library.service.impl;
|
||||||
|
|
||||||
|
import com.mzh.library.dao.ReaderDao;
|
||||||
|
import com.mzh.library.entity.AuthenticatedUser;
|
||||||
|
import com.mzh.library.entity.Permission;
|
||||||
|
import com.mzh.library.entity.Reader;
|
||||||
|
import com.mzh.library.entity.ReaderSearchCriteria;
|
||||||
|
import com.mzh.library.entity.ReaderStatus;
|
||||||
|
import com.mzh.library.exception.DaoException;
|
||||||
|
import com.mzh.library.service.PermissionPolicy;
|
||||||
|
import com.mzh.library.service.ReaderService;
|
||||||
|
import com.mzh.library.service.ServiceResult;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
public class ReaderServiceImpl implements ReaderService {
|
||||||
|
private static final Logger LOGGER = Logger.getLogger(ReaderServiceImpl.class.getName());
|
||||||
|
private static final String UNAVAILABLE_MESSAGE =
|
||||||
|
"Reader service is temporarily unavailable. Please try again later.";
|
||||||
|
private static final String VALIDATION_MESSAGE = "Please correct the highlighted reader fields.";
|
||||||
|
private static final String SEARCH_VALIDATION_MESSAGE = "Please correct the reader search filters.";
|
||||||
|
private static final String DENIED_MESSAGE = "You do not have permission to manage readers.";
|
||||||
|
private static final int MAX_BORROW_LIMIT = 50;
|
||||||
|
private static final Pattern PHONE_PATTERN = Pattern.compile("(?=.*\\d)[0-9+()\\-\\s]{6,32}");
|
||||||
|
private static final Pattern EMAIL_PATTERN = Pattern.compile("^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$");
|
||||||
|
|
||||||
|
private final ReaderDao readerDao;
|
||||||
|
private final PermissionPolicy permissionPolicy;
|
||||||
|
|
||||||
|
public ReaderServiceImpl(ReaderDao readerDao) {
|
||||||
|
this(readerDao, new PermissionPolicy());
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReaderServiceImpl(ReaderDao readerDao, PermissionPolicy permissionPolicy) {
|
||||||
|
this.readerDao = readerDao;
|
||||||
|
this.permissionPolicy = permissionPolicy;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ServiceResult<List<Reader>> searchReaders(ReaderSearchCriteria criteria) {
|
||||||
|
ReaderSearchCriteria normalized = criteria == null ? new ReaderSearchCriteria() : criteria;
|
||||||
|
Map<String, String> errors = validateSearch(normalized);
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return ServiceResult.validationFailure(SEARCH_VALIDATION_MESSAGE, errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return ServiceResult.success(readerDao.search(normalized));
|
||||||
|
} catch (DaoException ex) {
|
||||||
|
LOGGER.log(Level.SEVERE, "Unable to search readers", ex);
|
||||||
|
return ServiceResult.failure(UNAVAILABLE_MESSAGE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ServiceResult<Optional<Reader>> findReader(long id) {
|
||||||
|
if (id <= 0) {
|
||||||
|
return ServiceResult.failure("Select a valid reader.");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return ServiceResult.success(readerDao.findById(id));
|
||||||
|
} catch (DaoException ex) {
|
||||||
|
LOGGER.log(Level.SEVERE, "Unable to load reader id=" + id, ex);
|
||||||
|
return ServiceResult.failure(UNAVAILABLE_MESSAGE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ServiceResult<Long> createReader(AuthenticatedUser actor, Reader reader) {
|
||||||
|
if (!canManageReaders(actor)) {
|
||||||
|
return ServiceResult.failure(DENIED_MESSAGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
normalize(reader);
|
||||||
|
Map<String, String> errors = validate(reader, false);
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return ServiceResult.validationFailure(VALIDATION_MESSAGE, errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (readerDao.findByIdentifier(reader.getIdentifier()).isPresent()) {
|
||||||
|
errors.put("identifier", "Reader identifier is already in use.");
|
||||||
|
return ServiceResult.validationFailure(VALIDATION_MESSAGE, errors);
|
||||||
|
}
|
||||||
|
if (reader.getUserId() != null && readerDao.findByUserId(reader.getUserId()).isPresent()) {
|
||||||
|
errors.put("userId", "Linked account is already assigned to a reader profile.");
|
||||||
|
return ServiceResult.validationFailure(VALIDATION_MESSAGE, errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
long id = readerDao.create(reader);
|
||||||
|
LOGGER.info("Created reader id=" + id + " actorId=" + actor.getId());
|
||||||
|
return ServiceResult.success(id, "Reader profile created.");
|
||||||
|
} catch (DaoException ex) {
|
||||||
|
LOGGER.log(Level.SEVERE, "Unable to create reader actorId=" + actor.getId(), ex);
|
||||||
|
return ServiceResult.failure(UNAVAILABLE_MESSAGE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ServiceResult<Void> updateReader(AuthenticatedUser actor, Reader reader) {
|
||||||
|
if (!canManageReaders(actor)) {
|
||||||
|
return ServiceResult.failure(DENIED_MESSAGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
normalize(reader);
|
||||||
|
Map<String, String> errors = validate(reader, true);
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return ServiceResult.validationFailure(VALIDATION_MESSAGE, errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Optional<Reader> existingWithIdentifier = readerDao.findByIdentifier(reader.getIdentifier());
|
||||||
|
if (existingWithIdentifier.isPresent() && existingWithIdentifier.get().getId() != reader.getId()) {
|
||||||
|
errors.put("identifier", "Reader identifier is already in use.");
|
||||||
|
return ServiceResult.validationFailure(VALIDATION_MESSAGE, errors);
|
||||||
|
}
|
||||||
|
if (reader.getUserId() != null) {
|
||||||
|
Optional<Reader> existingWithUser = readerDao.findByUserId(reader.getUserId());
|
||||||
|
if (existingWithUser.isPresent() && existingWithUser.get().getId() != reader.getId()) {
|
||||||
|
errors.put("userId", "Linked account is already assigned to a reader profile.");
|
||||||
|
return ServiceResult.validationFailure(VALIDATION_MESSAGE, errors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!readerDao.update(reader)) {
|
||||||
|
return ServiceResult.failure("Reader profile was not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGGER.info("Updated reader id=" + reader.getId() + " actorId=" + actor.getId());
|
||||||
|
return ServiceResult.success(null, "Reader profile updated.");
|
||||||
|
} catch (DaoException ex) {
|
||||||
|
LOGGER.log(Level.SEVERE, "Unable to update reader id=" + reader.getId() + " actorId=" + actor.getId(), ex);
|
||||||
|
return ServiceResult.failure(UNAVAILABLE_MESSAGE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ServiceResult<Void> deactivateReader(AuthenticatedUser actor, long id) {
|
||||||
|
if (!canManageReaders(actor)) {
|
||||||
|
return ServiceResult.failure(DENIED_MESSAGE);
|
||||||
|
}
|
||||||
|
if (id <= 0) {
|
||||||
|
return ServiceResult.failure("Select a valid reader.");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!readerDao.deactivate(id)) {
|
||||||
|
return ServiceResult.failure("Reader profile was not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGGER.info("Deactivated reader id=" + id + " actorId=" + actor.getId());
|
||||||
|
return ServiceResult.success(null, "Reader profile deactivated.");
|
||||||
|
} catch (DaoException ex) {
|
||||||
|
LOGGER.log(Level.SEVERE, "Unable to deactivate reader id=" + id + " actorId=" + actor.getId(), ex);
|
||||||
|
return ServiceResult.failure(UNAVAILABLE_MESSAGE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean canManageReaders(AuthenticatedUser actor) {
|
||||||
|
return actor != null && permissionPolicy.allows(actor.getRole(), Permission.MANAGE_READERS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void normalize(Reader reader) {
|
||||||
|
if (reader == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reader.setIdentifier(trim(reader.getIdentifier()));
|
||||||
|
reader.setFullName(trim(reader.getFullName()));
|
||||||
|
reader.setPhone(trim(reader.getPhone()));
|
||||||
|
reader.setEmail(trim(reader.getEmail()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, String> validateSearch(ReaderSearchCriteria criteria) {
|
||||||
|
Map<String, String> errors = new LinkedHashMap<>();
|
||||||
|
if (criteria.getStatusCode() != null && !criteria.getStatusCode().isEmpty()) {
|
||||||
|
try {
|
||||||
|
criteria.setStatusCode(ReaderStatus.fromCode(criteria.getStatusCode()).getCode());
|
||||||
|
} catch (IllegalArgumentException ex) {
|
||||||
|
errors.put("status", "Select a valid status.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, String> validate(Reader reader, boolean requireId) {
|
||||||
|
Map<String, String> errors = new LinkedHashMap<>();
|
||||||
|
if (reader == null) {
|
||||||
|
errors.put("reader", "Reader details are required.");
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requireId && reader.getId() <= 0) {
|
||||||
|
errors.put("id", "Select a valid reader.");
|
||||||
|
}
|
||||||
|
requireLength(errors, "identifier", reader.getIdentifier(), "Reader identifier", 64);
|
||||||
|
requireLength(errors, "fullName", reader.getFullName(), "Full name", 100);
|
||||||
|
if (reader.getUserId() != null && reader.getUserId() <= 0) {
|
||||||
|
errors.put("userId", "Linked account ID must be positive.");
|
||||||
|
}
|
||||||
|
validateContact(errors, reader);
|
||||||
|
if (reader.getStatus() == null) {
|
||||||
|
errors.put("status", "Select a status.");
|
||||||
|
}
|
||||||
|
if (reader.getMaxBorrowCount() < 1 || reader.getMaxBorrowCount() > MAX_BORROW_LIMIT) {
|
||||||
|
errors.put("maxBorrowCount", "Max borrow count must be between 1 and " + MAX_BORROW_LIMIT + ".");
|
||||||
|
}
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateContact(Map<String, String> errors, Reader reader) {
|
||||||
|
String phone = reader.getPhone();
|
||||||
|
String email = reader.getEmail();
|
||||||
|
if ((phone == null || phone.isEmpty()) && (email == null || email.isEmpty())) {
|
||||||
|
errors.put("phone", "Phone or email is required.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (phone != null && !phone.isEmpty() && !PHONE_PATTERN.matcher(phone).matches()) {
|
||||||
|
errors.put("phone", "Phone must include a digit and use 6 to 32 digits or common phone symbols.");
|
||||||
|
}
|
||||||
|
if (email != null && !email.isEmpty() && !EMAIL_PATTERN.matcher(email).matches()) {
|
||||||
|
errors.put("email", "Email must be a valid address.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requireLength(Map<String, String> errors, String field, String value, String label, int maxLength) {
|
||||||
|
if (value == null || value.isEmpty()) {
|
||||||
|
errors.put(field, label + " is required.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (value.length() > maxLength) {
|
||||||
|
errors.put(field, label + " must be " + maxLength + " characters or fewer.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String trim(String value) {
|
||||||
|
return value == null ? "" : value.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -57,6 +57,31 @@ CREATE TABLE IF NOT EXISTS system_logs (
|
|||||||
KEY idx_system_logs_created_at (created_at)
|
KEY idx_system_logs_created_at (created_at)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS readers (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
reader_identifier VARCHAR(64) NOT NULL,
|
||||||
|
user_id BIGINT NULL,
|
||||||
|
full_name VARCHAR(100) NOT NULL,
|
||||||
|
phone VARCHAR(32) NULL,
|
||||||
|
email VARCHAR(120) NULL,
|
||||||
|
status VARCHAR(32) NOT NULL DEFAULT 'active',
|
||||||
|
max_borrow_count INT NOT NULL DEFAULT 5,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE KEY uk_readers_identifier (reader_identifier),
|
||||||
|
UNIQUE KEY uk_readers_user_id (user_id),
|
||||||
|
KEY idx_readers_full_name (full_name),
|
||||||
|
KEY idx_readers_phone (phone),
|
||||||
|
KEY idx_readers_email (email),
|
||||||
|
KEY idx_readers_status (status),
|
||||||
|
CONSTRAINT fk_readers_user
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users (id),
|
||||||
|
CONSTRAINT chk_readers_status
|
||||||
|
CHECK (status IN ('active', 'suspended', 'inactive')),
|
||||||
|
CONSTRAINT chk_readers_max_borrow_count
|
||||||
|
CHECK (max_borrow_count BETWEEN 1 AND 50)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS book_categories (
|
CREATE TABLE IF NOT EXISTS book_categories (
|
||||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||||
name VARCHAR(96) NOT NULL,
|
name VARCHAR(96) NOT NULL,
|
||||||
@@ -137,6 +162,18 @@ INSERT IGNORE INTO users (username, password_hash, display_name, role_code, acti
|
|||||||
('librarian', 'pbkdf2_sha256$60000$bXpoLWxpYnJhcmlhbi1kZW1vLXNhbHQ=$StIdJGDRIiF4aCr+qKuwvob5sL3+6j1caF2sQNqFi78=', 'Library Staff', 'librarian', 1),
|
('librarian', 'pbkdf2_sha256$60000$bXpoLWxpYnJhcmlhbi1kZW1vLXNhbHQ=$StIdJGDRIiF4aCr+qKuwvob5sL3+6j1caF2sQNqFi78=', 'Library Staff', 'librarian', 1),
|
||||||
('reader', 'pbkdf2_sha256$60000$bXpoLXJlYWRlci1kZW1vLXNhbHQ=$iaiZPGhaIQ+2R2o9UQRj6wsrmYSJ4efqS3jCzM/XU7g=', 'Demo Reader', 'reader', 1);
|
('reader', 'pbkdf2_sha256$60000$bXpoLXJlYWRlci1kZW1vLXNhbHQ=$iaiZPGhaIQ+2R2o9UQRj6wsrmYSJ4efqS3jCzM/XU7g=', 'Demo Reader', 'reader', 1);
|
||||||
|
|
||||||
|
INSERT INTO readers (reader_identifier, user_id, full_name, phone, email, status, max_borrow_count) VALUES
|
||||||
|
('RD-0001', (SELECT id FROM users WHERE username = 'reader'), 'Demo Reader', '13800000000',
|
||||||
|
'reader@example.com', 'active', 5),
|
||||||
|
('RD-0002', NULL, 'Suspended Reader', '13900000000', 'suspended.reader@example.com', 'suspended', 3)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
user_id = VALUES(user_id),
|
||||||
|
full_name = VALUES(full_name),
|
||||||
|
phone = VALUES(phone),
|
||||||
|
email = VALUES(email),
|
||||||
|
status = VALUES(status),
|
||||||
|
max_borrow_count = VALUES(max_borrow_count);
|
||||||
|
|
||||||
INSERT INTO book_categories (name, description) VALUES
|
INSERT INTO book_categories (name, description) VALUES
|
||||||
('Computer Science', 'Programming, software engineering, and systems books'),
|
('Computer Science', 'Programming, software engineering, and systems books'),
|
||||||
('Literature', 'Classic and modern literature'),
|
('Literature', 'Classic and modern literature'),
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
<c:if test="${sessionScope.userRole == 'administrator' or sessionScope.userRole == 'librarian'}">
|
<c:if test="${sessionScope.userRole == 'administrator' or sessionScope.userRole == 'librarian'}">
|
||||||
<a href="${pageContext.request.contextPath}/librarian/home">Librarian</a>
|
<a href="${pageContext.request.contextPath}/librarian/home">Librarian</a>
|
||||||
<a href="${pageContext.request.contextPath}/books">Books</a>
|
<a href="${pageContext.request.contextPath}/books">Books</a>
|
||||||
|
<a href="${pageContext.request.contextPath}/readers">Readers</a>
|
||||||
</c:if>
|
</c:if>
|
||||||
<a href="${pageContext.request.contextPath}/reader/home">Reader</a>
|
<a href="${pageContext.request.contextPath}/reader/home">Reader</a>
|
||||||
<span class="user-pill">
|
<span class="user-pill">
|
||||||
|
|||||||
@@ -40,6 +40,12 @@
|
|||||||
<p>Create, update, delete, and review book inventory records.</p>
|
<p>Create, update, delete, and review book inventory records.</p>
|
||||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/books">Open</a>
|
<a class="button button-secondary" href="${pageContext.request.contextPath}/books">Open</a>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
|
<article class="workspace-card">
|
||||||
|
<h2>Reader Management</h2>
|
||||||
|
<p>Create, update, deactivate, and review reader eligibility records.</p>
|
||||||
|
<a class="button button-secondary" href="${pageContext.request.contextPath}/readers">Open</a>
|
||||||
|
</article>
|
||||||
</c:if>
|
</c:if>
|
||||||
|
|
||||||
<article class="workspace-card">
|
<article class="workspace-card">
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>
|
||||||
|
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||||
|
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title><c:out value="${formTitle}" /> - MZH Library</title>
|
||||||
|
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
||||||
|
<main class="page-shell">
|
||||||
|
<section class="form-panel" aria-labelledby="reader-form-title">
|
||||||
|
<p class="eyebrow">Reader Management</p>
|
||||||
|
<h1 id="reader-form-title"><c:out value="${formTitle}" /></h1>
|
||||||
|
|
||||||
|
<c:if test="${not empty errorMessage}">
|
||||||
|
<div class="message message-error" role="alert">
|
||||||
|
<c:out value="${errorMessage}" />
|
||||||
|
</div>
|
||||||
|
</c:if>
|
||||||
|
|
||||||
|
<c:set var="hasFormValues" value="${not empty formValues}" />
|
||||||
|
<c:set var="identifierValue" value="${hasFormValues ? formValues.identifier : reader.identifier}" />
|
||||||
|
<c:set var="userIdValue" value="${hasFormValues ? formValues.userId : reader.userId}" />
|
||||||
|
<c:set var="fullNameValue" value="${hasFormValues ? formValues.fullName : reader.fullName}" />
|
||||||
|
<c:set var="phoneValue" value="${hasFormValues ? formValues.phone : reader.phone}" />
|
||||||
|
<c:set var="emailValue" value="${hasFormValues ? formValues.email : reader.email}" />
|
||||||
|
<c:set var="statusValue" value="${hasFormValues ? formValues.status : reader.status.code}" />
|
||||||
|
<c:set var="maxBorrowCountValue" value="${hasFormValues ? formValues.maxBorrowCount : reader.maxBorrowCount}" />
|
||||||
|
|
||||||
|
<form class="reader-form" action="${pageContext.request.contextPath}${formAction}" method="post" novalidate>
|
||||||
|
<c:if test="${reader.id > 0}">
|
||||||
|
<input type="hidden" name="id" value="${reader.id}">
|
||||||
|
</c:if>
|
||||||
|
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="form-field">
|
||||||
|
<label for="identifier">Reader ID</label>
|
||||||
|
<input id="identifier" name="identifier" type="text" value="${fn:escapeXml(identifierValue)}" required>
|
||||||
|
<c:if test="${not empty errors.identifier}">
|
||||||
|
<span class="field-error"><c:out value="${errors.identifier}" /></span>
|
||||||
|
</c:if>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-field">
|
||||||
|
<label for="fullName">Full name</label>
|
||||||
|
<input id="fullName" name="fullName" type="text" value="${fn:escapeXml(fullNameValue)}" required>
|
||||||
|
<c:if test="${not empty errors.fullName}">
|
||||||
|
<span class="field-error"><c:out value="${errors.fullName}" /></span>
|
||||||
|
</c:if>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-field">
|
||||||
|
<label for="phone">Phone</label>
|
||||||
|
<input id="phone" name="phone" type="tel" value="${fn:escapeXml(phoneValue)}">
|
||||||
|
<c:if test="${not empty errors.phone}">
|
||||||
|
<span class="field-error"><c:out value="${errors.phone}" /></span>
|
||||||
|
</c:if>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-field">
|
||||||
|
<label for="email">Email</label>
|
||||||
|
<input id="email" name="email" type="email" value="${fn:escapeXml(emailValue)}">
|
||||||
|
<c:if test="${not empty errors.email}">
|
||||||
|
<span class="field-error"><c:out value="${errors.email}" /></span>
|
||||||
|
</c:if>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-field">
|
||||||
|
<label for="userId">Linked account ID</label>
|
||||||
|
<input id="userId" name="userId" type="number" min="1" value="${fn:escapeXml(userIdValue)}">
|
||||||
|
<c:if test="${not empty errors.userId}">
|
||||||
|
<span class="field-error"><c:out value="${errors.userId}" /></span>
|
||||||
|
</c:if>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-field">
|
||||||
|
<label for="maxBorrowCount">Max borrow count</label>
|
||||||
|
<input id="maxBorrowCount" name="maxBorrowCount" type="number" min="1" max="50"
|
||||||
|
value="${fn:escapeXml(maxBorrowCountValue)}" required>
|
||||||
|
<c:if test="${not empty errors.maxBorrowCount}">
|
||||||
|
<span class="field-error"><c:out value="${errors.maxBorrowCount}" /></span>
|
||||||
|
</c:if>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-field">
|
||||||
|
<label for="status">Status</label>
|
||||||
|
<select id="status" name="status" required>
|
||||||
|
<option value="">Select status</option>
|
||||||
|
<c:forEach var="status" items="${statuses}">
|
||||||
|
<option value="${status.code}" <c:if test="${statusValue == status.code}">selected</c:if>>
|
||||||
|
<c:out value="${status.displayName}" />
|
||||||
|
</option>
|
||||||
|
</c:forEach>
|
||||||
|
</select>
|
||||||
|
<c:if test="${not empty errors.status}">
|
||||||
|
<span class="field-error"><c:out value="${errors.status}" /></span>
|
||||||
|
</c:if>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button class="button button-primary" type="submit">Save</button>
|
||||||
|
<a class="button button-secondary" href="${pageContext.request.contextPath}/readers">Cancel</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>
|
||||||
|
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||||
|
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Manage Readers - MZH Library</title>
|
||||||
|
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
||||||
|
<main class="page-shell">
|
||||||
|
<section class="dashboard-hero catalog-hero" aria-labelledby="manage-readers-title">
|
||||||
|
<p class="eyebrow">Reader Management</p>
|
||||||
|
<h1 id="manage-readers-title">Manage readers</h1>
|
||||||
|
<p>Create, update, and review reader eligibility and contact records.</p>
|
||||||
|
<a class="button button-primary" href="${pageContext.request.contextPath}/readers/new">New reader</a>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<c:if test="${not empty successMessage}">
|
||||||
|
<div class="message message-success" role="status">
|
||||||
|
<c:out value="${successMessage}" />
|
||||||
|
</div>
|
||||||
|
</c:if>
|
||||||
|
<c:if test="${not empty errorMessage}">
|
||||||
|
<div class="message message-error" role="alert">
|
||||||
|
<c:out value="${errorMessage}" />
|
||||||
|
</div>
|
||||||
|
</c:if>
|
||||||
|
|
||||||
|
<section class="toolbar-panel" aria-label="Reader management search">
|
||||||
|
<form class="search-form" action="${pageContext.request.contextPath}/readers" method="get">
|
||||||
|
<div class="search-field">
|
||||||
|
<label for="identifier">Reader ID</label>
|
||||||
|
<input id="identifier" name="identifier" type="text" value="${fn:escapeXml(criteria.identifier)}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="search-field">
|
||||||
|
<label for="name">Name</label>
|
||||||
|
<input id="name" name="name" type="text" value="${fn:escapeXml(criteria.name)}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="search-field">
|
||||||
|
<label for="contact">Phone or email</label>
|
||||||
|
<input id="contact" name="contact" type="text" value="${fn:escapeXml(criteria.contact)}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="search-field">
|
||||||
|
<label for="status">Status</label>
|
||||||
|
<select id="status" name="status">
|
||||||
|
<option value="">All statuses</option>
|
||||||
|
<c:forEach var="status" items="${statuses}">
|
||||||
|
<option value="${status.code}" <c:if test="${criteria.statusCode == status.code}">selected</c:if>>
|
||||||
|
<c:out value="${status.displayName}" />
|
||||||
|
</option>
|
||||||
|
</c:forEach>
|
||||||
|
</select>
|
||||||
|
<c:if test="${not empty errors.status}">
|
||||||
|
<span class="field-error"><c:out value="${errors.status}" /></span>
|
||||||
|
</c:if>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="button button-primary" type="submit">Search</button>
|
||||||
|
<a class="button button-secondary" href="${pageContext.request.contextPath}/readers">Clear</a>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="table-panel" aria-labelledby="reader-results-title">
|
||||||
|
<h2 id="reader-results-title">Reader records</h2>
|
||||||
|
<c:choose>
|
||||||
|
<c:when test="${empty readers}">
|
||||||
|
<p class="empty-state">No reader records match the current filters.</p>
|
||||||
|
</c:when>
|
||||||
|
<c:otherwise>
|
||||||
|
<div class="table-scroll">
|
||||||
|
<table class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">Reader ID</th>
|
||||||
|
<th scope="col">Name</th>
|
||||||
|
<th scope="col">Contact</th>
|
||||||
|
<th scope="col">Account</th>
|
||||||
|
<th scope="col">Borrow limit</th>
|
||||||
|
<th scope="col">Status</th>
|
||||||
|
<th scope="col">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<c:forEach var="reader" items="${readers}">
|
||||||
|
<tr>
|
||||||
|
<td><c:out value="${reader.identifier}" /></td>
|
||||||
|
<td><c:out value="${reader.fullName}" /></td>
|
||||||
|
<td>
|
||||||
|
<c:if test="${not empty reader.phone}">
|
||||||
|
<div><c:out value="${reader.phone}" /></div>
|
||||||
|
</c:if>
|
||||||
|
<c:if test="${not empty reader.email}">
|
||||||
|
<div><c:out value="${reader.email}" /></div>
|
||||||
|
</c:if>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<c:choose>
|
||||||
|
<c:when test="${not empty reader.username}">
|
||||||
|
<c:out value="${reader.username}" />
|
||||||
|
</c:when>
|
||||||
|
<c:otherwise>Unlinked</c:otherwise>
|
||||||
|
</c:choose>
|
||||||
|
</td>
|
||||||
|
<td><c:out value="${reader.maxBorrowCount}" /></td>
|
||||||
|
<td>
|
||||||
|
<span class="status-pill status-${reader.status.code}">
|
||||||
|
<c:out value="${reader.status.displayName}" />
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="table-actions">
|
||||||
|
<a class="button button-secondary"
|
||||||
|
href="${pageContext.request.contextPath}/readers/edit?id=${reader.id}">Edit</a>
|
||||||
|
<form action="${pageContext.request.contextPath}/readers/delete"
|
||||||
|
method="post"
|
||||||
|
onsubmit="return confirm('Deactivate this reader profile?');">
|
||||||
|
<input type="hidden" name="id" value="${reader.id}">
|
||||||
|
<button class="button button-danger" type="submit">Deactivate</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</c:forEach>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</c:otherwise>
|
||||||
|
</c:choose>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -33,6 +33,12 @@
|
|||||||
<p>Create, update, delete, and review inventory fields for book records.</p>
|
<p>Create, update, delete, and review inventory fields for book records.</p>
|
||||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/books">Manage books</a>
|
<a class="button button-secondary" href="${pageContext.request.contextPath}/books">Manage books</a>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
|
<article class="workspace-card">
|
||||||
|
<h2>Reader Management</h2>
|
||||||
|
<p>Create, update, deactivate, and review eligibility fields for reader records.</p>
|
||||||
|
<a class="button button-secondary" href="${pageContext.request.contextPath}/readers">Manage readers</a>
|
||||||
|
</article>
|
||||||
</c:if>
|
</c:if>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -97,6 +97,19 @@
|
|||||||
<url-pattern>/books/delete</url-pattern>
|
<url-pattern>/books/delete</url-pattern>
|
||||||
</servlet-mapping>
|
</servlet-mapping>
|
||||||
|
|
||||||
|
<servlet>
|
||||||
|
<servlet-name>ReaderManagementServlet</servlet-name>
|
||||||
|
<servlet-class>com.mzh.library.controller.ReaderManagementServlet</servlet-class>
|
||||||
|
</servlet>
|
||||||
|
<servlet-mapping>
|
||||||
|
<servlet-name>ReaderManagementServlet</servlet-name>
|
||||||
|
<url-pattern>/readers</url-pattern>
|
||||||
|
<url-pattern>/readers/new</url-pattern>
|
||||||
|
<url-pattern>/readers/edit</url-pattern>
|
||||||
|
<url-pattern>/readers/update</url-pattern>
|
||||||
|
<url-pattern>/readers/delete</url-pattern>
|
||||||
|
</servlet-mapping>
|
||||||
|
|
||||||
<servlet>
|
<servlet>
|
||||||
<servlet-name>UnauthorizedServlet</servlet-name>
|
<servlet-name>UnauthorizedServlet</servlet-name>
|
||||||
<servlet-class>com.mzh.library.controller.UnauthorizedServlet</servlet-class>
|
<servlet-class>com.mzh.library.controller.UnauthorizedServlet</servlet-class>
|
||||||
|
|||||||
@@ -303,7 +303,9 @@ h2 {
|
|||||||
.search-form input,
|
.search-form input,
|
||||||
.search-form select,
|
.search-form select,
|
||||||
.book-form input,
|
.book-form input,
|
||||||
.book-form select {
|
.book-form select,
|
||||||
|
.reader-form input,
|
||||||
|
.reader-form select {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 42px;
|
min-height: 42px;
|
||||||
padding: 9px 11px;
|
padding: 9px 11px;
|
||||||
@@ -316,7 +318,9 @@ h2 {
|
|||||||
.search-form input:focus,
|
.search-form input:focus,
|
||||||
.search-form select:focus,
|
.search-form select:focus,
|
||||||
.book-form input:focus,
|
.book-form input:focus,
|
||||||
.book-form select:focus {
|
.book-form select:focus,
|
||||||
|
.reader-form input:focus,
|
||||||
|
.reader-form select:focus {
|
||||||
outline: 3px solid rgba(37, 111, 108, 0.18);
|
outline: 3px solid rgba(37, 111, 108, 0.18);
|
||||||
border-color: var(--color-primary);
|
border-color: var(--color-primary);
|
||||||
}
|
}
|
||||||
@@ -378,6 +382,21 @@ h2 {
|
|||||||
background: #eef1f5;
|
background: #eef1f5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status-active {
|
||||||
|
color: var(--color-success);
|
||||||
|
background: #edf8ef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-suspended {
|
||||||
|
color: var(--color-warning);
|
||||||
|
background: #fff7e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-inactive {
|
||||||
|
color: var(--color-muted);
|
||||||
|
background: #eef1f5;
|
||||||
|
}
|
||||||
|
|
||||||
.table-actions {
|
.table-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
@@ -393,7 +412,8 @@ h2 {
|
|||||||
max-width: 860px;
|
max-width: 860px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.book-form {
|
.book-form,
|
||||||
|
.reader-form {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,9 +12,11 @@ public final class PermissionPolicyCheck {
|
|||||||
|
|
||||||
require(policy.allows(Role.ADMINISTRATOR, Permission.MANAGE_USERS), "administrator should manage users");
|
require(policy.allows(Role.ADMINISTRATOR, Permission.MANAGE_USERS), "administrator should manage users");
|
||||||
require(policy.allows(Role.LIBRARIAN, Permission.MANAGE_BORROWING), "librarian should manage borrowing");
|
require(policy.allows(Role.LIBRARIAN, Permission.MANAGE_BORROWING), "librarian should manage borrowing");
|
||||||
|
require(policy.allows(Role.LIBRARIAN, Permission.MANAGE_READERS), "librarian should manage readers");
|
||||||
require(!policy.allows(Role.LIBRARIAN, Permission.MANAGE_USERS), "librarian should not manage users");
|
require(!policy.allows(Role.LIBRARIAN, Permission.MANAGE_USERS), "librarian should not manage users");
|
||||||
require(policy.allows(Role.READER, Permission.VIEW_CATALOG), "reader should view catalog");
|
require(policy.allows(Role.READER, Permission.VIEW_CATALOG), "reader should view catalog");
|
||||||
require(!policy.allows(Role.READER, Permission.MANAGE_BOOKS), "reader should not manage books");
|
require(!policy.allows(Role.READER, Permission.MANAGE_BOOKS), "reader should not manage books");
|
||||||
|
require(!policy.allows(Role.READER, Permission.MANAGE_READERS), "reader should not manage readers");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void require(boolean condition, String message) {
|
private static void require(boolean condition, String message) {
|
||||||
|
|||||||
@@ -0,0 +1,287 @@
|
|||||||
|
package com.mzh.library.service;
|
||||||
|
|
||||||
|
import com.mzh.library.dao.ReaderDao;
|
||||||
|
import com.mzh.library.entity.AuthenticatedUser;
|
||||||
|
import com.mzh.library.entity.Permission;
|
||||||
|
import com.mzh.library.entity.Reader;
|
||||||
|
import com.mzh.library.entity.ReaderSearchCriteria;
|
||||||
|
import com.mzh.library.entity.ReaderStatus;
|
||||||
|
import com.mzh.library.entity.Role;
|
||||||
|
import com.mzh.library.exception.DaoException;
|
||||||
|
import com.mzh.library.service.impl.ReaderServiceImpl;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.EnumSet;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
public final class ReaderServiceCheck {
|
||||||
|
private static final String UNAVAILABLE_MESSAGE =
|
||||||
|
"Reader service is temporarily unavailable. Please try again later.";
|
||||||
|
|
||||||
|
private ReaderServiceCheck() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
Logger.getLogger(ReaderServiceImpl.class.getName()).setLevel(Level.OFF);
|
||||||
|
|
||||||
|
InMemoryReaderDao dao = new InMemoryReaderDao();
|
||||||
|
ReaderService service = new ReaderServiceImpl(dao);
|
||||||
|
AuthenticatedUser librarian = user(10L, Role.LIBRARIAN);
|
||||||
|
AuthenticatedUser readerUser = user(20L, Role.READER);
|
||||||
|
|
||||||
|
ServiceResult<Long> invalidContact = service.createReader(librarian,
|
||||||
|
reader(0L, "RD-1000", null, "Invalid Contact", "bad", "", ReaderStatus.ACTIVE, 5));
|
||||||
|
require(!invalidContact.isSuccessful(), "invalid contact should fail");
|
||||||
|
require(invalidContact.getErrors().containsKey("phone"), "invalid phone should target phone field");
|
||||||
|
|
||||||
|
ServiceResult<Long> symbolsOnlyPhone = service.createReader(librarian,
|
||||||
|
reader(0L, "RD-1009", null, "Symbols Only Phone", "------", "", ReaderStatus.ACTIVE, 5));
|
||||||
|
require(!symbolsOnlyPhone.isSuccessful(), "phone without digits should fail");
|
||||||
|
require(symbolsOnlyPhone.getErrors().containsKey("phone"), "phone without digits should target phone field");
|
||||||
|
|
||||||
|
ServiceResult<Long> missingContact = service.createReader(librarian,
|
||||||
|
reader(0L, "RD-1008", null, "Missing Contact", "", "", ReaderStatus.ACTIVE, 5));
|
||||||
|
require(!missingContact.isSuccessful(), "missing contact should fail");
|
||||||
|
require(missingContact.getErrors().containsKey("phone"), "missing contact should target phone field");
|
||||||
|
|
||||||
|
ServiceResult<Long> invalidBorrowLimit = service.createReader(librarian,
|
||||||
|
reader(0L, "RD-1001", null, "Invalid Limit", "13800000001", "", ReaderStatus.ACTIVE, 0));
|
||||||
|
require(!invalidBorrowLimit.isSuccessful(), "invalid borrow limit should fail");
|
||||||
|
require(invalidBorrowLimit.getErrors().containsKey("maxBorrowCount"),
|
||||||
|
"invalid borrow limit should target maxBorrowCount field");
|
||||||
|
|
||||||
|
ServiceResult<Long> invalidUserId = service.createReader(librarian,
|
||||||
|
reader(0L, "RD-1002", -1L, "Invalid User", "13800000002", "", ReaderStatus.ACTIVE, 5));
|
||||||
|
require(!invalidUserId.isSuccessful(), "invalid linked account should fail");
|
||||||
|
require(invalidUserId.getErrors().containsKey("userId"),
|
||||||
|
"invalid linked account should target userId field");
|
||||||
|
|
||||||
|
ServiceResult<Long> denied = service.createReader(readerUser,
|
||||||
|
reader(0L, "RD-1006", null, "Reader Write", "13800000007", "", ReaderStatus.ACTIVE, 5));
|
||||||
|
require(!denied.isSuccessful(), "reader write should fail");
|
||||||
|
require("You do not have permission to manage readers.".equals(denied.getMessage()),
|
||||||
|
"reader write should return permission message");
|
||||||
|
|
||||||
|
ServiceResult<Long> created = service.createReader(librarian,
|
||||||
|
reader(0L, "RD-1003", 30L, "Service Reader", "13800000003",
|
||||||
|
"service.reader@example.com", ReaderStatus.ACTIVE, 5));
|
||||||
|
require(created.isSuccessful(), "librarian should create a valid reader");
|
||||||
|
long createdId = created.getData();
|
||||||
|
|
||||||
|
ServiceResult<Long> duplicateIdentifier = service.createReader(librarian,
|
||||||
|
reader(0L, "RD-1003", null, "Duplicate Identifier", "13800000004", "", ReaderStatus.ACTIVE, 5));
|
||||||
|
require(!duplicateIdentifier.isSuccessful(), "duplicate reader identifier should fail");
|
||||||
|
require(duplicateIdentifier.getErrors().containsKey("identifier"),
|
||||||
|
"duplicate identifier should target identifier field");
|
||||||
|
|
||||||
|
ServiceResult<Long> duplicateUser = service.createReader(librarian,
|
||||||
|
reader(0L, "RD-1004", 30L, "Duplicate User", "13800000005", "", ReaderStatus.ACTIVE, 5));
|
||||||
|
require(!duplicateUser.isSuccessful(), "duplicate linked account should fail");
|
||||||
|
require(duplicateUser.getErrors().containsKey("userId"), "duplicate linked account should target userId field");
|
||||||
|
|
||||||
|
ServiceResult<Void> updated = service.updateReader(librarian,
|
||||||
|
reader(createdId, "RD-1005", 30L, "Updated Reader", "13800000006",
|
||||||
|
"updated.reader@example.com", ReaderStatus.SUSPENDED, 3));
|
||||||
|
require(updated.isSuccessful(), "librarian should update a valid reader");
|
||||||
|
require(dao.findById(createdId).get().getStatus() == ReaderStatus.SUSPENDED,
|
||||||
|
"update should persist reader status");
|
||||||
|
|
||||||
|
ServiceResult<List<Reader>> search = service.searchReaders(
|
||||||
|
new ReaderSearchCriteria("RD-1005", "Updated", "updated.reader", "suspended"));
|
||||||
|
require(search.isSuccessful(), "search should succeed");
|
||||||
|
require(search.getData().size() == 1, "search should find updated reader by filters");
|
||||||
|
|
||||||
|
ServiceResult<List<Reader>> invalidSearch = service.searchReaders(
|
||||||
|
new ReaderSearchCriteria("", "", "", "missing"));
|
||||||
|
require(!invalidSearch.isSuccessful(), "invalid search status should fail");
|
||||||
|
require(invalidSearch.getErrors().containsKey("status"), "invalid search status should target status field");
|
||||||
|
|
||||||
|
ServiceResult<Void> deactivated = service.deactivateReader(librarian, createdId);
|
||||||
|
require(deactivated.isSuccessful(), "librarian should deactivate a reader");
|
||||||
|
require(dao.findById(createdId).get().getStatus() == ReaderStatus.INACTIVE,
|
||||||
|
"deactivate should mark the reader inactive");
|
||||||
|
|
||||||
|
ReaderService failingService = new ReaderServiceImpl(new FailingReaderDao());
|
||||||
|
ServiceResult<List<Reader>> unavailable = failingService.searchReaders(new ReaderSearchCriteria());
|
||||||
|
require(!unavailable.isSuccessful(), "DAO failure should not escape service");
|
||||||
|
require(UNAVAILABLE_MESSAGE.equals(unavailable.getMessage()), "search DAO failure should map to safe message");
|
||||||
|
|
||||||
|
ServiceResult<Long> unavailableCreate = failingService.createReader(librarian,
|
||||||
|
reader(0L, "RD-1007", null, "Unavailable Create", "13800000008", "", ReaderStatus.ACTIVE, 5));
|
||||||
|
require(!unavailableCreate.isSuccessful(), "create DAO failure should not escape service");
|
||||||
|
require(UNAVAILABLE_MESSAGE.equals(unavailableCreate.getMessage()),
|
||||||
|
"create DAO failure should map to safe message");
|
||||||
|
|
||||||
|
ServiceResult<Void> unavailableUpdate = failingService.updateReader(librarian,
|
||||||
|
reader(1L, "RD-1007", null, "Unavailable Update", "13800000009", "", ReaderStatus.ACTIVE, 5));
|
||||||
|
require(!unavailableUpdate.isSuccessful(), "update DAO failure should not escape service");
|
||||||
|
require(UNAVAILABLE_MESSAGE.equals(unavailableUpdate.getMessage()),
|
||||||
|
"update DAO failure should map to safe message");
|
||||||
|
|
||||||
|
ServiceResult<Void> unavailableDeactivate = failingService.deactivateReader(librarian, 1L);
|
||||||
|
require(!unavailableDeactivate.isSuccessful(), "deactivate DAO failure should not escape service");
|
||||||
|
require(UNAVAILABLE_MESSAGE.equals(unavailableDeactivate.getMessage()),
|
||||||
|
"deactivate DAO failure should map to safe message");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AuthenticatedUser user(long id, Role role) {
|
||||||
|
return new AuthenticatedUser(id, role.getCode(), role.getDisplayName(), role,
|
||||||
|
role == Role.READER
|
||||||
|
? EnumSet.of(Permission.VIEW_CATALOG, Permission.BORROW_BOOKS)
|
||||||
|
: EnumSet.of(Permission.MANAGE_BOOKS, Permission.MANAGE_READERS, Permission.VIEW_CATALOG));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Reader reader(long id, String identifier, Long userId, String fullName, String phone,
|
||||||
|
String email, ReaderStatus status, int maxBorrowCount) {
|
||||||
|
Reader reader = new Reader();
|
||||||
|
reader.setId(id);
|
||||||
|
reader.setIdentifier(identifier);
|
||||||
|
reader.setUserId(userId);
|
||||||
|
reader.setFullName(fullName);
|
||||||
|
reader.setPhone(phone);
|
||||||
|
reader.setEmail(email);
|
||||||
|
reader.setStatus(status);
|
||||||
|
reader.setMaxBorrowCount(maxBorrowCount);
|
||||||
|
return reader;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void require(boolean condition, String message) {
|
||||||
|
if (!condition) {
|
||||||
|
throw new AssertionError(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class InMemoryReaderDao implements ReaderDao {
|
||||||
|
private final Map<Long, Reader> readers = new LinkedHashMap<>();
|
||||||
|
private long nextId = 1L;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Reader> search(ReaderSearchCriteria criteria) {
|
||||||
|
List<Reader> matches = new ArrayList<>();
|
||||||
|
for (Reader reader : readers.values()) {
|
||||||
|
if (matches(criteria.getIdentifier(), reader.getIdentifier())
|
||||||
|
&& matches(criteria.getName(), reader.getFullName())
|
||||||
|
&& contactMatches(criteria.getContact(), reader)
|
||||||
|
&& (criteria.getStatusCode() == null || criteria.getStatusCode().isEmpty()
|
||||||
|
|| reader.getStatus().getCode().equals(criteria.getStatusCode()))) {
|
||||||
|
matches.add(copy(reader));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<Reader> findById(long id) {
|
||||||
|
return Optional.ofNullable(readers.get(id)).map(this::copy);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<Reader> findByIdentifier(String identifier) {
|
||||||
|
for (Reader reader : readers.values()) {
|
||||||
|
if (reader.getIdentifier().equals(identifier)) {
|
||||||
|
return Optional.of(copy(reader));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<Reader> findByUserId(long userId) {
|
||||||
|
for (Reader reader : readers.values()) {
|
||||||
|
if (reader.getUserId() != null && reader.getUserId() == userId) {
|
||||||
|
return Optional.of(copy(reader));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long create(Reader reader) {
|
||||||
|
long id = nextId++;
|
||||||
|
Reader stored = copy(reader);
|
||||||
|
stored.setId(id);
|
||||||
|
readers.put(id, stored);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean update(Reader reader) {
|
||||||
|
if (!readers.containsKey(reader.getId())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
readers.put(reader.getId(), copy(reader));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean deactivate(long id) {
|
||||||
|
Reader reader = readers.get(id);
|
||||||
|
if (reader == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
reader.setStatus(ReaderStatus.INACTIVE);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean matches(String filter, String value) {
|
||||||
|
return filter == null || filter.isEmpty() || value.contains(filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean contactMatches(String filter, Reader reader) {
|
||||||
|
if (filter == null || filter.isEmpty()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return (reader.getPhone() != null && reader.getPhone().contains(filter))
|
||||||
|
|| (reader.getEmail() != null && reader.getEmail().contains(filter));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Reader copy(Reader source) {
|
||||||
|
Reader copy = reader(source.getId(), source.getIdentifier(), source.getUserId(), source.getFullName(),
|
||||||
|
source.getPhone(), source.getEmail(), source.getStatus(), source.getMaxBorrowCount());
|
||||||
|
copy.setUsername(source.getUsername());
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class FailingReaderDao implements ReaderDao {
|
||||||
|
@Override
|
||||||
|
public List<Reader> search(ReaderSearchCriteria criteria) {
|
||||||
|
throw new DaoException("Simulated search failure", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<Reader> findById(long id) {
|
||||||
|
throw new DaoException("Simulated find failure", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<Reader> findByIdentifier(String identifier) {
|
||||||
|
throw new DaoException("Simulated find failure", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<Reader> findByUserId(long userId) {
|
||||||
|
throw new DaoException("Simulated find failure", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long create(Reader reader) {
|
||||||
|
throw new DaoException("Simulated create failure", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean update(Reader reader) {
|
||||||
|
throw new DaoException("Simulated update failure", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean deactivate(long id) {
|
||||||
|
throw new DaoException("Simulated deactivate failure", null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user