forked from admin/struts2-demo
Compare commits
3 Commits
fb9d2d1206
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5466fb1ffd | ||
|
|
fba7b0497f | ||
|
|
1f60832445 |
48
web/WEB-INF/classes/com/demo/action/AjaxAction.java
Normal file
48
web/WEB-INF/classes/com/demo/action/AjaxAction.java
Normal file
@@ -0,0 +1,48 @@
|
||||
package com.demo.action;
|
||||
|
||||
import com.demo.model.User;
|
||||
import com.opensymphony.xwork2.ActionSupport;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class AjaxAction extends ActionSupport {
|
||||
|
||||
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
private boolean success;
|
||||
private String message;
|
||||
private String serverTime;
|
||||
private List<User> users;
|
||||
|
||||
@Override
|
||||
public String execute() {
|
||||
success = true;
|
||||
message = "Mock AJAX response generated from the Struts2 action.";
|
||||
serverTime = LocalDateTime.now().format(TIME_FORMATTER);
|
||||
users = Arrays.asList(
|
||||
new User(1L, "ops-admin", "ops-admin@example.com", "13800000001"),
|
||||
new User(2L, "platform-owner", "platform-owner@example.com", "13800000002"),
|
||||
new User(3L, "release-manager", "release-manager@example.com", "13800000003")
|
||||
);
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
public boolean isSuccess() {
|
||||
return success;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public String getServerTime() {
|
||||
return serverTime;
|
||||
}
|
||||
|
||||
public List<User> getUsers() {
|
||||
return users;
|
||||
}
|
||||
}
|
||||
93
web/WEB-INF/classes/com/demo/action/FileUploadAction.java
Normal file
93
web/WEB-INF/classes/com/demo/action/FileUploadAction.java
Normal file
@@ -0,0 +1,93 @@
|
||||
package com.demo.action;
|
||||
|
||||
import com.opensymphony.xwork2.ActionSupport;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
|
||||
public class FileUploadAction extends ActionSupport {
|
||||
|
||||
private File upload;
|
||||
private String uploadFileName;
|
||||
private String uploadContentType;
|
||||
private List<File> uploads;
|
||||
private List<String> uploadsFileName;
|
||||
private List<String> uploadsContentType;
|
||||
private int fileCount;
|
||||
private String summary;
|
||||
|
||||
@Override
|
||||
public String execute() {
|
||||
fileCount = 0;
|
||||
if (upload != null) {
|
||||
fileCount++;
|
||||
}
|
||||
if (uploads != null) {
|
||||
fileCount += uploads.size();
|
||||
}
|
||||
|
||||
if (fileCount == 0) {
|
||||
addActionError("Select at least one file before submitting the demo.");
|
||||
return INPUT;
|
||||
}
|
||||
|
||||
summary = "This demo captures upload metadata only. It does not persist files to disk, which keeps the sample safe for classroom use.";
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
public File getUpload() {
|
||||
return upload;
|
||||
}
|
||||
|
||||
public void setUpload(File upload) {
|
||||
this.upload = upload;
|
||||
}
|
||||
|
||||
public String getUploadFileName() {
|
||||
return uploadFileName;
|
||||
}
|
||||
|
||||
public void setUploadFileName(String uploadFileName) {
|
||||
this.uploadFileName = uploadFileName;
|
||||
}
|
||||
|
||||
public String getUploadContentType() {
|
||||
return uploadContentType;
|
||||
}
|
||||
|
||||
public void setUploadContentType(String uploadContentType) {
|
||||
this.uploadContentType = uploadContentType;
|
||||
}
|
||||
|
||||
public List<File> getUploads() {
|
||||
return uploads;
|
||||
}
|
||||
|
||||
public void setUploads(List<File> uploads) {
|
||||
this.uploads = uploads;
|
||||
}
|
||||
|
||||
public List<String> getUploadsFileName() {
|
||||
return uploadsFileName;
|
||||
}
|
||||
|
||||
public void setUploadsFileName(List<String> uploadsFileName) {
|
||||
this.uploadsFileName = uploadsFileName;
|
||||
}
|
||||
|
||||
public List<String> getUploadsContentType() {
|
||||
return uploadsContentType;
|
||||
}
|
||||
|
||||
public void setUploadsContentType(List<String> uploadsContentType) {
|
||||
this.uploadsContentType = uploadsContentType;
|
||||
}
|
||||
|
||||
public int getFileCount() {
|
||||
return fileCount;
|
||||
}
|
||||
|
||||
public String getSummary() {
|
||||
return summary;
|
||||
}
|
||||
}
|
||||
51
web/WEB-INF/classes/com/demo/action/HelloAction.java
Normal file
51
web/WEB-INF/classes/com/demo/action/HelloAction.java
Normal file
@@ -0,0 +1,51 @@
|
||||
package com.demo.action;
|
||||
|
||||
import com.opensymphony.xwork2.ActionSupport;
|
||||
|
||||
public class HelloAction extends ActionSupport {
|
||||
|
||||
private String name;
|
||||
private String message;
|
||||
private String tip;
|
||||
private String nextStep;
|
||||
private String requestSample;
|
||||
|
||||
@Override
|
||||
public String execute() {
|
||||
if (name == null || name.trim().isEmpty()) {
|
||||
name = "World";
|
||||
} else {
|
||||
name = name.trim();
|
||||
}
|
||||
|
||||
message = "Hello, " + name + "! Welcome to the Struts2 demo lab.";
|
||||
tip = "Struts2 injected the request parameter into the action property before execute() ran.";
|
||||
nextStep = "Try the login flow or open the demo catalog to compare different action patterns.";
|
||||
requestSample = "/hello?name=Platform%20Team";
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public String getTip() {
|
||||
return tip;
|
||||
}
|
||||
|
||||
public String getNextStep() {
|
||||
return nextStep;
|
||||
}
|
||||
|
||||
public String getRequestSample() {
|
||||
return requestSample;
|
||||
}
|
||||
}
|
||||
93
web/WEB-INF/classes/com/demo/action/LoginAction.java
Normal file
93
web/WEB-INF/classes/com/demo/action/LoginAction.java
Normal file
@@ -0,0 +1,93 @@
|
||||
package com.demo.action;
|
||||
|
||||
import com.opensymphony.xwork2.ActionSupport;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
public class LoginAction extends ActionSupport {
|
||||
|
||||
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
private String username;
|
||||
private String password;
|
||||
private String displayName;
|
||||
private String role;
|
||||
private String loginTime;
|
||||
private String recommendation;
|
||||
|
||||
@Override
|
||||
public String execute() {
|
||||
username = normalize(username);
|
||||
password = normalize(password);
|
||||
|
||||
if (!hasSubmitted()) {
|
||||
return INPUT;
|
||||
}
|
||||
|
||||
if ("admin".equals(username) && "123456".equals(password)) {
|
||||
displayName = "System Demo Admin";
|
||||
role = "Administrator";
|
||||
loginTime = LocalDateTime.now().format(TIME_FORMATTER);
|
||||
recommendation = "Continue with the user form, validation sample, or upload flow to explore the rest of the demo.";
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
addActionError("Invalid demo credentials. Use admin / 123456.");
|
||||
return INPUT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate() {
|
||||
if (!hasSubmitted()) {
|
||||
return;
|
||||
}
|
||||
if (username == null || username.length() < 3) {
|
||||
addFieldError("username", "Username must be at least 3 characters.");
|
||||
}
|
||||
if (password == null || password.length() < 6) {
|
||||
addFieldError("password", "Password must be at least 6 characters.");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasSubmitted() {
|
||||
return (username != null && !username.trim().isEmpty())
|
||||
|| (password != null && !password.trim().isEmpty());
|
||||
}
|
||||
|
||||
private String normalize(String value) {
|
||||
return value == null ? null : value.trim();
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
public String getRole() {
|
||||
return role;
|
||||
}
|
||||
|
||||
public String getLoginTime() {
|
||||
return loginTime;
|
||||
}
|
||||
|
||||
public String getRecommendation() {
|
||||
return recommendation;
|
||||
}
|
||||
}
|
||||
84
web/WEB-INF/classes/com/demo/action/UserAction.java
Normal file
84
web/WEB-INF/classes/com/demo/action/UserAction.java
Normal file
@@ -0,0 +1,84 @@
|
||||
package com.demo.action;
|
||||
|
||||
import com.opensymphony.xwork2.ActionSupport;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
public class UserAction extends ActionSupport {
|
||||
|
||||
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
private String username;
|
||||
private String email;
|
||||
private String phone;
|
||||
private String submittedAt;
|
||||
private String profileStage;
|
||||
|
||||
public String submit() {
|
||||
username = normalize(username);
|
||||
email = normalize(email);
|
||||
phone = normalize(phone);
|
||||
|
||||
if (!isValid()) {
|
||||
return INPUT;
|
||||
}
|
||||
|
||||
submittedAt = LocalDateTime.now().format(TIME_FORMATTER);
|
||||
profileStage = (phone != null && phone.length() >= 7) ? "Profile ready for follow-up demos." : "Profile captured. Add a stronger phone number next time.";
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
private boolean isValid() {
|
||||
boolean valid = true;
|
||||
if (username == null || username.length() < 3) {
|
||||
addFieldError("username", "Username must be at least 3 characters.");
|
||||
valid = false;
|
||||
}
|
||||
if (email == null || !email.contains("@")) {
|
||||
addFieldError("email", "Enter a valid email address.");
|
||||
valid = false;
|
||||
}
|
||||
if (phone == null || phone.replaceAll("[^0-9]", "").length() < 7) {
|
||||
addFieldError("phone", "Enter at least 7 digits for the phone number.");
|
||||
valid = false;
|
||||
}
|
||||
return valid;
|
||||
}
|
||||
|
||||
private String normalize(String value) {
|
||||
return value == null ? null : value.trim();
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public String getPhone() {
|
||||
return phone;
|
||||
}
|
||||
|
||||
public void setPhone(String phone) {
|
||||
this.phone = phone;
|
||||
}
|
||||
|
||||
public String getSubmittedAt() {
|
||||
return submittedAt;
|
||||
}
|
||||
|
||||
public String getProfileStage() {
|
||||
return profileStage;
|
||||
}
|
||||
}
|
||||
88
web/WEB-INF/classes/com/demo/action/ValidationAction.java
Normal file
88
web/WEB-INF/classes/com/demo/action/ValidationAction.java
Normal file
@@ -0,0 +1,88 @@
|
||||
package com.demo.action;
|
||||
|
||||
import com.opensymphony.xwork2.ActionSupport;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
public class ValidationAction extends ActionSupport {
|
||||
|
||||
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
private String username;
|
||||
private String email;
|
||||
private Integer age;
|
||||
private String bio;
|
||||
private String scoreBand;
|
||||
private String submittedAt;
|
||||
|
||||
@Override
|
||||
public String execute() {
|
||||
username = normalize(username);
|
||||
email = normalize(email);
|
||||
bio = normalize(bio);
|
||||
scoreBand = age >= 30 ? "Mid-career operator profile" : "Early-career operator profile";
|
||||
submittedAt = LocalDateTime.now().format(TIME_FORMATTER);
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate() {
|
||||
if (username == null || username.trim().length() < 3 || username.trim().length() > 20) {
|
||||
addFieldError("username", "Username must be between 3 and 20 characters.");
|
||||
}
|
||||
if (email == null || !email.contains("@") || email.indexOf('@') == email.length() - 1) {
|
||||
addFieldError("email", "Enter a valid email address.");
|
||||
}
|
||||
if (age == null || age < 18 || age > 60) {
|
||||
addFieldError("age", "Age must be between 18 and 60.");
|
||||
}
|
||||
if (bio != null && bio.trim().length() > 240) {
|
||||
addFieldError("bio", "Bio must stay under 240 characters.");
|
||||
}
|
||||
}
|
||||
|
||||
private String normalize(String value) {
|
||||
return value == null ? null : value.trim();
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public Integer getAge() {
|
||||
return age;
|
||||
}
|
||||
|
||||
public void setAge(Integer age) {
|
||||
this.age = age;
|
||||
}
|
||||
|
||||
public String getBio() {
|
||||
return bio;
|
||||
}
|
||||
|
||||
public void setBio(String bio) {
|
||||
this.bio = bio;
|
||||
}
|
||||
|
||||
public String getScoreBand() {
|
||||
return scoreBand;
|
||||
}
|
||||
|
||||
public String getSubmittedAt() {
|
||||
return submittedAt;
|
||||
}
|
||||
}
|
||||
38
web/WEB-INF/classes/com/demo/action/rest/UserRestAction.java
Normal file
38
web/WEB-INF/classes/com/demo/action/rest/UserRestAction.java
Normal file
@@ -0,0 +1,38 @@
|
||||
package com.demo.action.rest;
|
||||
|
||||
import com.demo.model.User;
|
||||
import com.opensymphony.xwork2.ActionSupport;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class UserRestAction extends ActionSupport {
|
||||
|
||||
private boolean success;
|
||||
private String message;
|
||||
private List<User> users;
|
||||
|
||||
@Override
|
||||
public String execute() {
|
||||
success = true;
|
||||
message = "REST-style demo payload";
|
||||
users = Arrays.asList(
|
||||
new User(101L, "alpha", "alpha@example.com", "13800001001"),
|
||||
new User(102L, "beta", "beta@example.com", "13800001002"),
|
||||
new User(103L, "gamma", "gamma@example.com", "13800001003")
|
||||
);
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
public boolean isSuccess() {
|
||||
return success;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public List<User> getUsers() {
|
||||
return users;
|
||||
}
|
||||
}
|
||||
54
web/WEB-INF/classes/com/demo/model/User.java
Normal file
54
web/WEB-INF/classes/com/demo/model/User.java
Normal file
@@ -0,0 +1,54 @@
|
||||
package com.demo.model;
|
||||
|
||||
public class User {
|
||||
private Long id;
|
||||
private String username;
|
||||
private String email;
|
||||
private String phone;
|
||||
|
||||
public User() {
|
||||
}
|
||||
|
||||
public User(Long id, String username, String email) {
|
||||
this(id, username, email, null);
|
||||
}
|
||||
|
||||
public User(Long id, String username, String email, String phone) {
|
||||
this.id = id;
|
||||
this.username = username;
|
||||
this.email = email;
|
||||
this.phone = phone;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public String getPhone() {
|
||||
return phone;
|
||||
}
|
||||
|
||||
public void setPhone(String phone) {
|
||||
this.phone = phone;
|
||||
}
|
||||
}
|
||||
50
web/WEB-INF/classes/struts.xml
Normal file
50
web/WEB-INF/classes/struts.xml
Normal file
@@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE struts PUBLIC
|
||||
"-//Apache Software Foundation//DTD Struts Configuration 2.5//EN"
|
||||
"http://struts.apache.org/dtds/struts-2.5.dtd">
|
||||
|
||||
<struts>
|
||||
<constant name="struts.devMode" value="true"/>
|
||||
<constant name="struts.enable.DynamicMethodInvocation" value="true"/>
|
||||
<constant name="struts.i18n.encoding" value="UTF-8"/>
|
||||
|
||||
<package name="default" namespace="/" extends="struts-default">
|
||||
<action name="index">
|
||||
<result>/index.jsp</result>
|
||||
</action>
|
||||
|
||||
<action name="hello" class="com.demo.action.HelloAction" method="execute">
|
||||
<result>/hello.jsp</result>
|
||||
</action>
|
||||
|
||||
<action name="login" class="com.demo.action.LoginAction" method="execute">
|
||||
<result name="success">/user/success.jsp</result>
|
||||
<result name="input">/user/login.jsp</result>
|
||||
</action>
|
||||
|
||||
<action name="submitUser" class="com.demo.action.UserAction" method="submit">
|
||||
<result name="success">/user/success.jsp</result>
|
||||
<result name="input">/user/form.jsp</result>
|
||||
</action>
|
||||
|
||||
<action name="upload" class="com.demo.action.FileUploadAction" method="execute">
|
||||
<result name="success">/upload/success.jsp</result>
|
||||
<result name="input">/upload/index.jsp</result>
|
||||
</action>
|
||||
|
||||
<action name="ajax" class="com.demo.action.AjaxAction" method="execute">
|
||||
<result type="json"/>
|
||||
</action>
|
||||
|
||||
<action name="validate" class="com.demo.action.ValidationAction" method="execute">
|
||||
<result name="success">/validation/success.jsp</result>
|
||||
<result name="input">/validation/form.jsp</result>
|
||||
</action>
|
||||
</package>
|
||||
|
||||
<package name="rest" namespace="/api" extends="struts-default">
|
||||
<action name="users" class="com.demo.action.rest.UserRestAction" method="execute">
|
||||
<result type="json"/>
|
||||
</action>
|
||||
</package>
|
||||
</struts>
|
||||
@@ -1,11 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<web-app>
|
||||
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
|
||||
http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
|
||||
version="4.0">
|
||||
|
||||
<display-name>Struts2 Demo Lab</display-name>
|
||||
|
||||
<filter>
|
||||
<filter-name>struts2</filter-name>
|
||||
<filter-class>org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter</filter-class>
|
||||
</filter>
|
||||
|
||||
<filter-mapping>
|
||||
<filter-name>struts2</filter-name>
|
||||
<url-pattern>/*</url-pattern>
|
||||
</filter-mapping>
|
||||
|
||||
<welcome-file-list>
|
||||
<welcome-file>index.jsp</welcome-file>
|
||||
</welcome-file-list>
|
||||
|
||||
<error-page>
|
||||
<error-code>404</error-code>
|
||||
<location>/error/404.jsp</location>
|
||||
</error-page>
|
||||
<error-page>
|
||||
<error-code>500</error-code>
|
||||
<location>/error/500.jsp</location>
|
||||
</error-page>
|
||||
</web-app>
|
||||
|
||||
51
web/demo/ajax/index.jsp
Normal file
51
web/demo/ajax/index.jsp
Normal file
@@ -0,0 +1,51 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AJAX and JSON Guide</title>
|
||||
<style>
|
||||
body { margin: 0; padding: 24px; font-family: "Aptos", "Segoe UI", sans-serif; background: linear-gradient(135deg, #122c63, #1464c7); }
|
||||
.shell { max-width: 980px; margin: 0 auto; background: rgba(255,255,255,0.96); border-radius: 28px; padding: 28px; box-shadow: 0 24px 60px rgba(0,0,0,0.18); }
|
||||
.eyebrow { font-size: 12px; text-transform: uppercase; letter-spacing: 0.12em; color: #1464c7; font-weight: 800; }
|
||||
h1, h2 { margin: 10px 0 12px; }
|
||||
p, li { color: #53667d; line-height: 1.9; }
|
||||
pre { background: #101827; color: #d9e7ff; padding: 18px; border-radius: 18px; overflow-x: auto; }
|
||||
.links { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 18px; }
|
||||
.btn { display: inline-flex; padding: 10px 14px; border-radius: 999px; text-decoration: none; font-weight: 700; background: #e8f2ff; color: #1464c7; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<div class="eyebrow">Guide</div>
|
||||
<h1>Returning JSON from Struts2 actions</h1>
|
||||
<p>The demo now includes both an action-level JSON response and a REST-style JSON route. That makes it easier to compare MVC pages with lightweight API payloads.</p>
|
||||
|
||||
<h2>What the action returns</h2>
|
||||
<ul>
|
||||
<li>A <code>success</code> flag.</li>
|
||||
<li>A short message describing the payload.</li>
|
||||
<li>Sample user records that can be consumed by AJAX.</li>
|
||||
</ul>
|
||||
|
||||
<pre>public class AjaxAction extends ActionSupport {
|
||||
private boolean success;
|
||||
private String message;
|
||||
private List<User> users;
|
||||
|
||||
public String execute() {
|
||||
success = true;
|
||||
message = "Mock AJAX response";
|
||||
return SUCCESS;
|
||||
}
|
||||
}</pre>
|
||||
|
||||
<div class="links">
|
||||
<a class="btn" href="../../ajax">Open action JSON</a>
|
||||
<a class="btn" href="../../api/users">Open REST JSON</a>
|
||||
<a class="btn" href="../../index.jsp">Back to portal</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
53
web/demo/hello/index.jsp
Normal file
53
web/demo/hello/index.jsp
Normal file
@@ -0,0 +1,53 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Hello Action Guide</title>
|
||||
<style>
|
||||
body { margin: 0; padding: 24px; font-family: "Aptos", "Segoe UI", sans-serif; background: linear-gradient(135deg, #1464c7, #3a8dff); }
|
||||
.shell { max-width: 980px; margin: 0 auto; background: rgba(255,255,255,0.96); border-radius: 28px; padding: 28px; box-shadow: 0 24px 60px rgba(0,0,0,0.18); }
|
||||
.eyebrow { font-size: 12px; text-transform: uppercase; letter-spacing: 0.12em; color: #1464c7; font-weight: 800; }
|
||||
h1, h2 { margin: 10px 0 12px; }
|
||||
p, li { color: #53667d; line-height: 1.9; }
|
||||
pre { background: #101827; color: #d9e7ff; padding: 18px; border-radius: 18px; overflow-x: auto; }
|
||||
.links { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 18px; }
|
||||
.btn { display: inline-flex; padding: 10px 14px; border-radius: 999px; text-decoration: none; font-weight: 700; background: #e8f2ff; color: #1464c7; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<div class="eyebrow">Guide</div>
|
||||
<h1>Hello action walkthrough</h1>
|
||||
<p>The hello demo is still the best first stop for explaining how Struts2 maps a request to an action and then routes to a JSP result.</p>
|
||||
|
||||
<h2>Flow</h2>
|
||||
<ul>
|
||||
<li>The browser requests <code>/hello</code> or <code>/hello?name=Team</code>.</li>
|
||||
<li>Struts populates the <code>name</code> property on <code>HelloAction</code>.</li>
|
||||
<li><code>execute()</code> builds a view message and returns <code>SUCCESS</code>.</li>
|
||||
<li>The framework resolves the success result to <code>/hello.jsp</code>.</li>
|
||||
</ul>
|
||||
|
||||
<h2>Minimal action shape</h2>
|
||||
<pre>public class HelloAction extends ActionSupport {
|
||||
private String name;
|
||||
private String message;
|
||||
|
||||
public String execute() {
|
||||
if (name == null || name.trim().isEmpty()) {
|
||||
name = "World";
|
||||
}
|
||||
message = "Hello, " + name + "!";
|
||||
return SUCCESS;
|
||||
}
|
||||
}</pre>
|
||||
|
||||
<div class="links">
|
||||
<a class="btn" href="../../hello?name=Platform%20Team">Run hello action</a>
|
||||
<a class="btn" href="../../index.jsp">Back to portal</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
54
web/demo/model/index.jsp
Normal file
54
web/demo/model/index.jsp
Normal file
@@ -0,0 +1,54 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Model Binding Guide</title>
|
||||
<style>
|
||||
body { margin: 0; padding: 24px; font-family: "Aptos", "Segoe UI", sans-serif; background: linear-gradient(135deg, #7c3aed, #a855f7); }
|
||||
.shell { max-width: 980px; margin: 0 auto; background: rgba(255,255,255,0.96); border-radius: 28px; padding: 28px; box-shadow: 0 24px 60px rgba(0,0,0,0.18); }
|
||||
.eyebrow { font-size: 12px; text-transform: uppercase; letter-spacing: 0.12em; color: #7c3aed; font-weight: 800; }
|
||||
h1, h2 { margin: 10px 0 12px; }
|
||||
p, li { color: #5e5570; line-height: 1.9; }
|
||||
pre { background: #101827; color: #d9e7ff; padding: 18px; border-radius: 18px; overflow-x: auto; }
|
||||
.links { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 18px; }
|
||||
.btn { display: inline-flex; padding: 10px 14px; border-radius: 999px; text-decoration: none; font-weight: 700; background: #f0e9ff; color: #7c3aed; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<div class="eyebrow">Guide</div>
|
||||
<h1>Property binding vs model-driven binding</h1>
|
||||
<p>This note helps explain how Struts2 turns request parameters into action state. It works well alongside the user form and validation examples.</p>
|
||||
|
||||
<h2>Property-driven</h2>
|
||||
<ul>
|
||||
<li>Define simple fields directly on the action.</li>
|
||||
<li>Expose setters and getters for each form field.</li>
|
||||
<li>Great for short demos and small forms.</li>
|
||||
</ul>
|
||||
|
||||
<h2>Model-driven</h2>
|
||||
<ul>
|
||||
<li>Return a dedicated model object from <code>getModel()</code>.</li>
|
||||
<li>Keep request data separate from action orchestration logic.</li>
|
||||
<li>Better for larger forms and nested objects.</li>
|
||||
</ul>
|
||||
|
||||
<pre>public class UserAction extends ActionSupport implements ModelDriven<User> {
|
||||
private final User user = new User();
|
||||
|
||||
@Override
|
||||
public User getModel() {
|
||||
return user;
|
||||
}
|
||||
}</pre>
|
||||
|
||||
<div class="links">
|
||||
<a class="btn" href="../../user/form.jsp">Open user form</a>
|
||||
<a class="btn" href="../../index.jsp">Back to portal</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
50
web/demo/upload/index.jsp
Normal file
50
web/demo/upload/index.jsp
Normal file
@@ -0,0 +1,50 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Upload Guide</title>
|
||||
<style>
|
||||
body { margin: 0; padding: 24px; font-family: "Aptos", "Segoe UI", sans-serif; background: linear-gradient(135deg, #0ea5e9, #38bdf8); }
|
||||
.shell { max-width: 980px; margin: 0 auto; background: rgba(255,255,255,0.96); border-radius: 28px; padding: 28px; box-shadow: 0 24px 60px rgba(0,0,0,0.18); }
|
||||
.eyebrow { font-size: 12px; text-transform: uppercase; letter-spacing: 0.12em; color: #0284c7; font-weight: 800; }
|
||||
h1, h2 { margin: 10px 0 12px; }
|
||||
p, li { color: #53667d; line-height: 1.9; }
|
||||
pre { background: #101827; color: #d9e7ff; padding: 18px; border-radius: 18px; overflow-x: auto; }
|
||||
.links { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 18px; }
|
||||
.btn { display: inline-flex; padding: 10px 14px; border-radius: 999px; text-decoration: none; font-weight: 700; background: #edf7ff; color: #0284c7; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<div class="eyebrow">Guide</div>
|
||||
<h1>Upload demo guide</h1>
|
||||
<p>The refreshed upload flow is deliberately safer for demo use. It shows multipart binding and metadata capture without persisting files.</p>
|
||||
|
||||
<h2>Core binding fields</h2>
|
||||
<ul>
|
||||
<li><code>File upload</code> for the primary file object.</li>
|
||||
<li><code>String uploadFileName</code> for the original filename.</li>
|
||||
<li><code>String uploadContentType</code> for the MIME type.</li>
|
||||
<li>Optional lists for multiple files.</li>
|
||||
</ul>
|
||||
|
||||
<pre>public class FileUploadAction extends ActionSupport {
|
||||
private File upload;
|
||||
private String uploadFileName;
|
||||
private String uploadContentType;
|
||||
|
||||
public String execute() {
|
||||
// demo keeps metadata only
|
||||
return SUCCESS;
|
||||
}
|
||||
}</pre>
|
||||
|
||||
<div class="links">
|
||||
<a class="btn" href="../../upload/index.jsp">Open upload demo</a>
|
||||
<a class="btn" href="../../index.jsp">Back to portal</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
227
web/demo/validation/index.jsp
Normal file
227
web/demo/validation/index.jsp
Normal file
@@ -0,0 +1,227 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>表单验证 - Struts2 学习</title>
|
||||
<style>
|
||||
body { font-family: 'Segoe UI', 'PingFang SC', sans-serif; background: linear-gradient(135deg, #667eea, #764ba2); min-height: 100vh; margin: 0; padding: 20px; }
|
||||
.container { max-width: 1200px; margin: 0 auto; }
|
||||
.breadcrumb { background: white; padding: 15px 25px; border-radius: 10px; margin-bottom: 20px; }
|
||||
.breadcrumb a { color: #667eea; text-decoration: none; }
|
||||
.content { background: white; border-radius: 20px; padding: 40px; box-shadow: 0 10px 40px rgba(0,0,0,0.2); }
|
||||
h1 { color: #667eea; border-bottom: 3px solid #667eea; padding-bottom: 15px; }
|
||||
h2 { color: #764ba2; margin-top: 30px; }
|
||||
.section { margin: 25px 0; padding: 20px; background: #f8f9fa; border-radius: 10px; }
|
||||
pre { background: #1e1e1e; color: #d4d4d4; padding: 20px; border-radius: 10px; overflow-x: auto; font-size: 0.9em; }
|
||||
.keyword { color: #569cd6; }
|
||||
.string { color: #ce9178; }
|
||||
.comment { color: #6a9955; }
|
||||
.btn { display: inline-block; padding: 12px 30px; background: #667eea; color: white; text-decoration: none; border-radius: 25px; margin-right: 10px; }
|
||||
.note { background: #fff3e0; border-left: 4px solid #ff9800; padding: 15px; margin: 20px 0; border-radius: 0 10px 10px 0; }
|
||||
.tip { background: #e3f2fd; border-left: 4px solid #2196f3; padding: 15px; margin: 20px 0; border-radius: 0 10px 10px 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="breadcrumb">
|
||||
<a href="/">🏠 首页</a> / 表单验证
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<h1>✅ 表单验证</h1>
|
||||
|
||||
<div class="note">
|
||||
<strong>📝 本节要点:</strong> 掌握 Struts2 的两种验证方式:编程式验证和声明式验证
|
||||
</div>
|
||||
|
||||
<h2>1. 验证方式概览</h2>
|
||||
<div class="section">
|
||||
<table style="width:100%; border-collapse: collapse;">
|
||||
<tr style="background:#667eea; color:white;">
|
||||
<th style="padding:10px; text-align:left;">方式</th>
|
||||
<th style="padding:10px; text-align:left;">实现方式</th>
|
||||
<th style="padding:10px; text-align:left;">适用场景</th>
|
||||
</tr>
|
||||
<tr style="background:#f8f9fa;">
|
||||
<td style="padding:10px;">编程式</td>
|
||||
<td style="padding:10px;">重写 validate() 方法</td>
|
||||
<td style="padding:10px;">简单验证,逻辑复杂</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:10px;">声明式 - 方法级</td>
|
||||
<td style="padding:10px;">validateXxx() 方法</td>
|
||||
<td style="padding:10px;">特定方法的验证</td>
|
||||
</tr>
|
||||
<tr style="background:#f8f9fa;">
|
||||
<td style="padding:10px;">声明式 - XML</td>
|
||||
<td style="padding:10px;">XxxAction-validation.xml</td>
|
||||
<td style="padding:10px;">可复用,团队协作</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h2>2. 方式一:编程式验证 (validate)</h2>
|
||||
<div class="section">
|
||||
<pre><span class="keyword">public class</span> LoginAction <span class="keyword">extends</span> ActionSupport {
|
||||
|
||||
<span class="keyword">private</span> String username;
|
||||
<span class="keyword">private</span> String password;
|
||||
|
||||
<span class="annotation">@Override</span>
|
||||
<span class="keyword">public</span> String execute() {
|
||||
<span class="comment">// 登录逻辑...</span>
|
||||
<span class="keyword">return</span> SUCCESS;
|
||||
}
|
||||
|
||||
<span class="comment">/**
|
||||
* 验证方法 - 对所有 execute 方法都生效
|
||||
*/</span>
|
||||
<span class="annotation">@Override</span>
|
||||
<span class="keyword">public void</span> validate() {
|
||||
<span class="comment">// 用户名验证</span>
|
||||
<span class="keyword">if</span> (username == <span class="keyword">null</span> || username.trim().length() < 3) {
|
||||
addFieldError(<span class="string">"username"</span>, <span class="string">"用户名至少3个字符"</span>);
|
||||
}
|
||||
|
||||
<span class="comment">// 密码验证</span>
|
||||
<span class="keyword">if</span> (password == <span class="keyword">null</span> || password.length() < 6) {
|
||||
addFieldError(<span class="string">"password"</span>, <span class="string">"密码至少6个字符"</span>);
|
||||
}
|
||||
}
|
||||
|
||||
<span class="comment">// getter/setter...</span>
|
||||
}</pre>
|
||||
</div>
|
||||
|
||||
<h2>3. 方式二:方法级验证 (validateXxx)</h2>
|
||||
<div class="section">
|
||||
<pre><span class="comment">/**
|
||||
* 只验证 register 方法,不验证 login 方法
|
||||
*/</span>
|
||||
<span class="keyword">public void</span> validateRegister() {
|
||||
<span class="keyword">if</span> (email == <span class="keyword">null</span> || !email.contains(<span class="string">"@"</span>)) {
|
||||
addFieldError(<span class="string">"email"</span>, <span class="string">"邮箱格式不正确"</span>);
|
||||
}
|
||||
}</pre>
|
||||
</div>
|
||||
|
||||
<h2>4. 方式三:XML 声明式验证</h2>
|
||||
<div class="tip">
|
||||
<strong>优点:</strong> 验证规则与代码分离,团队协作更清晰,可复用
|
||||
</div>
|
||||
|
||||
<h3>4.1 创建验证文件</h3>
|
||||
<div class="section">
|
||||
<p>文件名格式:<code>ActionName-validation.xml</code></p>
|
||||
<p>位置:<code>WEB-INF/classes/com/demo/action/</code></p>
|
||||
<pre><span class="comment"><?xml version="1.0" encoding="UTF-8"?></span>
|
||||
<span class="keyword"><!DOCTYPE validators PUBLIC</span>
|
||||
<span class="string">"-//OpenSymphony Group//XWork Validator 1.0.3//EN"</span>
|
||||
<span class="string">"http://www.opensymphony.com/xwork/xwork-validator-1.0.3.dtd"</span><span class="keyword">></span>
|
||||
|
||||
<span class="keyword"><validators></span>
|
||||
<span class="comment"><!-- 字段验证 --></span>
|
||||
<span class="keyword"><field</span> name=<span class="string">"username"</span><span class="keyword">></span>
|
||||
<span class="keyword"><field-validator</span> type=<span class="string">"requiredstring"</span><span class="keyword">></span>
|
||||
<span class="keyword"><message></span>用户名不能为空<span class="keyword"></message></span>
|
||||
<span class="keyword"></field-validator></span>
|
||||
<span class="keyword"><field-validator</span> type=<span class="string">"stringlength"</span><span class="keyword">></span>
|
||||
<span class="keyword"><param</span> name=<span class="string">"minLength"</span><span class="keyword">></span>3<span class="keyword"></param></span>
|
||||
<span class="keyword"><param</span> name=<span class="string">"maxLength"</span><span class="keyword">></span>20<span class="keyword"></param></span>
|
||||
<span class="keyword"><message></span>用户名长度3-20字符<span class="keyword"></message></span>
|
||||
<span class="keyword"></field-validator></span>
|
||||
<span class="keyword"></field></span>
|
||||
|
||||
<span class="comment"><!-- 字段验证:email --></span>
|
||||
<span class="keyword"><field</span> name=<span class="string">"email"</span><span class="keyword">></span>
|
||||
<span class="keyword"><field-validator</span> type=<span class="string">"email"</span><span class="keyword">></span>
|
||||
<span class="keyword"><message></span>邮箱格式不正确<span class="keyword"></message></span>
|
||||
<span class="keyword"></field-validator></span>
|
||||
<span class="keyword"></field></span>
|
||||
|
||||
<span class="comment"><!-- 非字段验证 --></span>
|
||||
<span class="keyword"><validator</span> type=<span class="string">"expression"</span><span class="keyword">></span>
|
||||
<span class="keyword"><param</span> name=<span class="string">"expression"</span><span class="keyword">></span>password==confirmPassword<span class="keyword"></param></span>
|
||||
<span class="keyword"><message></span>两次密码不一致<span class="keyword"></message></span>
|
||||
<span class="keyword"></validator></span>
|
||||
<span class="keyword"></validators></span></pre>
|
||||
</div>
|
||||
|
||||
<h2>5. 常用验证器类型</h2>
|
||||
<div class="section">
|
||||
<table style="width:100%; border-collapse: collapse;">
|
||||
<tr style="background:#667eea; color:white;">
|
||||
<th style="padding:10px; text-align:left;">验证器</th>
|
||||
<th style="padding:10px; text-align:left;">说明</th>
|
||||
<th style="padding:10px; text-align:left;">示例</th>
|
||||
</tr>
|
||||
<tr style="background:#f8f9fa;">
|
||||
<td style="padding:10px;">required</td>
|
||||
<td style="padding:10px;">必填</td>
|
||||
<td style="padding:10px;">username 不能为空</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:10px;">requiredstring</td>
|
||||
<td style="padding:10px;">字符串必填</td>
|
||||
<td style="padding:10px;">不能为空字符串</td>
|
||||
</tr>
|
||||
<tr style="background:#f8f9fa;">
|
||||
<td style="padding:10px;">stringlength</td>
|
||||
<td style="padding:10px;">字符串长度</td>
|
||||
<td style="padding:10px;">minLength, maxLength</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:10px;">email</td>
|
||||
<td style="padding:10px;">邮箱格式</td>
|
||||
<td style="padding:10px;">xxx@xxx.com</td>
|
||||
</tr>
|
||||
<tr style="background:#f8f9fa;">
|
||||
<td style="padding:10px;">regex</td>
|
||||
<td style="padding:10px;">正则表达式</td>
|
||||
<td style="padding:10px;">手机号、身份证等</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:10px;">int</td>
|
||||
<td style="padding:10px;">整数范围</td>
|
||||
<td style="padding:10px;">min, max</td>
|
||||
</tr>
|
||||
<tr style="background:#f8f9fa;">
|
||||
<td style="padding:10px;">url</td>
|
||||
<td style="padding:10px;">URL 格式</td>
|
||||
<td style="padding:10px;">http://xxx.com</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:10px;">date</td>
|
||||
<td style="padding:10px;">日期范围</td>
|
||||
<td style="padding:10px;">min, max</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h2>6. 在 JSP 中显示错误</h2>
|
||||
<div class="section">
|
||||
<pre><span class="comment"><%@ taglib prefix="s" uri="/struts-tags" %></span>
|
||||
|
||||
<span class="comment"><!-- 显示特定字段的错误 --></span>
|
||||
<span class="keyword"><s:fielderror</span> fieldName=<span class="string">"username"</span>/>
|
||||
|
||||
<span class="comment"><!-- 显示所有字段错误 --></span>
|
||||
<span class="keyword"><s:fielderror</span>/>
|
||||
|
||||
<span class="comment"><!-- 显示 Action 级别错误 --></span>
|
||||
<span class="keyword"><s:actionerror</span>/>
|
||||
|
||||
<span class="comment"><!-- 显示 Action 级别消息 --></span>
|
||||
<span class="keyword"><s:actionmessage</span>/></pre>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 30px;">
|
||||
<a href="/demo/model" class="btn">下一节:数据封装 →</a>
|
||||
<a href="/" class="btn" style="background: #764ba2;">← 返回首页</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
47
web/error/404.jsp
Normal file
47
web/error/404.jsp
Normal file
@@ -0,0 +1,47 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>404 - Struts2 Demo Lab</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: "Aptos", "Segoe UI", "Microsoft YaHei", sans-serif;
|
||||
background: linear-gradient(135deg, #122c63 0%, #1464c7 52%, #f68b1f 100%);
|
||||
}
|
||||
.card {
|
||||
width: min(680px, calc(100vw - 32px));
|
||||
background: rgba(255,255,255,0.95);
|
||||
border-radius: 28px;
|
||||
padding: 28px;
|
||||
text-align: center;
|
||||
box-shadow: 0 24px 60px rgba(0,0,0,0.22);
|
||||
}
|
||||
h1 { margin: 0 0 12px; font-size: 52px; color: #1464c7; }
|
||||
p { margin: 0; color: #56697f; line-height: 1.85; }
|
||||
a {
|
||||
display: inline-flex;
|
||||
margin-top: 20px;
|
||||
padding: 12px 18px;
|
||||
border-radius: 999px;
|
||||
background: #e8f2ff;
|
||||
color: #1464c7;
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<h1>404</h1>
|
||||
<p>The requested demo page was not found. Use the portal to jump back into the working Struts2 examples.</p>
|
||||
<a href="../index.jsp">Back to portal</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
47
web/error/500.jsp
Normal file
47
web/error/500.jsp
Normal file
@@ -0,0 +1,47 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>500 - Struts2 Demo Lab</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: "Aptos", "Segoe UI", "Microsoft YaHei", sans-serif;
|
||||
background: linear-gradient(135deg, #7f1d1d 0%, #dc2626 55%, #fee2e2 100%);
|
||||
}
|
||||
.card {
|
||||
width: min(700px, calc(100vw - 32px));
|
||||
background: rgba(255,255,255,0.95);
|
||||
border-radius: 28px;
|
||||
padding: 28px;
|
||||
text-align: center;
|
||||
box-shadow: 0 24px 60px rgba(0,0,0,0.22);
|
||||
}
|
||||
h1 { margin: 0 0 12px; font-size: 52px; color: #b91c1c; }
|
||||
p { margin: 0; color: #6d4b4b; line-height: 1.85; }
|
||||
a {
|
||||
display: inline-flex;
|
||||
margin-top: 20px;
|
||||
padding: 12px 18px;
|
||||
border-radius: 999px;
|
||||
background: #fee2e2;
|
||||
color: #b91c1c;
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<h1>500</h1>
|
||||
<p>The demo hit an internal error. Return to the portal and try one of the working examples again.</p>
|
||||
<a href="../index.jsp">Back to portal</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
124
web/hello.jsp
Normal file
124
web/hello.jsp
Normal file
@@ -0,0 +1,124 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="s" uri="/struts-tags" %>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Hello Action - Struts2 Demo Lab</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: "Aptos", "Segoe UI", "Microsoft YaHei", sans-serif;
|
||||
background: linear-gradient(135deg, #1464c7 0%, #3f8df7 52%, #f68b1f 100%);
|
||||
color: #122033;
|
||||
}
|
||||
.card {
|
||||
width: min(860px, calc(100vw - 32px));
|
||||
background: rgba(255,255,255,0.94);
|
||||
border: 1px solid rgba(255,255,255,0.45);
|
||||
border-radius: 30px;
|
||||
padding: 30px;
|
||||
box-shadow: 0 24px 60px rgba(0,0,0,0.24);
|
||||
}
|
||||
.eyebrow {
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: #1464c7;
|
||||
font-weight: 800;
|
||||
}
|
||||
h1 {
|
||||
margin: 12px 0;
|
||||
font-size: 42px;
|
||||
line-height: 1.12;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
color: #53667d;
|
||||
line-height: 1.9;
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.panel {
|
||||
padding: 18px;
|
||||
border-radius: 20px;
|
||||
background: #f6f9fd;
|
||||
border: 1px solid #dbe5f0;
|
||||
}
|
||||
.panel h3 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 18px;
|
||||
}
|
||||
.panel code {
|
||||
display: inline-block;
|
||||
margin-top: 8px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 10px;
|
||||
background: #e8f2ff;
|
||||
color: #1464c7;
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 24px;
|
||||
}
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 12px 18px;
|
||||
border-radius: 999px;
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
}
|
||||
.btn-primary { background: #1464c7; color: #fff; }
|
||||
.btn-soft { background: #e8f2ff; color: #1464c7; }
|
||||
@media (max-width: 720px) {
|
||||
.grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<div class="eyebrow">Hello Action</div>
|
||||
<h1><s:property value="message" default="Hello from Struts2!"/></h1>
|
||||
<p>This page proves the classic request -> action -> result flow. It accepts a name parameter, prepares a response in the action, and renders it through the JSP view.</p>
|
||||
|
||||
<div class="grid">
|
||||
<section class="panel">
|
||||
<h3>What just happened</h3>
|
||||
<p><s:property value="tip" default="The action populated a few view properties before returning SUCCESS."/></p>
|
||||
</section>
|
||||
<section class="panel">
|
||||
<h3>Good next step</h3>
|
||||
<p><s:property value="nextStep" default="Open the login flow next."/></p>
|
||||
</section>
|
||||
<section class="panel">
|
||||
<h3>Try another request</h3>
|
||||
<p>Use a custom query parameter to change the greeting.</p>
|
||||
<code><s:property value="requestSample" default="/hello?name=Platform%20Team"/></code>
|
||||
</section>
|
||||
<section class="panel">
|
||||
<h3>Current input</h3>
|
||||
<p>Name value: <strong><s:property value="name" default="World"/></strong></p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<a class="btn btn-primary" href="hello?name=Platform%20Team">Run again with sample name</a>
|
||||
<a class="btn btn-soft" href="user/login.jsp">Open login demo</a>
|
||||
<a class="btn btn-soft" href="index.jsp">Back to portal</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
514
web/index.jsp
Normal file
514
web/index.jsp
Normal file
@@ -0,0 +1,514 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Struts2 Demo Lab</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #f4f6fb;
|
||||
--panel: rgba(255,255,255,0.94);
|
||||
--line: #d7e0ea;
|
||||
--text: #122033;
|
||||
--muted: #5c6f84;
|
||||
--brand: #1464c7;
|
||||
--brand-2: #f68b1f;
|
||||
--soft: #e8f2ff;
|
||||
--shadow: 0 20px 44px rgba(18, 32, 51, 0.14);
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
font-family: "Aptos", "Segoe UI", "Microsoft YaHei", sans-serif;
|
||||
color: var(--text);
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(20, 100, 199, 0.15), transparent 28%),
|
||||
radial-gradient(circle at bottom left, rgba(246, 139, 31, 0.12), transparent 24%),
|
||||
var(--bg);
|
||||
}
|
||||
a { color: inherit; text-decoration: none; }
|
||||
.shell { max-width: 1480px; margin: 0 auto; padding: 24px; }
|
||||
.hero, .card {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 28px;
|
||||
box-shadow: var(--shadow);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
.hero {
|
||||
padding: 28px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.eyebrow {
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
font-weight: 800;
|
||||
color: var(--brand);
|
||||
}
|
||||
.hero-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.hero h1 {
|
||||
margin: 10px 0 12px;
|
||||
font-size: 42px;
|
||||
line-height: 1.1;
|
||||
}
|
||||
.hero p {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
line-height: 1.9;
|
||||
max-width: 860px;
|
||||
}
|
||||
.hero-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.btn, .btn-soft {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 999px;
|
||||
padding: 12px 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.btn { background: linear-gradient(135deg, var(--brand), #3a8dff); color: white; }
|
||||
.btn-soft { background: var(--soft); color: var(--brand); }
|
||||
.metrics {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.metric {
|
||||
padding: 16px;
|
||||
border-radius: 18px;
|
||||
border: 1px solid var(--line);
|
||||
background: rgba(255,255,255,0.56);
|
||||
}
|
||||
.metric span { display: block; color: var(--muted); font-size: 12px; margin-bottom: 8px; }
|
||||
.metric strong { font-size: 26px; }
|
||||
.layout {
|
||||
display: grid;
|
||||
grid-template-columns: 340px minmax(0, 1fr);
|
||||
gap: 18px;
|
||||
align-items: start;
|
||||
}
|
||||
.sidebar, .content { display: flex; flex-direction: column; gap: 18px; }
|
||||
.card { padding: 22px; }
|
||||
.search {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
.search input {
|
||||
flex: 1;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 16px;
|
||||
padding: 13px 14px;
|
||||
font: inherit;
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
outline: none;
|
||||
}
|
||||
.helper-list {
|
||||
margin: 14px 0 0;
|
||||
padding-left: 18px;
|
||||
color: var(--muted);
|
||||
line-height: 1.9;
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
.demo-card {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 22px;
|
||||
padding: 18px;
|
||||
background: rgba(255,255,255,0.45);
|
||||
transition: transform 0.18s ease, border-color 0.18s ease;
|
||||
}
|
||||
.demo-card:hover {
|
||||
transform: translateY(-3px);
|
||||
border-color: rgba(20, 100, 199, 0.28);
|
||||
}
|
||||
.demo-card h3 {
|
||||
margin: 10px 0 8px;
|
||||
font-size: 20px;
|
||||
}
|
||||
.demo-card p {
|
||||
margin: 0 0 14px;
|
||||
color: var(--muted);
|
||||
line-height: 1.8;
|
||||
}
|
||||
.tag-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.tag {
|
||||
display: inline-flex;
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
background: var(--soft);
|
||||
color: var(--brand);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.demo-links {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.link-btn {
|
||||
display: inline-flex;
|
||||
padding: 9px 14px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--line);
|
||||
color: var(--text);
|
||||
background: white;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.pipeline {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
.step {
|
||||
padding: 16px;
|
||||
border-radius: 18px;
|
||||
border: 1px solid var(--line);
|
||||
background: rgba(255,255,255,0.52);
|
||||
}
|
||||
.step strong { display: block; margin-bottom: 8px; }
|
||||
.step p { margin: 0; color: var(--muted); line-height: 1.75; }
|
||||
.empty {
|
||||
padding: 14px;
|
||||
border: 1px dashed var(--line);
|
||||
border-radius: 16px;
|
||||
color: var(--muted);
|
||||
text-align: center;
|
||||
}
|
||||
.tracker {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
.tracker-item {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
padding: 12px 14px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid var(--line);
|
||||
background: rgba(255,255,255,0.5);
|
||||
}
|
||||
.tracker-item input {
|
||||
margin-top: 3px;
|
||||
}
|
||||
.tracker-item strong {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
@media (max-width: 1100px) {
|
||||
.layout { grid-template-columns: 1fr; }
|
||||
.grid, .pipeline, .metrics { grid-template-columns: 1fr 1fr; }
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.shell { padding: 14px; }
|
||||
.hero-head, .search { flex-direction: column; align-items: stretch; }
|
||||
.grid, .pipeline, .metrics { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<section class="hero">
|
||||
<div class="hero-head">
|
||||
<div>
|
||||
<div class="eyebrow">Struts2 Demo Lab</div>
|
||||
<h1>Turn scattered examples into a guided demo portal</h1>
|
||||
<p>This workspace now acts like a mini learning application instead of a pile of sample pages. You can jump into action basics, login flow, validation, uploads, and JSON demos from one place.</p>
|
||||
</div>
|
||||
<div class="hero-actions">
|
||||
<a class="btn" href="hello?name=Platform%20Team">Run hello action</a>
|
||||
<a class="btn-soft" href="user/login.jsp">Open login flow</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="metrics">
|
||||
<div class="metric"><span>Live action routes</span><strong>5</strong></div>
|
||||
<div class="metric"><span>Guide pages</span><strong>4</strong></div>
|
||||
<div class="metric"><span>Demo forms</span><strong>3</strong></div>
|
||||
<div class="metric"><span>JSON endpoints</span><strong>2</strong></div>
|
||||
<div class="metric"><span>Core lab track</span><strong>4</strong></div>
|
||||
<div class="metric"><span>Completed labs</span><strong id="completedLabs">0</strong></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="layout">
|
||||
<aside class="sidebar">
|
||||
<section class="card">
|
||||
<div class="eyebrow">Quick search</div>
|
||||
<h2>Filter demos by keyword</h2>
|
||||
<div class="search">
|
||||
<input id="searchInput" type="text" placeholder="Try login, validation, upload, json, or hello" />
|
||||
</div>
|
||||
<ul class="helper-list">
|
||||
<li>Use the action demos to see real Struts requests and results.</li>
|
||||
<li>Use the guide pages to compare implementation patterns.</li>
|
||||
<li>Use the JSON routes to explain API-style actions in class.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<div class="eyebrow">Fast routes</div>
|
||||
<h2>Best places to start</h2>
|
||||
<div class="demo-links">
|
||||
<a class="link-btn" href="user/form.jsp">User form</a>
|
||||
<a class="link-btn" href="validation/form.jsp">Validation form</a>
|
||||
<a class="link-btn" href="upload/index.jsp">Upload flow</a>
|
||||
<a class="link-btn" href="api/users">JSON endpoint</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<div class="eyebrow">Lab tracker</div>
|
||||
<h2>Mark the demos you have finished</h2>
|
||||
<div class="tracker">
|
||||
<label class="tracker-item">
|
||||
<input type="checkbox" data-track="hello">
|
||||
<span>
|
||||
<strong>Hello action</strong>
|
||||
See the smallest request -> action -> result path.
|
||||
</span>
|
||||
</label>
|
||||
<label class="tracker-item">
|
||||
<input type="checkbox" data-track="login">
|
||||
<span>
|
||||
<strong>Login flow</strong>
|
||||
Follow validation, action errors, and success routing.
|
||||
</span>
|
||||
</label>
|
||||
<label class="tracker-item">
|
||||
<input type="checkbox" data-track="validation">
|
||||
<span>
|
||||
<strong>Validation flow</strong>
|
||||
Compare invalid and valid submissions.
|
||||
</span>
|
||||
</label>
|
||||
<label class="tracker-item">
|
||||
<input type="checkbox" data-track="json">
|
||||
<span>
|
||||
<strong>JSON and AJAX</strong>
|
||||
Finish by explaining how Struts can return API-style payloads.
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
</aside>
|
||||
|
||||
<main class="content">
|
||||
<section class="card">
|
||||
<div class="eyebrow">Request lifecycle</div>
|
||||
<h2>How to explain Struts2 with one mental model</h2>
|
||||
<div class="pipeline">
|
||||
<div class="step">
|
||||
<strong>1. Request enters</strong>
|
||||
<p>The browser sends a URL and optional form fields or query params.</p>
|
||||
</div>
|
||||
<div class="step">
|
||||
<strong>2. Action mapping</strong>
|
||||
<p><code>struts.xml</code> resolves the action name and target class.</p>
|
||||
</div>
|
||||
<div class="step">
|
||||
<strong>3. Parameter binding</strong>
|
||||
<p>Struts populates action properties before <code>execute()</code> runs.</p>
|
||||
</div>
|
||||
<div class="step">
|
||||
<strong>4. Result routing</strong>
|
||||
<p>The returned result name decides which JSP or JSON renderer will respond.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<div class="eyebrow">Catalog</div>
|
||||
<h2>Interactive demos and guides</h2>
|
||||
<div class="grid" id="demoGrid">
|
||||
<article class="demo-card" data-keywords="hello action request parameter basics">
|
||||
<div class="tag-list">
|
||||
<span class="tag">Action</span>
|
||||
<span class="tag">Beginner</span>
|
||||
</div>
|
||||
<h3>Hello action</h3>
|
||||
<p>Run a real Struts action, inject a request parameter, and inspect the rendered result page.</p>
|
||||
<div class="demo-links">
|
||||
<a class="link-btn" href="hello?name=Platform%20Team">Run demo</a>
|
||||
<a class="link-btn" href="demo/hello/index.jsp">Read guide</a>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="demo-card" data-keywords="login session form validation credentials">
|
||||
<div class="tag-list">
|
||||
<span class="tag">Form</span>
|
||||
<span class="tag">Core flow</span>
|
||||
</div>
|
||||
<h3>Login flow</h3>
|
||||
<p>Use demo credentials, see field validation, and land on a richer post-login dashboard.</p>
|
||||
<div class="demo-links">
|
||||
<a class="link-btn" href="user/login.jsp">Open login</a>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="demo-card" data-keywords="user form submit profile">
|
||||
<div class="tag-list">
|
||||
<span class="tag">Action</span>
|
||||
<span class="tag">Form submit</span>
|
||||
</div>
|
||||
<h3>User intake form</h3>
|
||||
<p>Submit a small profile payload and see action-backed validation with a success summary.</p>
|
||||
<div class="demo-links">
|
||||
<a class="link-btn" href="user/form.jsp">Open form</a>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="demo-card" data-keywords="validation field errors age email bio">
|
||||
<div class="tag-list">
|
||||
<span class="tag">Validation</span>
|
||||
<span class="tag">Teaching</span>
|
||||
</div>
|
||||
<h3>Validation demo</h3>
|
||||
<p>Test length, email, age, and text limits while keeping the page readable for explanation.</p>
|
||||
<div class="demo-links">
|
||||
<a class="link-btn" href="validation/form.jsp">Open validation</a>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="demo-card" data-keywords="upload multipart metadata file">
|
||||
<div class="tag-list">
|
||||
<span class="tag">Upload</span>
|
||||
<span class="tag">Safe demo</span>
|
||||
</div>
|
||||
<h3>Upload metadata demo</h3>
|
||||
<p>Capture file metadata through Struts form binding without writing anything to disk.</p>
|
||||
<div class="demo-links">
|
||||
<a class="link-btn" href="upload/index.jsp">Open upload</a>
|
||||
<a class="link-btn" href="demo/upload/index.jsp">Read guide</a>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="demo-card" data-keywords="ajax json api rest users">
|
||||
<div class="tag-list">
|
||||
<span class="tag">JSON</span>
|
||||
<span class="tag">API style</span>
|
||||
</div>
|
||||
<h3>AJAX and REST payloads</h3>
|
||||
<p>Show how Struts2 can return JSON from actions for AJAX examples and lightweight REST-style demos.</p>
|
||||
<div class="demo-links">
|
||||
<a class="link-btn" href="ajax">Action JSON</a>
|
||||
<a class="link-btn" href="api/users">REST JSON</a>
|
||||
<a class="link-btn" href="demo/ajax/index.jsp">Read guide</a>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="demo-card" data-keywords="model driven user bean object binding">
|
||||
<div class="tag-list">
|
||||
<span class="tag">Guide</span>
|
||||
<span class="tag">Patterns</span>
|
||||
</div>
|
||||
<h3>Model binding guide</h3>
|
||||
<p>Compare property-driven and model-driven input binding to explain how Struts builds action state.</p>
|
||||
<div class="demo-links">
|
||||
<a class="link-btn" href="demo/model/index.jsp">Open guide</a>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<div class="empty" id="emptyState" style="display: none; margin-top: 16px;">No demo matched the current search.</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<div class="eyebrow">Learning pipeline</div>
|
||||
<h2>Suggested order for the full demo</h2>
|
||||
<div class="pipeline">
|
||||
<div class="step">
|
||||
<strong>1. Start with hello</strong>
|
||||
<p>See the smallest action and result mapping first.</p>
|
||||
</div>
|
||||
<div class="step">
|
||||
<strong>2. Move to forms</strong>
|
||||
<p>Login and user intake show parameter binding in action.</p>
|
||||
</div>
|
||||
<div class="step">
|
||||
<strong>3. Validate inputs</strong>
|
||||
<p>Explain field errors, business rules, and success pages.</p>
|
||||
</div>
|
||||
<div class="step">
|
||||
<strong>4. Expand to JSON</strong>
|
||||
<p>Close the session with AJAX and REST-style payload demos.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
const cards = Array.from(document.querySelectorAll('.demo-card'));
|
||||
const emptyState = document.getElementById('emptyState');
|
||||
const trackerItems = Array.from(document.querySelectorAll('[data-track]'));
|
||||
const trackerKey = 'struts_demo_tracker';
|
||||
|
||||
function getTrackerState() {
|
||||
return JSON.parse(localStorage.getItem(trackerKey) || '{}');
|
||||
}
|
||||
|
||||
function renderTracker() {
|
||||
const state = getTrackerState();
|
||||
let completed = 0;
|
||||
trackerItems.forEach((item) => {
|
||||
item.checked = Boolean(state[item.dataset.track]);
|
||||
if (item.checked) {
|
||||
completed += 1;
|
||||
}
|
||||
});
|
||||
document.getElementById('completedLabs').textContent = completed;
|
||||
}
|
||||
|
||||
trackerItems.forEach((item) => {
|
||||
item.addEventListener('change', () => {
|
||||
const state = getTrackerState();
|
||||
state[item.dataset.track] = item.checked;
|
||||
localStorage.setItem(trackerKey, JSON.stringify(state));
|
||||
renderTracker();
|
||||
});
|
||||
});
|
||||
|
||||
searchInput.addEventListener('input', () => {
|
||||
const keyword = searchInput.value.trim().toLowerCase();
|
||||
let visible = 0;
|
||||
cards.forEach((card) => {
|
||||
const match = !keyword || card.dataset.keywords.includes(keyword);
|
||||
card.style.display = match ? 'block' : 'none';
|
||||
if (match) {
|
||||
visible += 1;
|
||||
}
|
||||
});
|
||||
emptyState.style.display = visible ? 'none' : 'block';
|
||||
});
|
||||
|
||||
renderTracker();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
117
web/upload/index.jsp
Normal file
117
web/upload/index.jsp
Normal file
@@ -0,0 +1,117 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="s" uri="/struts-tags" %>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Upload Demo - Struts2 Demo Lab</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
padding: 24px;
|
||||
font-family: "Aptos", "Segoe UI", "Microsoft YaHei", sans-serif;
|
||||
background: linear-gradient(135deg, #0ea5e9 0%, #38bdf8 55%, #dff4ff 100%);
|
||||
}
|
||||
.shell {
|
||||
max-width: 860px;
|
||||
margin: 0 auto;
|
||||
background: rgba(255,255,255,0.95);
|
||||
border-radius: 28px;
|
||||
padding: 28px;
|
||||
box-shadow: 0 24px 60px rgba(0,0,0,0.18);
|
||||
}
|
||||
.eyebrow {
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: #0284c7;
|
||||
font-weight: 800;
|
||||
}
|
||||
h1 { margin: 10px 0 12px; }
|
||||
p { margin: 0; color: #54687c; line-height: 1.85; }
|
||||
.note {
|
||||
margin-top: 16px;
|
||||
padding: 16px;
|
||||
border-radius: 18px;
|
||||
background: #eef8ff;
|
||||
border: 1px solid #d6e9f7;
|
||||
}
|
||||
.field { margin-top: 16px; }
|
||||
label { display: block; margin-bottom: 8px; font-weight: 700; color: #173652; }
|
||||
input[type="file"] {
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
border-radius: 16px;
|
||||
border: 1px dashed #5ab8f0;
|
||||
background: #f8fcff;
|
||||
}
|
||||
button {
|
||||
width: 100%;
|
||||
margin-top: 18px;
|
||||
padding: 14px;
|
||||
border: 0;
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(135deg, #0284c7, #38bdf8);
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
cursor: pointer;
|
||||
}
|
||||
.action-error {
|
||||
margin-top: 16px;
|
||||
padding: 14px;
|
||||
border-radius: 14px;
|
||||
background: #fff1f1;
|
||||
color: #b63a3a;
|
||||
border: 1px solid #f1c4c4;
|
||||
}
|
||||
.links { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 18px; }
|
||||
.link-btn {
|
||||
display: inline-flex;
|
||||
padding: 10px 14px;
|
||||
border-radius: 999px;
|
||||
background: #edf7ff;
|
||||
color: #0284c7;
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<div class="eyebrow">Upload demo</div>
|
||||
<h1>Capture file metadata without writing anything to disk</h1>
|
||||
<p>This safer upload sample focuses on how Struts binds multipart form fields. The action records metadata only, which makes the demo easier to run in constrained environments.</p>
|
||||
|
||||
<div class="note">
|
||||
<strong>What the action collects</strong>
|
||||
<p style="margin-top: 8px;">Primary filename, content type, and total selected file count.</p>
|
||||
</div>
|
||||
|
||||
<s:if test="hasActionErrors()">
|
||||
<div class="action-error"><s:actionerror/></div>
|
||||
</s:if>
|
||||
|
||||
<s:form action="upload" method="post" enctype="multipart/form-data" namespace="/">
|
||||
<div class="field">
|
||||
<label>Primary file</label>
|
||||
<s:file name="upload"/>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Extra files</label>
|
||||
<s:file name="uploads" multiple="multiple"/>
|
||||
</div>
|
||||
|
||||
<button type="submit">Submit upload metadata</button>
|
||||
</s:form>
|
||||
|
||||
<div class="links">
|
||||
<a class="link-btn" href="../index.jsp">Back to portal</a>
|
||||
<a class="link-btn" href="../demo/upload/index.jsp">Open upload guide</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
92
web/upload/success.jsp
Normal file
92
web/upload/success.jsp
Normal file
@@ -0,0 +1,92 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="s" uri="/struts-tags" %>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Upload Summary - Struts2 Demo Lab</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
font-family: "Aptos", "Segoe UI", "Microsoft YaHei", sans-serif;
|
||||
background: linear-gradient(135deg, #0ea5e9 0%, #38bdf8 55%, #dff4ff 100%);
|
||||
}
|
||||
.card {
|
||||
width: min(760px, 100%);
|
||||
background: rgba(255,255,255,0.95);
|
||||
border-radius: 28px;
|
||||
padding: 28px;
|
||||
box-shadow: 0 24px 60px rgba(0,0,0,0.18);
|
||||
}
|
||||
.eyebrow {
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: #0284c7;
|
||||
font-weight: 800;
|
||||
}
|
||||
h1 { margin: 10px 0 12px; }
|
||||
p { margin: 0; color: #53677b; line-height: 1.85; }
|
||||
.stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
.stat {
|
||||
padding: 16px;
|
||||
border-radius: 18px;
|
||||
background: #f5fbff;
|
||||
border: 1px solid #d8ebf8;
|
||||
}
|
||||
.stat span { display: block; font-size: 12px; color: #62809d; margin-bottom: 6px; }
|
||||
.stat strong { font-size: 20px; }
|
||||
.links { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 18px; }
|
||||
.link-btn {
|
||||
display: inline-flex;
|
||||
padding: 10px 14px;
|
||||
border-radius: 999px;
|
||||
background: #edf7ff;
|
||||
color: #0284c7;
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.stats { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<div class="eyebrow">Upload summary</div>
|
||||
<h1>Upload metadata captured successfully</h1>
|
||||
<p><s:property value="summary" default="The demo collected upload metadata without persisting files."/></p>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat">
|
||||
<span>Selected files</span>
|
||||
<strong><s:property value="fileCount" default="0"/></strong>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span>Primary filename</span>
|
||||
<strong><s:property value="uploadFileName" default="Not provided"/></strong>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span>Content type</span>
|
||||
<strong><s:property value="uploadContentType" default="Unknown"/></strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="links">
|
||||
<a class="link-btn" href="index.jsp">Try another upload</a>
|
||||
<a class="link-btn" href="../index.jsp">Back to portal</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
102
web/user/form.jsp
Normal file
102
web/user/form.jsp
Normal file
@@ -0,0 +1,102 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="s" uri="/struts-tags" %>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>User Form - Struts2 Demo Lab</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
padding: 24px;
|
||||
font-family: "Aptos", "Segoe UI", "Microsoft YaHei", sans-serif;
|
||||
background: linear-gradient(135deg, #f97316 0%, #fb923c 50%, #ffedd5 100%);
|
||||
}
|
||||
.shell {
|
||||
max-width: 820px;
|
||||
margin: 0 auto;
|
||||
background: rgba(255,255,255,0.95);
|
||||
border-radius: 28px;
|
||||
padding: 28px;
|
||||
box-shadow: 0 24px 60px rgba(0,0,0,0.18);
|
||||
}
|
||||
.eyebrow {
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: #ea580c;
|
||||
font-weight: 800;
|
||||
}
|
||||
h1 { margin: 10px 0 12px; }
|
||||
p { margin: 0; color: #6c5545; line-height: 1.85; }
|
||||
.field { margin-top: 16px; }
|
||||
label { display: block; margin-bottom: 8px; font-weight: 700; color: #4c3422; }
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 13px 14px;
|
||||
border-radius: 14px;
|
||||
border: 1px solid #ead8ca;
|
||||
font: inherit;
|
||||
}
|
||||
button {
|
||||
width: 100%;
|
||||
margin-top: 18px;
|
||||
padding: 14px;
|
||||
border: 0;
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(135deg, #ea580c, #fb923c);
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
cursor: pointer;
|
||||
}
|
||||
.error { color: #c2410c; font-size: 13px; margin-top: 6px; }
|
||||
.links { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 18px; }
|
||||
.link-btn {
|
||||
display: inline-flex;
|
||||
padding: 10px 14px;
|
||||
border-radius: 999px;
|
||||
background: #fff1e8;
|
||||
color: #c2410c;
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<div class="eyebrow">User action</div>
|
||||
<h1>Capture a user profile through Struts binding</h1>
|
||||
<p>This form now demonstrates a more realistic user intake flow with field errors and a structured success screen.</p>
|
||||
|
||||
<s:form action="submitUser" method="post" namespace="/">
|
||||
<div class="field">
|
||||
<label for="username">Username</label>
|
||||
<s:textfield id="username" name="username" placeholder="platform-owner"/>
|
||||
<div class="error"><s:fielderror fieldName="username"/></div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="email">Email</label>
|
||||
<s:textfield id="email" name="email" placeholder="platform@example.com"/>
|
||||
<div class="error"><s:fielderror fieldName="email"/></div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="phone">Phone</label>
|
||||
<s:textfield id="phone" name="phone" placeholder="13800000000"/>
|
||||
<div class="error"><s:fielderror fieldName="phone"/></div>
|
||||
</div>
|
||||
|
||||
<button type="submit">Submit profile</button>
|
||||
</s:form>
|
||||
|
||||
<div class="links">
|
||||
<a class="link-btn" href="../index.jsp">Back to portal</a>
|
||||
<a class="link-btn" href="login.jsp">Open login demo</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
176
web/user/login.jsp
Normal file
176
web/user/login.jsp
Normal file
@@ -0,0 +1,176 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="s" uri="/struts-tags" %>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Login Demo - Struts2 Demo Lab</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
font-family: "Aptos", "Segoe UI", "Microsoft YaHei", sans-serif;
|
||||
background: linear-gradient(135deg, #122c63 0%, #1464c7 52%, #4db5ff 100%);
|
||||
}
|
||||
.shell {
|
||||
width: min(1040px, 100%);
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 420px;
|
||||
gap: 18px;
|
||||
}
|
||||
.panel {
|
||||
background: rgba(255,255,255,0.95);
|
||||
border-radius: 28px;
|
||||
padding: 28px;
|
||||
box-shadow: 0 24px 60px rgba(0,0,0,0.22);
|
||||
}
|
||||
.eyebrow {
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: #1464c7;
|
||||
font-weight: 800;
|
||||
}
|
||||
h1, h2 { margin: 10px 0 12px; }
|
||||
p { margin: 0; color: #566a80; line-height: 1.85; }
|
||||
.note {
|
||||
margin-top: 18px;
|
||||
padding: 16px;
|
||||
border-radius: 18px;
|
||||
background: #edf5ff;
|
||||
border: 1px solid #d7e5f7;
|
||||
}
|
||||
.stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
.stat {
|
||||
padding: 14px;
|
||||
border-radius: 18px;
|
||||
border: 1px solid #d7e5f7;
|
||||
background: #f8fbff;
|
||||
}
|
||||
.stat strong { display: block; margin-bottom: 6px; }
|
||||
.field {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 700;
|
||||
color: #1f3650;
|
||||
}
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 13px 14px;
|
||||
border-radius: 14px;
|
||||
border: 1px solid #d6e1ed;
|
||||
font: inherit;
|
||||
}
|
||||
button {
|
||||
width: 100%;
|
||||
padding: 14px;
|
||||
border: 0;
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(135deg, #1464c7, #3d8ef6);
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
cursor: pointer;
|
||||
}
|
||||
.error {
|
||||
color: #c53d3d;
|
||||
font-size: 13px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
.action-error {
|
||||
margin: 0 0 16px;
|
||||
padding: 14px;
|
||||
border-radius: 14px;
|
||||
background: #fff1f1;
|
||||
color: #b63a3a;
|
||||
border: 1px solid #f1c4c4;
|
||||
}
|
||||
.links {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 18px;
|
||||
}
|
||||
.link-btn {
|
||||
display: inline-flex;
|
||||
padding: 10px 14px;
|
||||
border-radius: 999px;
|
||||
background: #edf5ff;
|
||||
color: #1464c7;
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
}
|
||||
@media (max-width: 860px) {
|
||||
.shell { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<section class="panel">
|
||||
<div class="eyebrow">Demo login</div>
|
||||
<h1>Login flow with validation and guided next steps</h1>
|
||||
<p>This page turns the original login example into a better teaching demo. It keeps validation close to the form, shows a reusable credentials note, and links to follow-up demos after success.</p>
|
||||
|
||||
<div class="note">
|
||||
<strong>Demo credentials</strong>
|
||||
<p style="margin-top: 8px;">Username: <code>admin</code><br/>Password: <code>123456</code></p>
|
||||
</div>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat">
|
||||
<strong>What this demonstrates</strong>
|
||||
<span>Action execution, field validation, action errors, and result routing.</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<strong>Suggested next route</strong>
|
||||
<span>After login, continue with the user form or validation demo.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="links">
|
||||
<a class="link-btn" href="../index.jsp">Back to portal</a>
|
||||
<a class="link-btn" href="../hello?name=Team">Run hello action</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="eyebrow">Sign in</div>
|
||||
<h2>Enter the demo account</h2>
|
||||
|
||||
<s:if test="hasActionErrors()">
|
||||
<div class="action-error"><s:actionerror/></div>
|
||||
</s:if>
|
||||
|
||||
<s:form action="login" method="post" namespace="/">
|
||||
<div class="field">
|
||||
<label for="username">Username</label>
|
||||
<s:textfield id="username" name="username" placeholder="admin"/>
|
||||
<div class="error"><s:fielderror fieldName="username"/></div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="password">Password</label>
|
||||
<s:password id="password" name="password" placeholder="123456" showPassword="true"/>
|
||||
<div class="error"><s:fielderror fieldName="password"/></div>
|
||||
</div>
|
||||
|
||||
<button type="submit">Continue to the dashboard</button>
|
||||
</s:form>
|
||||
</section>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
133
web/user/success.jsp
Normal file
133
web/user/success.jsp
Normal file
@@ -0,0 +1,133 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="s" uri="/struts-tags" %>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Success Dashboard - Struts2 Demo Lab</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
padding: 24px;
|
||||
font-family: "Aptos", "Segoe UI", "Microsoft YaHei", sans-serif;
|
||||
background: linear-gradient(135deg, #0d8f7c 0%, #31c48d 55%, #d4f7e7 100%);
|
||||
color: #123028;
|
||||
}
|
||||
.shell {
|
||||
max-width: 1080px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
}
|
||||
.card {
|
||||
background: rgba(255,255,255,0.94);
|
||||
border-radius: 28px;
|
||||
padding: 28px;
|
||||
box-shadow: 0 24px 60px rgba(0,0,0,0.18);
|
||||
}
|
||||
.eyebrow {
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: #0d8f7c;
|
||||
font-weight: 800;
|
||||
}
|
||||
h1, h2 { margin: 10px 0 12px; }
|
||||
p { margin: 0; color: #446056; line-height: 1.85; }
|
||||
.stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
.stat {
|
||||
padding: 16px;
|
||||
border-radius: 18px;
|
||||
border: 1px solid #d7eee6;
|
||||
background: #f7fcfa;
|
||||
}
|
||||
.stat span { display: block; color: #5f7f75; font-size: 12px; margin-bottom: 6px; }
|
||||
.stat strong { font-size: 22px; }
|
||||
.links {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 18px;
|
||||
}
|
||||
.link-btn {
|
||||
display: inline-flex;
|
||||
padding: 10px 14px;
|
||||
border-radius: 999px;
|
||||
background: #e8f8f2;
|
||||
color: #0d8f7c;
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
}
|
||||
@media (max-width: 760px) {
|
||||
.stats { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<section class="card">
|
||||
<div class="eyebrow">Result</div>
|
||||
<h1>Demo flow completed successfully</h1>
|
||||
<p>This page now acts as a shared success dashboard for multiple form-based actions. It makes the result screen more useful than a single success line.</p>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<div class="eyebrow">Current submission</div>
|
||||
<h2><s:property value="displayName != null ? displayName : username"/></h2>
|
||||
<p><s:property value="recommendation != null ? recommendation : profileStage"/></p>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat">
|
||||
<span>Username</span>
|
||||
<strong><s:property value="username" default="demo-user"/></strong>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span>Role or stage</span>
|
||||
<strong><s:property value="role != null ? role : profileStage"/></strong>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span>Timestamp</span>
|
||||
<strong><s:property value="loginTime != null ? loginTime : submittedAt"/></strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="links">
|
||||
<a class="link-btn" href="../index.jsp">Back to portal</a>
|
||||
<a class="link-btn" href="form.jsp">Open user form</a>
|
||||
<a class="link-btn" href="../validation/form.jsp">Run validation demo</a>
|
||||
<a class="link-btn" href="../upload/index.jsp">Open upload demo</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<div class="eyebrow">What happened</div>
|
||||
<h2>Map this result back to the Struts stack</h2>
|
||||
<div class="stats">
|
||||
<div class="stat">
|
||||
<span>Action mapping</span>
|
||||
<strong>login</strong>
|
||||
<p>The request matched the <code>login</code> action in <code>struts.xml</code>.</p>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span>Execute result</span>
|
||||
<strong>SUCCESS</strong>
|
||||
<p>The action returned the success result instead of redisplaying the form.</p>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span>Rendered view</span>
|
||||
<strong>/user/success.jsp</strong>
|
||||
<p>The result mapping selected this JSP to present the post-action summary.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
109
web/validation/form.jsp
Normal file
109
web/validation/form.jsp
Normal file
@@ -0,0 +1,109 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="s" uri="/struts-tags" %>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Validation Demo - Struts2 Demo Lab</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
padding: 24px;
|
||||
font-family: "Aptos", "Segoe UI", "Microsoft YaHei", sans-serif;
|
||||
background: linear-gradient(135deg, #db2777 0%, #f472b6 55%, #ffe4f1 100%);
|
||||
}
|
||||
.shell {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
background: rgba(255,255,255,0.95);
|
||||
border-radius: 28px;
|
||||
padding: 28px;
|
||||
box-shadow: 0 24px 60px rgba(0,0,0,0.18);
|
||||
}
|
||||
.eyebrow {
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: #db2777;
|
||||
font-weight: 800;
|
||||
}
|
||||
h1 { margin: 10px 0 12px; }
|
||||
p { margin: 0; color: #6e5565; line-height: 1.85; }
|
||||
.field { margin-top: 16px; }
|
||||
label { display: block; margin-bottom: 8px; font-weight: 700; color: #4a223b; }
|
||||
input, textarea {
|
||||
width: 100%;
|
||||
padding: 13px 14px;
|
||||
border-radius: 14px;
|
||||
border: 1px solid #ecd6e0;
|
||||
font: inherit;
|
||||
}
|
||||
textarea { resize: vertical; min-height: 120px; }
|
||||
button {
|
||||
width: 100%;
|
||||
margin-top: 18px;
|
||||
padding: 14px;
|
||||
border: 0;
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(135deg, #db2777, #f472b6);
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
cursor: pointer;
|
||||
}
|
||||
.error { color: #c0265d; font-size: 13px; margin-top: 6px; }
|
||||
.links { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 18px; }
|
||||
.link-btn {
|
||||
display: inline-flex;
|
||||
padding: 10px 14px;
|
||||
border-radius: 999px;
|
||||
background: #fff0f7;
|
||||
color: #db2777;
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<div class="eyebrow">Validation demo</div>
|
||||
<h1>Test field validation with realistic profile inputs</h1>
|
||||
<p>This form shows how Struts2 can keep validation close to the action while still rendering a friendlier teaching page.</p>
|
||||
|
||||
<s:form action="validate" method="post" namespace="/">
|
||||
<div class="field">
|
||||
<label for="username">Username (3-20 chars)</label>
|
||||
<s:textfield id="username" name="username" placeholder="release-manager"/>
|
||||
<div class="error"><s:fielderror fieldName="username"/></div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="email">Email</label>
|
||||
<s:textfield id="email" name="email" placeholder="release@example.com"/>
|
||||
<div class="error"><s:fielderror fieldName="email"/></div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="age">Age (18-60)</label>
|
||||
<s:textfield id="age" name="age" placeholder="30"/>
|
||||
<div class="error"><s:fielderror fieldName="age"/></div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="bio">Bio</label>
|
||||
<s:textarea id="bio" name="bio" placeholder="Describe the role, strengths, or current platform focus."/>
|
||||
<div class="error"><s:fielderror fieldName="bio"/></div>
|
||||
</div>
|
||||
|
||||
<button type="submit">Validate this profile</button>
|
||||
</s:form>
|
||||
|
||||
<div class="links">
|
||||
<a class="link-btn" href="../index.jsp">Back to portal</a>
|
||||
<a class="link-btn" href="../demo/ajax/index.jsp">Read the JSON guide</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
99
web/validation/success.jsp
Normal file
99
web/validation/success.jsp
Normal file
@@ -0,0 +1,99 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="s" uri="/struts-tags" %>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Validation Summary - Struts2 Demo Lab</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
font-family: "Aptos", "Segoe UI", "Microsoft YaHei", sans-serif;
|
||||
background: linear-gradient(135deg, #db2777 0%, #f472b6 55%, #ffe4f1 100%);
|
||||
}
|
||||
.card {
|
||||
width: min(820px, 100%);
|
||||
background: rgba(255,255,255,0.95);
|
||||
border-radius: 28px;
|
||||
padding: 28px;
|
||||
box-shadow: 0 24px 60px rgba(0,0,0,0.18);
|
||||
}
|
||||
.eyebrow {
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: #db2777;
|
||||
font-weight: 800;
|
||||
}
|
||||
h1 { margin: 10px 0 12px; }
|
||||
p { margin: 0; color: #6a5060; line-height: 1.85; }
|
||||
.stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
.stat {
|
||||
padding: 16px;
|
||||
border-radius: 18px;
|
||||
background: #fff5fa;
|
||||
border: 1px solid #f1d8e5;
|
||||
}
|
||||
.stat span { display: block; font-size: 12px; color: #8a697b; margin-bottom: 6px; }
|
||||
.stat strong { font-size: 18px; }
|
||||
.links { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 18px; }
|
||||
.link-btn {
|
||||
display: inline-flex;
|
||||
padding: 10px 14px;
|
||||
border-radius: 999px;
|
||||
background: #fff0f7;
|
||||
color: #db2777;
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.stats { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<div class="eyebrow">Validation summary</div>
|
||||
<h1>Profile validation passed</h1>
|
||||
<p>The action accepted the submitted fields and calculated a simple profile band for the demo.</p>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat"><span>Username</span><strong><s:property value="username"/></strong></div>
|
||||
<div class="stat"><span>Email</span><strong><s:property value="email"/></strong></div>
|
||||
<div class="stat"><span>Age</span><strong><s:property value="age"/></strong></div>
|
||||
<div class="stat"><span>Profile band</span><strong><s:property value="scoreBand"/></strong></div>
|
||||
<div class="stat"><span>Submitted at</span><strong><s:property value="submittedAt"/></strong></div>
|
||||
<div class="stat"><span>Bio</span><strong><s:property value="bio" default="No bio submitted."/></strong></div>
|
||||
</div>
|
||||
|
||||
<div class="links">
|
||||
<a class="link-btn" href="form.jsp">Try validation again</a>
|
||||
<a class="link-btn" href="../index.jsp">Back to portal</a>
|
||||
</div>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat">
|
||||
<span>Rule path</span>
|
||||
<strong>validate()</strong>
|
||||
The action accepted every field, so execution continued to the success result.
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span>Teaching focus</span>
|
||||
<strong>Input before logic</strong>
|
||||
Compare this page with the form error state to explain why validation runs before business output.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user