Erweiterungen

This commit is contained in:
2025-07-08 20:35:58 +02:00
parent aaf4e45f9d
commit 953b7782bd
25 changed files with 665 additions and 425 deletions

View File

@@ -1,24 +1,31 @@
package de.assecutor.emulatorstation; package de.assecutor.emulatorstation;
import com.vaadin.flow.component.page.AppShellConfigurator; import com.vaadin.flow.component.page.AppShellConfigurator;
import com.vaadin.flow.component.page.Push;
import com.vaadin.flow.theme.Theme; import com.vaadin.flow.theme.Theme;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean; import java.util.Map;
import java.time.Clock;
@SpringBootApplication @SpringBootApplication
@Push
@Theme("default") @Theme("default")
public class Application implements AppShellConfigurator { public class Application implements AppShellConfigurator {
public static final Map<String, String> users = Map.ofEntries(
@Bean Map.entry("admin", "ZY6X9X93Co8m"),
public Clock clock() { Map.entry("GFL", "GFL123"),
return Clock.systemDefaultZone(); // You can also use Clock.systemUTC() Map.entry("Berlin", "Berlin123"),
} Map.entry("Bremen", "Bremen123"),
Map.entry("Hamburg", "Hamburg123"),
Map.entry("Essen", "Essen123"),
Map.entry("Leipzig", "Leipzig123"),
Map.entry("Dresden", "Dresden123"),
Map.entry("Hannover", "Hannover123"),
Map.entry("Stuttgart", "Stuttgart123"),
Map.entry("FrankfurtAmMain", "FrankfurtAmMain123")
);
public static void main(String[] args) { public static void main(String[] args) {
SpringApplication.run(Application.class, args); SpringApplication.run(Application.class, args);
} }
} }

View File

@@ -1,44 +0,0 @@
package de.assecutor.emulatorstation.base.domain;
import jakarta.persistence.MappedSuperclass;
import org.jspecify.annotations.Nullable;
import org.springframework.data.util.ProxyUtils;
@MappedSuperclass
public abstract class AbstractEntity<ID> {
public abstract @Nullable ID getId();
@Override
public String toString() {
return "%s{id=%s}".formatted(getClass().getSimpleName(), getId());
}
@Override
public int hashCode() {
// Hashcode should never change during the lifetime of an object. Because of
// this we can't use getId() to calculate the hashcode. Unless you have sets
// with lots of entities in them, returning the same hashcode should not be a
// problem.
return ProxyUtils.getUserClass(getClass()).hashCode();
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
} else if (obj == this) {
return true;
}
var thisUserClass = ProxyUtils.getUserClass(getClass());
var otherUserClass = ProxyUtils.getUserClass(obj);
if (thisUserClass != otherUserClass) {
return false;
}
var id = getId();
return id != null && id.equals(((AbstractEntity<?>) obj).getId());
}
}

View File

@@ -1,7 +0,0 @@
/**
* This package contains reusable domain classes.
*/
@NullMarked
package de.assecutor.emulatorstation.base.domain;
import org.jspecify.annotations.NullMarked;

View File

@@ -0,0 +1,75 @@
package de.assecutor.emulatorstation.base.ui.view;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.html.Main;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.router.Menu;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.server.VaadinService;
import com.vaadin.flow.theme.lumo.LumoUtility;
import jakarta.annotation.security.PermitAll;
import util.PreferencesKeyValueStore;
import util.Util;
@Route("admin")
@PageTitle("Admin")
@Menu(order = 0, icon = "vaadin:clipboard-check", title = "Admin")
@PermitAll // When security is enabled, allow all authenticated users
public class AdminView extends Main {
final TextField serverPortTextView;
final Button saveBtn;
final Button logoutBtn;
final PreferencesKeyValueStore preferences = new PreferencesKeyValueStore();
public AdminView() {
addClassName(LumoUtility.Padding.MEDIUM);
var verticalLayout = new VerticalLayout();
add(verticalLayout);
serverPortTextView = new TextField();
serverPortTextView.setPlaceholder("Server");
serverPortTextView.setValue("172.16.0.158");
serverPortTextView.setMinWidth("20em");
verticalLayout.add(serverPortTextView);
var serverPort = preferences.get("server");
serverPort.ifPresent(serverPortTextView::setValue);
var horizontalLayout = new HorizontalLayout();
add(horizontalLayout);
saveBtn = new Button("Save", event -> savePreferences());
saveBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
horizontalLayout.add(saveBtn);
logoutBtn = new Button("Logout", event -> logout());
logoutBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
horizontalLayout.add(logoutBtn);
setSizeFull();
}
private void logout() {
VaadinService.getCurrentRequest().getWrappedSession().invalidate();
// Navigiere den Benutzer zur Login-Seite
UI.getCurrent().navigate("");
}
private void savePreferences() {
var preferences = new PreferencesKeyValueStore();
preferences.put("server", serverPortTextView.getValue());
Notification.show("Daten gespeichert!", 3000, Notification.Position.MIDDLE);
}
}

View File

@@ -0,0 +1,64 @@
package de.assecutor.emulatorstation.base.ui.view;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.html.Span;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.PasswordField;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.server.VaadinService;
import com.vaadin.flow.server.VaadinSession;
import de.assecutor.emulatorstation.Application;
@Route("")
public class LoginView extends VerticalLayout {
public LoginView() {
setAlignItems(Alignment.CENTER);
Span title = new Span("Anmelden");
title.getStyle().set("font-size", "24px").set("font-weight", "bold");
TextField usernameField = new TextField("Benutzername");
PasswordField passwordField = new PasswordField("Passwort");
Button loginButton = new Button("Login", event -> {
String username = usernameField.getValue();
String password = passwordField.getValue();
if (authenticate(username, password)) {
var currentSession = VaadinSession.getCurrent().getSession();
currentSession.setMaxInactiveInterval(60); // Timeout in Sekunden
currentSession.setAttribute("username", username);
VaadinService.getCurrentRequest().getWrappedSession().setAttribute("user", username);
Notification.show("Anmeldung erfolgreich!");
if (username.equals("admin")) {
getUI().ifPresent(ui -> ui.navigate(AdminView.class));
} else {
getUI().ifPresent(ui -> ui.navigate(MainView.class));
}
} else {
Notification.show("Anmeldung fehlgeschlagen. Bitte überprüfen Sie Ihre Daten!", 3000, Notification.Position.MIDDLE);
}
});
loginButton.getStyle().set("margin-top", "10px");
add(title, usernameField, passwordField, loginButton);
MainLayout.instance.setDrawerOpened(false);
}
// Einfache Authentifizierung (hier nur als Beispiel, später durch echte Logik ersetzen)
private boolean authenticate(String username, String password) {
if (Application.users.containsKey(username)) {
var userPassword = Application.users.get(username);
return userPassword.equals(password);
}
return false;
}
}

View File

@@ -1,6 +1,7 @@
package de.assecutor.emulatorstation.base.ui.view; package de.assecutor.emulatorstation.base.ui.view;
import com.vaadin.flow.component.Component; import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.applayout.AppLayout; import com.vaadin.flow.component.applayout.AppLayout;
import com.vaadin.flow.component.avatar.Avatar; import com.vaadin.flow.component.avatar.Avatar;
import com.vaadin.flow.component.avatar.AvatarVariant; import com.vaadin.flow.component.avatar.AvatarVariant;
@@ -14,6 +15,7 @@ import com.vaadin.flow.component.orderedlayout.Scroller;
import com.vaadin.flow.component.sidenav.SideNav; import com.vaadin.flow.component.sidenav.SideNav;
import com.vaadin.flow.component.sidenav.SideNavItem; import com.vaadin.flow.component.sidenav.SideNavItem;
import com.vaadin.flow.router.Layout; import com.vaadin.flow.router.Layout;
import com.vaadin.flow.server.VaadinSession;
import com.vaadin.flow.server.menu.MenuConfiguration; import com.vaadin.flow.server.menu.MenuConfiguration;
import com.vaadin.flow.server.menu.MenuEntry; import com.vaadin.flow.server.menu.MenuEntry;
import jakarta.annotation.security.PermitAll; import jakarta.annotation.security.PermitAll;
@@ -23,12 +25,41 @@ import static com.vaadin.flow.theme.lumo.LumoUtility.*;
@Layout @Layout
@PermitAll // When security is enabled, allow all authenticated users @PermitAll // When security is enabled, allow all authenticated users
public final class MainLayout extends AppLayout { public final class MainLayout extends AppLayout {
public static MainLayout instance = null;
MainLayout() { MainLayout() {
instance = this;
startPolling();
setPrimarySection(Section.DRAWER); setPrimarySection(Section.DRAWER);
addToDrawer(createHeader(), new Scroller(createSideNav()), createUserMenu()); addToDrawer(createHeader(), new Scroller(createSideNav()), createUserMenu());
setDrawerOpened(false);
} }
private void startPolling() {
UI ui = UI.getCurrent();
ui.setPollInterval(60000); // Poll-Intervall auf 60 Sekunden setzen
// Session expiration handling
ui.addPollListener(event -> {
if (!isUserSessionValid()) {
VaadinSession.getCurrent().getSession().invalidate(); // Session invalidieren
ui.access(() -> ui.navigate("login")); // Benutzer zur Login-Seite weiterleiten
}
});
}
private boolean isUserSessionValid() {
// Logik für Sitzungsprüfung, z.B. Timeout-Zeit überprüfen
return VaadinSession.getCurrent() != null
&& VaadinSession.getCurrent().getSession() != null
&& VaadinSession.getCurrent().getSession().getLastAccessedTime() + 300000
> System.currentTimeMillis(); // 300000 ms = 5 Minuten
}
private Div createHeader() { private Div createHeader() {
// TODO Replace with real application logo and name // TODO Replace with real application logo and name
var appLogo = VaadinIcon.CUBES.create(); var appLogo = VaadinIcon.CUBES.create();
@@ -76,5 +107,4 @@ public final class MainLayout extends AppLayout {
return userMenu; return userMenu;
} }
} }

View File

@@ -1,33 +1,423 @@
package de.assecutor.emulatorstation.base.ui.view; package de.assecutor.emulatorstation.base.ui.view;
import de.assecutor.emulatorstation.base.ui.component.ViewToolbar; import com.vaadin.flow.component.ClientCallable;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.dialog.Dialog;
import com.vaadin.flow.component.html.IFrame;
import com.vaadin.flow.component.html.Paragraph;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.BeforeEnterEvent;
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.Div;
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.theme.lumo.LumoUtility; import com.vaadin.flow.theme.lumo.LumoUtility;
import de.assecutor.emulatorstation.pojo.ExecResponse;
import jakarta.annotation.security.PermitAll; import jakarta.annotation.security.PermitAll;
import util.Util;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* This view shows up when a user navigates to the root ('/') of the application.
*/
@Route
@PermitAll // When security is enabled, allow all authenticated users @PermitAll // When security is enabled, allow all authenticated users
public final class MainView extends Main { @Route("main")
public final class MainView extends Main implements BeforeEnterObserver
{
private final String username;
private final String server;
// TODO Replace with your own main view. private final IFrame webView;
private UI ui;
private final ExecutorService executor = Executors.newSingleThreadExecutor();
MainView() { MainView() {
try {
} catch (Exception ex) {
showWaitDialog("Fehler!\n" + ex.toString());
}
var currentSession = VaadinService.getCurrentRequest().getWrappedSession();
username = Util.getSessionAttributeWithDefault(currentSession, "username", null);
server = Util.getSessionAttributeWithDefault(currentSession, "server", "172.16.0.158");
addClassName(LumoUtility.Padding.MEDIUM); addClassName(LumoUtility.Padding.MEDIUM);
add(new ViewToolbar("Main"));
add(new Div("Please select a view from the menu on the left.")); String jsCode = """
var inactivityTime = function () {
var time;
window.onload = resetTimer;
document.onmousemove = resetTimer;
document.onkeypress = resetTimer;
document.onclick = resetTimer;
document.onscroll = resetTimer;
function logout() {
$0.$server.logout(); // Server-Logout via RPC
}
function resetTimer() {
clearTimeout(time);
time = setTimeout(logout, 5 * 60 * 1000); // 5 Minute Inaktivität
}
};
inactivityTime();
""";
UI.getCurrent().getPage().executeJs(jsCode, this);
var horizontalLayout = new HorizontalLayout();
add(horizontalLayout);
Button startBtn = new Button("Start", event -> startup());
startBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
horizontalLayout.add(startBtn);
Button logoutBtn = new Button("Logout", event -> logout());
logoutBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
horizontalLayout.add(logoutBtn);
webView = new IFrame();
webView.setSizeFull(); // IFrame auf volle Größe setzen
webView.getElement().setAttribute("frameborder", "0"); // Optional: Rahmen entfernen
// Layout konfigurieren
setSizeFull(); // Vertikales Layout auf volle Größe setzen
add(webView);
addAttachListener(event -> {
ui = event.getUI();
});
} }
/** @Override
* Navigates to the main view. public void beforeEnter(BeforeEnterEvent beforeEnterEvent) {
*/ String user = (String) VaadinService.getCurrentRequest().getWrappedSession().getAttribute("user");
public static void showMainView() { if (user == null) {
UI.getCurrent().navigate(MainView.class); beforeEnterEvent.rerouteTo("login");
}
}
private void startup() {
UI ui = UI.getCurrent();
var dialog = showWaitDialog("Der Emulator wird gestartet, bitte warten...");
executor.submit(() -> {
try {
shutdown();
createContainer();
startContainer();
waitContainerStart();
downloadApp();
installApp();
} catch (Exception ex) {
// Logging, Fehlerbehandlung …
} finally {
// 4) UI-update sicher aus Nicht-UI-Thread heraus
ui.access(() -> {
dialog.close();
refreshWebView();
});
}
});
}
private void shutdown() {
stopContainer();
deleteContainer();
}
private void refreshWebView() {
webView.setSrc("http://" + server + ":6080/?autoconnect=true");
webView.reload();
}
private void downloadApp() {
try {
HttpClient client = HttpClient.newHttpClient();
String jsonPayload = """
{
"Cmd": ["curl", "-O", "https://www.appcreation.de/download/sb.apk"],
"AttachStdout": true,
"AttachStderr": true
}""";
// HTTP-Request erstellen
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://" + server + ":2375/containers/android-container-" + username + "/exec"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(jsonPayload))
.build();
// Anfrage senden und Antwort verarbeiten
try {
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("HTTP-Response-Code: " + response.statusCode());
System.out.println("Response-Body: " + response.body());
var execId = ExecResponse.parse(response.body());
exec(execId);
} catch (Exception e) {
e.printStackTrace();
}
} catch (Exception e) {
e.printStackTrace();
}
}
private void installApp() {
HttpClient client = HttpClient.newHttpClient();
String jsonPayload = """
{
"Cmd": ["adb", "install", "sb.apk"],
"AttachStdout": true,
"AttachStderr": true
}""";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://" + server + ":2375/containers/android-container-" + username + "/exec"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(jsonPayload))
.build();
try {
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("HTTP-Response-Code: " + response.statusCode());
System.out.println("Response-Body: " + response.body());
var execId = ExecResponse.parse(response.body());
exec(execId);
} catch (Exception e) {
e.printStackTrace();
}
}
private String exec(String execId) {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://" + server + ":2375/exec/" + execId + "/start"))
.POST(HttpRequest.BodyPublishers.noBody())
.version(HttpClient.Version.HTTP_1_1)
.build();
try {
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("HTTP-Response-Code: " + response.statusCode()); // Statuscode ausgeben
System.out.println("Response-Body: " + response.body()); // Antwort-Body ausgeben
return response.body();
} catch (Exception e) {
e.printStackTrace(); // Fehler behandeln
}
return null;
}
private void startContainer() {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://" + server + ":2375/containers/android-container-" + username + "/start"))
.POST(HttpRequest.BodyPublishers.noBody()) // Kein Body wird gesendet
.build();
try {
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("HTTP-Response-Code: " + response.statusCode()); // Statuscode ausgeben
System.out.println("Response-Body: " + response.body()); // Antwort-Body ausgeben
} catch (Exception e) {
e.printStackTrace(); // Fehler behandeln
}
}
private void waitContainerStart() {
boolean endLoop = false;
do {
try {
HttpClient client = HttpClient.newHttpClient();
String jsonPayload = """
{
"Cmd": ["cat", "device_status"],
"AttachStdout": true,
"AttachStderr": true
}""";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://" + server + ":2375/containers/android-container-" + username + "/exec"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(jsonPayload))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("HTTP-Response-Code: " + response.statusCode());
System.out.println("Response-Body: " + response.body());
var execId = ExecResponse.parse(response.body());
var execResponse = exec(execId);
if (execResponse != null) {
var trimmedResponse = execResponse.trim();
if (trimmedResponse.equals("READY")) {
endLoop = true;
} else {
Thread.sleep(1000);
}
}
} catch (Exception e) {
e.printStackTrace();
}
} while (!endLoop);
}
private void createContainer() {
HttpClient client = HttpClient.newHttpClient();
// JSON-Payload für die Anfrage
String jsonPayload = """
{
"Image": "budtmo/docker-android:emulator_11.0",
"Env": [
"EMULATOR_DEVICE=Samsung Galaxy S10",
"WEB_VNC=true"
],
"HostConfig": {
"PortBindings": {
"6080/tcp": [
{ "HostPort": "6080" }
]
},
"Devices": [
{
"PathOnHost": "/dev/kvm",
"PathInContainer": "/dev/kvm",
"CgroupPermissions": "rwm"
}
]
},
"ExposedPorts": {
"6080/tcp": {}
}
}
""";
// HTTP-Request erstellen
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://" + server + ":2375/containers/create?name=android-container-" + username))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(jsonPayload))
.build();
// Anfrage senden und Antwort verarbeiten
try {
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("HTTP-Response-Code: " + response.statusCode());
System.out.println("Response-Body: " + response.body());
} catch (Exception e) {
e.printStackTrace();
}
}
private void stopContainer() {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://" + server + ":2375/containers/android-container-" + username + "/stop"))
.POST(HttpRequest.BodyPublishers.noBody()) // Kein Body wird gesendet
.build();
try {
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("HTTP-Response-Code: " + response.statusCode()); // Statuscode ausgeben
System.out.println("Response-Body: " + response.body()); // Antwort-Body ausgeben
} catch (Exception e) {
e.printStackTrace(); // Fehler behandeln
}
}
private void deleteContainer() {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://" + server + ":2375/containers/android-container-" + username + "?force=true"))
.DELETE() // Kein Body wird gesendet
.build();
try {
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("HTTP-Response-Code: " + response.statusCode()); // Statuscode ausgeben
System.out.println("Response-Body: " + response.body()); // Antwort-Body ausgeben
} catch (Exception e) {
e.printStackTrace(); // Fehler behandeln
}
}
private Dialog showWaitDialog(String message) {
Dialog dialog = new Dialog();
dialog.setHeaderTitle("Bitte warten");
dialog.setModal(true);
dialog.setCloseOnOutsideClick(false);
VerticalLayout dialogLayout = new VerticalLayout(
new Paragraph(message)
);
dialog.add(dialogLayout);
dialog.open();
return dialog;
}
@ClientCallable
public void logout() {
UI ui = UI.getCurrent();
var vaadinSession = VaadinService.getCurrentRequest().getWrappedSession();
var dialog = showWaitDialog("Sie werden abgemeldet. Bitte warten...");
dialog.open();
executor.submit(() -> {
try {
shutdown();
} catch (Exception ex) {
// Logging, Fehlerbehandlung …
} finally {
// 4) UI-update sicher aus Nicht-UI-Thread heraus
ui.access(() -> {
dialog.close();
vaadinSession.invalidate();
// Navigiere den Benutzer zur Login-Seite
ui.navigate("");
});
}
});
} }
} }

View File

@@ -1,7 +0,0 @@
/**
* This package contains reusable or cross-cutting view-related classes.
*/
@NullMarked
package de.assecutor.emulatorstation.base.ui.view;
import org.jspecify.annotations.NullMarked;

View File

@@ -0,0 +1,21 @@
package de.assecutor.emulatorstation.pojo;
import com.fasterxml.jackson.annotation.JsonKey;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public class ExecResponse {
@com.fasterxml.jackson.annotation.JsonProperty("Id")
private String id;
public static String parse(String json) throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
var value = objectMapper.readValue(json, ExecResponse.class);
return value.getId();
}
// Getter und Setter
public String getId() { return id; }
public void setId(String id) { this.id = id; }
}

View File

@@ -1,61 +0,0 @@
package de.assecutor.emulatorstation.taskmanagement.domain;
import de.assecutor.emulatorstation.base.domain.AbstractEntity;
import jakarta.persistence.*;
import jakarta.validation.constraints.Size;
import org.jspecify.annotations.Nullable;
import java.time.Instant;
import java.time.LocalDate;
@Entity
@Table(name = "task")
public class Task extends AbstractEntity<Long> {
public static final int DESCRIPTION_MAX_LENGTH = 255;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "task_id")
private Long id;
@Column(name = "description", nullable = false, length = DESCRIPTION_MAX_LENGTH)
@Size(max = DESCRIPTION_MAX_LENGTH)
private String description;
@Column(name = "creation_date", nullable = false)
private Instant creationDate;
@Column(name = "due_date")
@Nullable
private LocalDate dueDate;
@Override
public @Nullable Long getId() {
return id;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public Instant getCreationDate() {
return creationDate;
}
public void setCreationDate(Instant creationDate) {
this.creationDate = creationDate;
}
public @Nullable LocalDate getDueDate() {
return dueDate;
}
public void setDueDate(@Nullable LocalDate dueDate) {
this.dueDate = dueDate;
}
}

View File

@@ -1,12 +0,0 @@
package de.assecutor.emulatorstation.taskmanagement.domain;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
public interface TaskRepository extends JpaRepository<Task, Long>, JpaSpecificationExecutor<Task> {
// If you don't need a total row count, Slice is better than Page.
Slice<Task> findAllBy(Pageable pageable);
}

View File

@@ -1,14 +0,0 @@
/**
* This package contains the domain model of the Task Management sample feature.
* <p>
* You can add as many domain model artifacts (such as entities, value objects, repositories, domain events, and domain
* services) to this package, as long as they belong to the same feature.
* </p>
* <p>
* If you have domain classes that are re-usable across multiple features, add them to the {@code base.domain} package.
* </p>
*/
@NullMarked
package de.assecutor.emulatorstation.taskmanagement.domain;
import org.jspecify.annotations.NullMarked;

View File

@@ -1,15 +0,0 @@
/**
* This is a feature package for the Task Management sample feature. Its purpose is to demonstrate how you typically
* structure Vaadin business applications, and how the different building blocks interact.
* <p>
* A feature package represents a self-contained unit of functionality, including UI components, business logic, and
* data access. It could be a subdomain or bounded context (e.g., "Billing"), a specific use case (e.g., "User
* Registration"), or even a complex UI view (e.g., "Dashboard").
* </p>
* <p>
* If your application is very small, you may not need dedicated feature packages. In that case, move the subpackages
* directly to the application package.
* </p>
*/
package de.assecutor.emulatorstation.taskmanagement;
// TODO Remove this package once you have added real features

View File

@@ -1,43 +0,0 @@
package de.assecutor.emulatorstation.taskmanagement.service;
import de.assecutor.emulatorstation.taskmanagement.domain.Task;
import de.assecutor.emulatorstation.taskmanagement.domain.TaskRepository;
import org.jspecify.annotations.Nullable;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.time.Clock;
import java.time.LocalDate;
import java.util.List;
@Service
@Transactional(propagation = Propagation.REQUIRES_NEW)
public class TaskService {
private final TaskRepository taskRepository;
private final Clock clock;
TaskService(TaskRepository taskRepository, Clock clock) {
this.taskRepository = taskRepository;
this.clock = clock;
}
public void createTask(String description, @Nullable LocalDate dueDate) {
if ("fail".equals(description)) {
throw new RuntimeException("This is for testing the error handler");
}
var task = new Task();
task.setDescription(description);
task.setCreationDate(clock.instant());
task.setDueDate(dueDate);
taskRepository.saveAndFlush(task);
}
public List<Task> list(Pageable pageable) {
return taskRepository.findAllBy(pageable).toList();
}
}

View File

@@ -1,11 +0,0 @@
/**
* This package contains the application services of the Task Management sample feature.
* <p>
* You can add as many application services as you want to this package, as long as they belong to the same feature. If
* any of them use Data Transfer Objects (DTO), you can add them here as well.
* </p>
*/
@NullMarked
package de.assecutor.emulatorstation.taskmanagement.service;
import org.jspecify.annotations.NullMarked;

View File

@@ -1,12 +1,9 @@
package de.assecutor.emulatorstation.taskmanagement.ui.view; package de.assecutor.emulatorstation.taskmanagement.ui.view;
import de.assecutor.emulatorstation.base.ui.component.ViewToolbar; import de.assecutor.emulatorstation.base.ui.component.ViewToolbar;
import de.assecutor.emulatorstation.taskmanagement.domain.Task;
import de.assecutor.emulatorstation.taskmanagement.service.TaskService;
import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant; import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.datepicker.DatePicker; import com.vaadin.flow.component.datepicker.DatePicker;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.html.Main; import com.vaadin.flow.component.html.Main;
import com.vaadin.flow.component.notification.Notification; import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.notification.NotificationVariant; import com.vaadin.flow.component.notification.NotificationVariant;
@@ -18,32 +15,20 @@ import com.vaadin.flow.theme.lumo.LumoUtility;
import jakarta.annotation.security.PermitAll; import jakarta.annotation.security.PermitAll;
import java.time.Clock; import java.time.Clock;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Optional;
import static com.vaadin.flow.spring.data.VaadinSpringDataHelpers.toSpringPageRequest;
@Route("task-list") @Route("task-list")
@PageTitle("Task List") @PageTitle("Task List")
@Menu(order = 0, icon = "vaadin:clipboard-check", title = "Task List") @Menu(order = 0, icon = "vaadin:clipboard-check", title = "Task List")
@PermitAll // When security is enabled, allow all authenticated users @PermitAll // When security is enabled, allow all authenticated users
public class TaskListView extends Main { public class TaskListView extends Main {
private final TaskService taskService;
final TextField description; final TextField description;
final DatePicker dueDate; final DatePicker dueDate;
final Button createBtn; final Button createBtn;
final Grid<Task> taskGrid;
public TaskListView(TaskService taskService, Clock clock) {
this.taskService = taskService;
public TaskListView() {
description = new TextField(); description = new TextField();
description.setPlaceholder("What do you want to do?"); description.setPlaceholder("What do you want to do?");
description.setAriaLabel("Task description"); description.setAriaLabel("Task description");
description.setMaxLength(Task.DESCRIPTION_MAX_LENGTH);
description.setMinWidth("20em"); description.setMinWidth("20em");
dueDate = new DatePicker(); dueDate = new DatePicker();
@@ -53,33 +38,13 @@ public class TaskListView extends Main {
createBtn = new Button("Create", event -> createTask()); createBtn = new Button("Create", event -> createTask());
createBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY); createBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
var dateTimeFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).withZone(clock.getZone())
.withLocale(getLocale());
var dateFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).withLocale(getLocale());
taskGrid = new Grid<>();
taskGrid.setItems(query -> taskService.list(toSpringPageRequest(query)).stream());
taskGrid.addColumn(Task::getDescription).setHeader("Description");
taskGrid.addColumn(task -> Optional.ofNullable(task.getDueDate()).map(dateFormatter::format).orElse("Never"))
.setHeader("Due Date");
taskGrid.addColumn(task -> dateTimeFormatter.format(task.getCreationDate())).setHeader("Creation Date");
taskGrid.setSizeFull();
setSizeFull(); setSizeFull();
addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, LumoUtility.FlexDirection.COLUMN, addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, LumoUtility.FlexDirection.COLUMN,
LumoUtility.Padding.MEDIUM, LumoUtility.Gap.SMALL); LumoUtility.Padding.MEDIUM, LumoUtility.Gap.SMALL);
add(new ViewToolbar("Task List", ViewToolbar.group(description, dueDate, createBtn))); add(new ViewToolbar("Task List", ViewToolbar.group(description, dueDate, createBtn)));
add(taskGrid);
} }
private void createTask() { private void createTask() {
taskService.createTask(description.getValue(), dueDate.getValue());
taskGrid.getDataProvider().refreshAll();
description.clear();
dueDate.clear();
Notification.show("Task added", 3000, Notification.Position.BOTTOM_END)
.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
} }
} }

View File

@@ -1,14 +0,0 @@
/**
* This package contains the views of the Task Management sample feature.
* <p>
* You can add as many views as you want to this package, as long as they belong to the same feature.
* </p>
* <p>
* For smaller UI-components, consider creating a separate {@code ui.component} package. If the components are re-usable
* across multiple features, add them to the {@code base.ui.component} package.
* </p>
*/
@NullMarked
package de.assecutor.emulatorstation.taskmanagement.ui.view;
import org.jspecify.annotations.NullMarked;

View File

@@ -0,0 +1,20 @@
package util;
import org.springframework.stereotype.Service;
import java.util.Optional;
import java.util.prefs.Preferences;
@Service
public class PreferencesKeyValueStore {
private final Preferences prefs = Preferences.userRoot().node("emulatorstation");
public void put(String key, String value) {
prefs.put(key, value);
}
public Optional<String> get(String key) {
return Optional.ofNullable(prefs.get(key, null));
}
}

View File

@@ -0,0 +1,19 @@
package util;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.applayout.AppLayout;
import com.vaadin.flow.server.VaadinSession;
import com.vaadin.flow.server.WrappedSession;
public class Util {
public static String getSessionAttributeWithDefault(WrappedSession currentSession, String key, String defaultValue) {
var result = defaultValue;
try {
result = currentSession.getAttribute(key).toString();
} catch (Exception e) {}
return result;
}
}

View File

@@ -0,0 +1,10 @@
import com.vaadin.flow.server.ServiceInitEvent;
import com.vaadin.flow.server.VaadinServiceInitListener;
public class MyVaadinServiceInitListener implements VaadinServiceInitListener {
@Override
public void serviceInit(ServiceInitEvent event) {
// Setze das global für den Service gültige Heartbeat-Interval
event.getSource().setHeartbeatInterval(300); // 5 Minuten (300 Sekunden)
}
}

View File

@@ -12,3 +12,6 @@ spring.jpa.open-in-view=false
# Initialize the JPA Entity Manager before considering data.sql so that the EM can create the schema and data.sql contain data # Initialize the JPA Entity Manager before considering data.sql so that the EM can create the schema and data.sql contain data
spring.jpa.defer-datasource-initialization = true spring.jpa.defer-datasource-initialization = true
server.servlet.session.timeout=300s
vaadin.heartbeatInterval=300

View File

@@ -1,55 +0,0 @@
package de.assecutor.emulatorstation;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import org.junit.jupiter.api.Test;
import org.springframework.data.repository.Repository;
import org.springframework.transaction.annotation.Transactional;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
import static com.tngtech.archunit.library.dependencies.SlicesRuleDefinition.slices;
class ArchitectureTest {
static final String BASE_PACKAGE = "de.assecutor.emulatorstation";
private final JavaClasses importedClasses = new ClassFileImporter().importPackages(BASE_PACKAGE);
// TODO Add your own rules and remove those that don't apply to your project
@Test
void domain_model_should_not_depend_on_application_services() {
noClasses().that().resideInAPackage(BASE_PACKAGE + "..domain..").should().dependOnClassesThat()
.resideInAPackage(BASE_PACKAGE + "..service..").check(importedClasses);
}
@Test
void domain_model_should_not_depend_on_the_user_interface() {
noClasses().that().resideInAPackage(BASE_PACKAGE + "..domain..").should().dependOnClassesThat()
.resideInAnyPackage(BASE_PACKAGE + "..ui..").check(importedClasses);
}
@Test
void repositories_should_only_be_used_by_application_services_and_other_domain_classes() {
classes().that().areAssignableTo(Repository.class).should().onlyHaveDependentClassesThat()
.resideInAnyPackage(BASE_PACKAGE + "..domain..", BASE_PACKAGE + "..service..").check(importedClasses);
}
@Test
void repositories_should_only_be_accessed_by_transactional_classes() {
classes().that().areAssignableTo(Repository.class).should().onlyBeAccessed().byClassesThat()
.areAnnotatedWith(Transactional.class).check(importedClasses);
}
@Test
void application_services_should_not_depend_on_the_user_interface() {
noClasses().that().resideInAPackage(BASE_PACKAGE + "..service..").should().dependOnClassesThat()
.resideInAnyPackage(BASE_PACKAGE + "..ui..").check(importedClasses);
}
@Test
void there_should_not_be_circular_dependencies_between_feature_packages() {
slices().matching(BASE_PACKAGE + ".(*)..").should().beFreeOfCycles().check(importedClasses);
}
}

View File

@@ -1,14 +0,0 @@
package de.assecutor.emulatorstation;
import org.springframework.boot.SpringApplication;
/**
* Run this application class to start your application locally, using Testcontainers for all external services. You
* have to configure the containers in {@link TestcontainersConfiguration}.
*/
public class TestApplication {
public static void main(String[] args) {
SpringApplication.from(Application::main).with(TestcontainersConfiguration.class).run(args);
}
}

View File

@@ -1,10 +0,0 @@
package de.assecutor.emulatorstation;
import org.springframework.boot.test.context.TestConfiguration;
@TestConfiguration(proxyBeanMethods = false)
public class TestcontainersConfiguration {
// TODO Configure your Testcontainers here.
// See https://docs.spring.io/spring-boot/reference/testing/testcontainers.html for details.
}

View File

@@ -1,57 +0,0 @@
package de.assecutor.emulatorstation.taskmanagement.service;
import de.assecutor.emulatorstation.TestcontainersConfiguration;
import de.assecutor.emulatorstation.taskmanagement.domain.Task;
import de.assecutor.emulatorstation.taskmanagement.domain.TaskRepository;
import jakarta.validation.ValidationException;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.springframework.data.domain.PageRequest;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.time.Clock;
import java.time.LocalDate;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@Import(TestcontainersConfiguration.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@Transactional(propagation = Propagation.NOT_SUPPORTED)
class TaskServiceIT {
@Autowired
TaskService taskService;
@Autowired
TaskRepository taskRepository;
@Autowired
Clock clock;
@AfterEach
void cleanUp() {
taskRepository.deleteAll();
}
@Test
public void tasks_are_stored_in_the_database_with_the_current_timestamp() {
var now = clock.instant();
var due = LocalDate.of(2025, 2, 7);
taskService.createTask("Do this", due);
assertThat(taskService.list(PageRequest.ofSize(1))).singleElement()
.matches(task -> task.getDescription().equals("Do this") && due.equals(task.getDueDate())
&& task.getCreationDate().isAfter(now));
}
@Test
public void tasks_are_validated_before_they_are_stored() {
assertThatThrownBy(() -> taskService.createTask("X".repeat(Task.DESCRIPTION_MAX_LENGTH + 1), null))
.isInstanceOf(ValidationException.class);
assertThat(taskRepository.count()).isEqualTo(0);
}
}