Erweiterungen

This commit is contained in:
2025-09-18 20:07:23 +02:00
parent 2f46ac3177
commit cf1bf1eaa0
5 changed files with 111 additions and 76 deletions

16
AGENTS.md Normal file
View File

@@ -0,0 +1,16 @@
# Repository Guidelines
## Project Structure & Module Organization
Backend sources sit in `src/main/java`, anchored by `de.assecutor.emulatorstation.Application`. Core business logic lives under `.../base/domain` and UI components in `.../base/ui/**`, while lightweight DTOs reside in `.../pojo`. Shared helpers use the top-level `util` package; keep new utilities there if they cross module boundaries. Vaadin client assets are in `src/main/frontend`; generated files stay in `generated/`, custom theme assets in `themes/default`. Internationalization bundles belong in `src/main/bundles`, and configuration files in `src/main/resources`.
## Build, Test, and Development Commands
Run the app locally with `./mvnw spring-boot:run` (serves the Vaadin UI with live reload). Use `./mvnw vaadin:prepare-frontend` after dependency upgrades to regenerate Flow metadata. Perform a full build with `./mvnw clean package`, or `./mvnw -Pproduction clean package` for an optimized artifact. Execute unit tests via `./mvnw test`. Apply code formatting using `./mvnw spotless:apply`.
## Coding Style & Naming Conventions
Java formatting is enforced by Spotless + the bundled `eclipse-formatter.xml`; commit only formatted code (4-space indentation, braces on the same line). Stick to the `de.assecutor.emulatorstation.*` package hierarchy and CamelCase class names with lowerCamelCase members. TypeScript and theme resources follow Prettier rules from `.prettierrc.json`; run the linter via Spotless before committing. Keep Spring components annotated explicitly (`@Service`, `@Route`, etc.) for clarity.
## Testing Guidelines
Adopt JUnit 5 for unit tests under `src/test/java`, mirroring the production package tree. Name pure unit tests `*Test` and integration tests `*IT`; the latter run through the `integration-test` Maven profile and may leverage Testcontainers. Add ArchUnit rules for architectural policies when touching `base` packages. Aim for coverage of business branches and Vaadin view logic; provide mock data for H2-backed repositories.
## Commit & Pull Request Guidelines
Recent history favors short one-word summaries; build on that by writing concise, descriptive titles (≤50 chars) followed by optional detail in the body. Reference Jira or GitHub issues with `#123` when relevant. For pull requests, include: purpose, testing notes (`./mvnw test` output), and UI screenshots whenever a view changes. Keep PRs focused per feature or bug to simplify review.

View File

@@ -15,11 +15,15 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticatio
import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import java.util.List; import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Route("login") @Route("login")
@AnonymousAllowed @AnonymousAllowed
public class LoginView extends VerticalLayout { public class LoginView extends VerticalLayout {
private static final Logger logger = LoggerFactory.getLogger(LoginView.class);
public LoginView() { public LoginView() {
setAlignItems(Alignment.CENTER); setAlignItems(Alignment.CENTER);
@@ -68,14 +72,20 @@ public class LoginView extends VerticalLayout {
var authentication = new UsernamePasswordAuthenticationToken(username, password, authorities); var authentication = new UsernamePasswordAuthenticationToken(username, password, authorities);
SecurityContextHolder.getContext().setAuthentication(authentication); SecurityContextHolder.getContext().setAuthentication(authentication);
var niederlassungInfo = Application.niederlassungen.get(niederlassung);
if (niederlassungInfo == null) {
Notification.show("Ausgewählte Niederlassung ist ungültig", 3000, Notification.Position.MIDDLE);
return;
}
getUI().ifPresent(ui -> { getUI().ifPresent(ui -> {
ui.getSession().setAttribute("user", username); ui.getSession().setAttribute("user", username);
ui.getSession().setAttribute("username", username); ui.getSession().setAttribute("username", username);
ui.getSession().setAttribute("niederlassung", niederlassung); ui.getSession().setAttribute("niederlassung", niederlassungInfo);
System.out.println("Login erfolgreich - Session-Daten gesetzt:"); logger.info("Login erfolgreich - Session-Daten gesetzt:");
System.out.println("Username: " + username); logger.info("Username: {}", username);
System.out.println("Niederlassung: " + niederlassung); logger.info("Niederlassung: {}", niederlassungInfo.name());
ui.navigate("main"); ui.navigate("main");
}); });
@@ -87,4 +97,4 @@ public class LoginView extends VerticalLayout {
MainLayout.instance.setDrawerOpened(false); MainLayout.instance.setDrawerOpened(false);
} }
} }
} }

View File

@@ -14,13 +14,13 @@ import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.BeforeEnterEvent; import com.vaadin.flow.router.BeforeEnterEvent;
import com.vaadin.flow.router.BeforeEnterObserver; import com.vaadin.flow.router.BeforeEnterObserver;
import com.vaadin.flow.server.VaadinService;
import com.vaadin.flow.component.UI; import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.html.Main; import com.vaadin.flow.component.html.Main;
import com.vaadin.flow.router.Route; import com.vaadin.flow.router.Route;
import com.vaadin.flow.server.auth.AnonymousAllowed; import com.vaadin.flow.server.auth.AnonymousAllowed;
import de.assecutor.emulatorstation.pojo.ExecResponse; import de.assecutor.emulatorstation.pojo.ExecResponse;
import de.assecutor.emulatorstation.pojo.NiederlassungInfo;
import de.assecutor.emulatorstation.Application; import de.assecutor.emulatorstation.Application;
import jakarta.annotation.security.PermitAll; import jakarta.annotation.security.PermitAll;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -40,7 +40,7 @@ public final class MainView extends Main implements BeforeEnterObserver
{ {
private static final Logger logger = LoggerFactory.getLogger(MainView.class); private static final Logger logger = LoggerFactory.getLogger(MainView.class);
private String username; private String username;
private String niederlassung; private NiederlassungInfo niederlassung;
private String server; private String server;
private final IFrame webView = new IFrame(); private final IFrame webView = new IFrame();
@@ -162,48 +162,60 @@ public final class MainView extends Main implements BeforeEnterObserver
.set("margin-bottom", "16px"); .set("margin-bottom", "16px");
webView.setSizeFull(); webView.setSizeFull();
webView.getElement().setAttribute("frameborder", "0"); ensureWebViewScrollbarsHidden();
webView.getElement().setAttribute("scrolling", "no");
webView.getElement().getStyle()
.set("overflow", "hidden")
.set("-webkit-overflow-scrolling", "touch")
.set("-ms-overflow-style", "none")
.set("scrollbar-width", "none");
// CSS für Webkit-Browser (Chrome, Safari)
webView.getElement().executeJs(
"this.style.setProperty('overflow', 'hidden', 'important');" +
"var style = document.createElement('style');" +
"style.textContent = 'iframe::-webkit-scrollbar { display: none !important; }';" +
"document.head.appendChild(style);"
);
emulatorContainer.add(webView); emulatorContainer.add(webView);
container.add(emulatorContainer); container.add(emulatorContainer);
} }
private void ensureWebViewScrollbarsHidden() {
webView.addClassName("mainview-webview");
webView.getElement().setAttribute("frameborder", "0");
webView.getElement().setAttribute("scrolling", "no");
webView.getElement().getStyle()
.set("border", "0")
.set("overflow", "hidden")
.set("-ms-overflow-style", "none")
.set("scrollbar-width", "none");
// Force-disable iframe scrollbars for all major engines
webView.getElement().executeJs(
"const frame = this;" +
"frame.style.setProperty('overflow', 'hidden', 'important');" +
"frame.style.setProperty('scrollbar-width', 'none', 'important');" +
"frame.style.setProperty('-ms-overflow-style', 'none', 'important');" +
"frame.classList.add('mainview-webview');" +
"if (!document.getElementById('mainview-webview-scroll-style')) {" +
" const style = document.createElement('style');" +
" style.id = 'mainview-webview-scroll-style';" +
" style.textContent = '.mainview-webview { overflow: hidden !important; scrollbar-width: none !important; -ms-overflow-style: none !important; } .mainview-webview::-webkit-scrollbar { display: none !important; width: 0 !important; height: 0 !important; }';" +
" document.head.appendChild(style);" +
"}"
);
}
private void loadSessionData() { private void loadSessionData() {
var currentUI = getUI(); var currentUI = getUI();
if (currentUI.isPresent()) { if (currentUI.isPresent()) {
var vaadinSession = currentUI.get().getSession(); var vaadinSession = currentUI.get().getSession();
username = (String) vaadinSession.getAttribute("username"); username = (String) vaadinSession.getAttribute("username");
niederlassung = (String) vaadinSession.getAttribute("niederlassung"); niederlassung = (NiederlassungInfo) vaadinSession.getAttribute("niederlassung");
server = (String) vaadinSession.getAttribute("server"); server = (String) vaadinSession.getAttribute("server");
} else { } else {
System.err.println("UI nicht verfügbar - kann Session-Daten nicht laden"); logger.error("UI nicht verfügbar - kann Session-Daten nicht laden");
} }
if (server == null) { if (server == null) {
server = "172.16.0.158"; server = "172.16.0.158";
} }
System.out.println("MainView Session-Daten geladen:"); logger.info("MainView Session-Daten geladen:");
System.out.println("Username: " + username); logger.info("Username: {}", username);
System.out.println("Niederlassung: " + niederlassung); logger.info("Niederlassung: {}", (niederlassung != null ? niederlassung.name() : "null"));
System.out.println("Server: " + server); logger.info("Server: {}", server);
if (niederlassung == null) { if (niederlassung == null) {
System.err.println("FEHLER: Niederlassung ist null! Benutzer muss sich neu anmelden."); logger.error("FEHLER: Niederlassung ist null! Benutzer muss sich neu anmelden.");
getUI().ifPresent(ui -> ui.navigate("login")); getUI().ifPresent(ui -> ui.navigate("login"));
} }
} }
@@ -264,19 +276,13 @@ public final class MainView extends Main implements BeforeEnterObserver
private void refreshWebView() { private void refreshWebView() {
if (niederlassung == null) { if (niederlassung == null) {
System.err.println("Niederlassung ist null - kann WebView nicht aktualisieren"); logger.error("Niederlassung ist null - kann WebView nicht aktualisieren");
return; return;
} }
var niederlassungInfo = Application.niederlassungen.get(niederlassung); var url = "https://sb-app.emu.assecutor.org" + niederlassung.urlExtension() + "?autoconnect=true";
if (niederlassungInfo == null) {
System.err.println("Niederlassungsinfo für '" + niederlassung + "' nicht gefunden");
return;
}
var url = "https://sb-app.emu.assecutor.org" + niederlassungInfo.urlExtension() + "?autoconnect=true"; logger.info("URL: {}", url);
System.out.println("URL: " + url);
webView.setSrc(url); webView.setSrc(url);
@@ -296,7 +302,7 @@ public final class MainView extends Main implements BeforeEnterObserver
// HTTP-Request erstellen // HTTP-Request erstellen
HttpRequest request = HttpRequest.newBuilder() HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://" + server + ":2375/containers/android-container-" + username + "/exec")) .uri(URI.create("http://" + server + ":2375/containers/android-container-" + niederlassung.name() + "/exec"))
.header("Content-Type", "application/json") .header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(jsonPayload)) .POST(HttpRequest.BodyPublishers.ofString(jsonPayload))
.build(); .build();
@@ -304,8 +310,8 @@ public final class MainView extends Main implements BeforeEnterObserver
// Anfrage senden und Antwort verarbeiten // Anfrage senden und Antwort verarbeiten
try { try {
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("HTTP-Response-Code: " + response.statusCode()); logger.info("HTTP-Response-Code: {}", response.statusCode());
System.out.println("Response-Body: " + response.body()); logger.info("Response-Body: {}", response.body());
var execId = ExecResponse.parse(response.body()); var execId = ExecResponse.parse(response.body());
@@ -329,15 +335,15 @@ public final class MainView extends Main implements BeforeEnterObserver
}"""; }""";
HttpRequest request = HttpRequest.newBuilder() HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://" + server + ":2375/containers/android-container-" + username + "/exec")) .uri(URI.create("http://" + server + ":2375/containers/android-container-" + niederlassung.name() + "/exec"))
.header("Content-Type", "application/json") .header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(jsonPayload)) .POST(HttpRequest.BodyPublishers.ofString(jsonPayload))
.build(); .build();
try { try {
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("HTTP-Response-Code: " + response.statusCode()); logger.info("HTTP-Response-Code: {}", response.statusCode());
System.out.println("Response-Body: " + response.body()); logger.info("Response-Body: {}", response.body());
var execId = ExecResponse.parse(response.body()); var execId = ExecResponse.parse(response.body());
@@ -358,8 +364,8 @@ public final class MainView extends Main implements BeforeEnterObserver
try { try {
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("HTTP-Response-Code: " + response.statusCode()); // Statuscode ausgeben logger.info("HTTP-Response-Code: {}", response.statusCode()); // Statuscode ausgeben
System.out.println("Response-Body: " + response.body()); // Antwort-Body ausgeben logger.info("Response-Body: {}", response.body()); // Antwort-Body ausgeben
return response.body(); return response.body();
} catch (Exception e) { } catch (Exception e) {
@@ -373,14 +379,14 @@ public final class MainView extends Main implements BeforeEnterObserver
HttpClient client = HttpClient.newHttpClient(); HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder() HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://" + server + ":2375/containers/android-container-" + username + "/start")) .uri(URI.create("http://" + server + ":2375/containers/android-container-" + niederlassung.name() + "/start"))
.POST(HttpRequest.BodyPublishers.noBody()) // Kein Body wird gesendet .POST(HttpRequest.BodyPublishers.noBody()) // Kein Body wird gesendet
.build(); .build();
try { try {
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("HTTP-Response-Code: " + response.statusCode()); // Statuscode ausgeben logger.info("HTTP-Response-Code: {}", response.statusCode()); // Statuscode ausgeben
System.out.println("Response-Body: " + response.body()); // Antwort-Body ausgeben logger.info("Response-Body: {}", response.body()); // Antwort-Body ausgeben
} catch (Exception e) { } catch (Exception e) {
logger.error("Fehler beim HTTP-Request", e); logger.error("Fehler beim HTTP-Request", e);
} }
@@ -401,14 +407,14 @@ public final class MainView extends Main implements BeforeEnterObserver
}"""; }""";
HttpRequest request = HttpRequest.newBuilder() HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://" + server + ":2375/containers/android-container-" + username + "/exec")) .uri(URI.create("http://" + server + ":2375/containers/android-container-" + niederlassung.name() + "/exec"))
.header("Content-Type", "application/json") .header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(jsonPayload)) .POST(HttpRequest.BodyPublishers.ofString(jsonPayload))
.build(); .build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("HTTP-Response-Code: " + response.statusCode()); logger.info("HTTP-Response-Code: {}", response.statusCode());
System.out.println("Response-Body: " + response.body()); logger.info("Response-Body: {}", response.body());
var execId = ExecResponse.parse(response.body()); var execId = ExecResponse.parse(response.body());
@@ -478,23 +484,17 @@ public final class MainView extends Main implements BeforeEnterObserver
"""; """;
if (niederlassung == null) { if (niederlassung == null) {
System.err.println("Niederlassung ist null - kann Container nicht erstellen"); logger.error("Niederlassung ist null - kann Container nicht erstellen");
return; return;
} }
var niederlassungInfo = Application.niederlassungen.get(niederlassung); jsonPayload = jsonPayload.formatted(niederlassung.port(), niederlassung.ip());
if (niederlassungInfo == null) {
System.err.println("Niederlassungsinfo für '" + niederlassung + "' nicht gefunden");
return;
}
jsonPayload = jsonPayload.formatted(niederlassungInfo.port(), niederlassungInfo.ip()); logger.info("JSON Payload: {}", jsonPayload);
System.out.println(jsonPayload);
// HTTP-Request erstellen // HTTP-Request erstellen
HttpRequest request = HttpRequest.newBuilder() HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://" + server + ":2375/containers/create?name=android-container-" + username)) .uri(URI.create("http://" + server + ":2375/containers/create?name=android-container-" + niederlassung.name()))
.header("Content-Type", "application/json") .header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(jsonPayload)) .POST(HttpRequest.BodyPublishers.ofString(jsonPayload))
.build(); .build();
@@ -502,8 +502,8 @@ public final class MainView extends Main implements BeforeEnterObserver
// Anfrage senden und Antwort verarbeiten // Anfrage senden und Antwort verarbeiten
try { try {
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("HTTP-Response-Code: " + response.statusCode()); logger.info("HTTP-Response-Code: {}", response.statusCode());
System.out.println("Response-Body: " + response.body()); logger.info("Response-Body: {}", response.body());
} catch (Exception e) { } catch (Exception e) {
logger.error("Fehler beim Erstellen des Containers", e); logger.error("Fehler beim Erstellen des Containers", e);
} }
@@ -513,14 +513,14 @@ public final class MainView extends Main implements BeforeEnterObserver
HttpClient client = HttpClient.newHttpClient(); HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder() HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://" + server + ":2375/containers/android-container-" + username + "/stop")) .uri(URI.create("http://" + server + ":2375/containers/android-container-" + niederlassung.name() + "/stop"))
.POST(HttpRequest.BodyPublishers.noBody()) // Kein Body wird gesendet .POST(HttpRequest.BodyPublishers.noBody()) // Kein Body wird gesendet
.build(); .build();
try { try {
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("HTTP-Response-Code: " + response.statusCode()); // Statuscode ausgeben logger.info("HTTP-Response-Code: {}", response.statusCode()); // Statuscode ausgeben
System.out.println("Response-Body: " + response.body()); // Antwort-Body ausgeben logger.info("Response-Body: {}", response.body()); // Antwort-Body ausgeben
} catch (Exception e) { } catch (Exception e) {
logger.error("Fehler beim HTTP-Request", e); logger.error("Fehler beim HTTP-Request", e);
} }
@@ -530,14 +530,14 @@ public final class MainView extends Main implements BeforeEnterObserver
HttpClient client = HttpClient.newHttpClient(); HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder() HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://" + server + ":2375/containers/android-container-" + username + "?force=true")) .uri(URI.create("http://" + server + ":2375/containers/android-container-" + niederlassung.name() + "?force=true"))
.DELETE() // Kein Body wird gesendet .DELETE() // Kein Body wird gesendet
.build(); .build();
try { try {
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("HTTP-Response-Code: " + response.statusCode()); // Statuscode ausgeben logger.info("HTTP-Response-Code: {}", response.statusCode()); // Statuscode ausgeben
System.out.println("Response-Body: " + response.body()); // Antwort-Body ausgeben logger.info("Response-Body: {}", response.body()); // Antwort-Body ausgeben
} catch (Exception e) { } catch (Exception e) {
logger.error("Fehler beim HTTP-Request", e); logger.error("Fehler beim HTTP-Request", e);
} }
@@ -548,6 +548,7 @@ public final class MainView extends Main implements BeforeEnterObserver
dialog.setHeaderTitle("Bitte warten"); dialog.setHeaderTitle("Bitte warten");
dialog.setModal(true); dialog.setModal(true);
dialog.setCloseOnOutsideClick(false); dialog.setCloseOnOutsideClick(false);
dialog.setCloseOnOutsideClick(false);
VerticalLayout dialogLayout = new VerticalLayout( VerticalLayout dialogLayout = new VerticalLayout(
new Paragraph(message) new Paragraph(message)
@@ -573,7 +574,7 @@ public final class MainView extends Main implements BeforeEnterObserver
// Session cleanup sofort // Session cleanup sofort
if (niederlassung != null) { if (niederlassung != null) {
Application.activeNiederlassungen.remove(niederlassung); Application.activeNiederlassungen.remove(niederlassung.name());
} }
if (username != null) { if (username != null) {
Application.activeUsers.remove(username); Application.activeUsers.remove(username);

View File

@@ -4,6 +4,7 @@ import com.vaadin.flow.server.SessionDestroyEvent;
import com.vaadin.flow.server.SessionDestroyListener; import com.vaadin.flow.server.SessionDestroyListener;
import com.vaadin.flow.spring.annotation.SpringComponent; import com.vaadin.flow.spring.annotation.SpringComponent;
import de.assecutor.emulatorstation.Application; import de.assecutor.emulatorstation.Application;
import de.assecutor.emulatorstation.pojo.NiederlassungInfo;
@SpringComponent @SpringComponent
public class SessionListener implements SessionDestroyListener { public class SessionListener implements SessionDestroyListener {
@@ -11,10 +12,10 @@ public class SessionListener implements SessionDestroyListener {
@Override @Override
public void sessionDestroy(SessionDestroyEvent event) { public void sessionDestroy(SessionDestroyEvent event) {
String username = (String) event.getSession().getAttribute("user"); String username = (String) event.getSession().getAttribute("user");
String niederlassung = (String) event.getSession().getAttribute("niederlassung"); NiederlassungInfo niederlassung = (NiederlassungInfo) event.getSession().getAttribute("niederlassung");
if (username != null && niederlassung != null) { if (username != null && niederlassung != null) {
Application.activeNiederlassungen.remove(niederlassung); Application.activeNiederlassungen.remove(niederlassung.name());
} }
} }
} }

View File

@@ -1,4 +1,11 @@
package de.assecutor.emulatorstation.pojo; package de.assecutor.emulatorstation.pojo;
public record NiederlassungInfo(String name, String ip, String port, String urlExtension) { import java.io.Serial;
} import java.io.Serializable;
public record NiederlassungInfo(String name, String ip, String port, String urlExtension)
implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
}