CUPRINS
1) Prezentare generală a aplicației....................................2
2) Tipul de design și stilurile arhitecturale.........................3
3) O primă privire a aplicației............................................4
4) Securitatea aplicației...................................................7
5) Prezentarea frontend-ului...........................................13
6) Principiul fallback......................................................19
7) Notificări – Toast Notification......................................23
8) Caching.....................................................................28
9) Modul de implementare al căutării și sortării datelor....33
10) Studiu de caz – Prezentare SupplierOrders................35
1
ChainOptim
ChainOptim este un manager și optimizator de lanț de aprovizionare, format
dintr-un backend Spring Boot și un frontend realizat dintr-o aplicație desktop în JavaFX.
Este conceput ca un serviciu general care poate satisface nevoile companiilor dintr-o
varietate de sectoare, oferind perspective detaliate asupra lanțului lor de aprovizionare,
de la furnizori la producție și până la clienți.
Caracteristici
Organizații:
ChainOptim utilizează o arhitectură multi-chiriaș care minimizează costurile
pentru clienți (menținând în același timp o securitate solidă) și facilitează comunicarea
interorganizațională.
Produse și componente:
Produsele sunt bunurile fabricate de o companie, în timp ce componentele sunt
bunurile generale necesare în procesul de fabricație. Fiecare produs poate avea un flux
de producție care configurează modul în care componentele sunt asamblate în
produse.
Fabrici și depozite:
Sunt locațiile unei organizații pentru producția și stocarea bunurilor.
Configurarea etapelor de producție ale unei fabrici permite software-ului să ofere
informații valoroase despre operațiunile acesteia, inclusiv alocarea resurselor, găsirea
de soluții pentru deficitul de resurse și evaluarea performanței în timp.
Furnizori și clienți:
ChainOptim permite înregistrarea furnizorilor și clienților, urmărirea comenzilor
și livrărilor și răspunde în timp real la întreruperi imprevizibile, din ce în ce mai frecvente
în lanțul de aprovizionare.
2
Tipul de design și stilurile arhitecturale
Proiectul este organizat folosind o arhitectură stratificată, care include următoarele
straturi:
1. Controller Layer: Acest strat este responsabil pentru gestionarea cererilor
primite și traducerea lor în acțiuni ce urmează să fie realizate de stratul de
servicii.
2. Service Layer: Acest strat encapsulează logica de business a aplicației,
controlând tranzacțiile și coordonând răspunsurile.
3. Model Layer: Acest strat reprezintă datele și regulile care guvernează accesul și
actualizările acestor date.
4. DTO (Data Transfer Object) Layer: Acest strat este utilizat pentru transferul
datelor între procese sau prin conexiuni de rețea.
Proiectul utilizează mai multe modele de design, inclusiv:
1. Dependency Injection (DI): Acest model este utilizat pentru a injecta
dependențele în clase, făcându-le mai modulare și mai ușor de testat.
2. Service Layer Pattern: Acest model încapsulează logica de business a
aplicației, controlează tranzacțiile și coordonează răspunsurile în implementare.
3. Factory Pattern: Acest model furnizează o interfață pentru crearea obiectelor
într-o superclasă, dar permite subclaselor să altereze tipul de obiecte care vor fi
create.
4. Observer Pattern: Acest model permite obiectelor să se actualizeze automat în
răspuns la schimbările dintr-un alt obiect.
5. Caching Pattern: Acest model este utilizat pentru a stoca temporar date
duplicate, cu scopul de a crește performanța și de a reduce cerințele asupra
resurselor externe.
6. Singleton Pattern: Variabila injector este o instanță statică a clasei Injector, care
este creată o singură dată și apoi utilizată în întreaga aplicație. Aceasta
3
O primă privire a aplicației
Figură 1 - Fereastra de pornire
Figură 2 - Detaliile abonamentului actual al organizatiei
4
Figură 3 - Diferitele tipuri de roluri accesibile utilizatorilor aplicației
Figură 4 - Setările notificărilor
5
Figură 5 - Raportul de performanță pentru un anumit furnizor
Figură 6 - Raportul de performanță pentru o anumită fabrică
6
Securitatea aplicației
JSON Web Tokens (JWT)
JSON Web Tokens (JWT) reprezintă o metodă compactă și sigură de transferare a
declarațiilor între două părți. Aceste afirmații sunt codificate ca un obiect JSON, care
poate fi semnat digital sau protejat pentru integritate cu un cod de autentificare a
mesajului (MAC) și/sau criptat. JWT-urile sunt utilizate frecvent în aplicațiile web pentru
autentificare și schimb de informații. La autentificarea reușită a unui utilizator, serverul
generează un JWT care codifică identitatea utilizatorului și îl trimite înapoi clientului.
Clientul poate folosi apoi JWT-ul pentru a autentifica cererile ulterioare către server.
Această abordare transferă sarcina gestionării sesiunilor de utilizator către client,
îmbunătățind potențial performanța prin reducerea necesității de interogări în baza de
date pentru autentificarea cererilor.
Clasa TokenManager
Această clasă este responsabilă pentru gestionarea tokenurilor JWT pe partea
clientului. Utilizează API-ul Preferences în Java pentru a stoca și a recupera în mod sigur
tokenul JWT.
Clasa TokenManager are trei metode statice: saveToken(String token),
getToken() și removeToken().
Metoda saveToken(String token) este folosită pentru a salva un token JWT.
Aceasta obține o instanță Preferences asociată cu clasa TokenManager și folosește
metoda put(String key, String value) a clasei Preferences pentru a stoca tokenul JWT.
Cheia folosită pentru a stoca tokenul este "jwtToken".
Metoda getToken() este folosită pentru a recupera tokenul JWT salvat. Aceasta
obține o instanță Preferences asociată cu clasa TokenManager și folosește metoda
get(String key, String def) a clasei Preferences pentru a recupera tokenul JWT. Dacă
nu se găsește niciun token, returnează null.
Metoda removeToken() este folosită pentru a elimina tokenul JWT salvat.
Aceasta obține o instanță Preferences asociată cu clasa TokenManager și folosește
metoda remove(String key) a clasei Preferences pentru a elimina tokenul JWT.
Clasa TokenManager nu interacționează direct cu serverul backend.
Gestionarea efectivă a comunicării cu serverul se face în alte părți ale aplicației, unde
sunt realizate cererile HTTP către server. TokenManager oferă o modalitate sigură de a
stoca, recupera și elimina tokenurile JWT. Aceasta permite aplicației să rețină starea de
autentificare a utilizatorului între sesiuni diferite, îmbunătățind astfel experiența
utilizatorului.
7
public class TokenManager {
private static final String JWT_KEY = "jwtToken";
public static void saveToken(String token) {
Preferences prefs =
Preferences.userNodeForPackage(TokenManager.class);
prefs.put(JWT_KEY, token);
}
public static String getToken() {
Preferences prefs =
Preferences.userNodeForPackage(TokenManager.class);
return prefs.get(JWT_KEY, null);
}
public static void removeToken() {
Preferences prefs =
Preferences.userNodeForPackage(TokenManager.class);
prefs.remove(JWT_KEY);
}
}
În backend-ul aplicației, este o clasă dedicată, JwtTokenProvider, care se ocupă
de crearea, parcurgerea și validarea JSON Web Tokens (JWTs). Această clasă este un
component al aplicației Spring Boot și este gestionată automat de framework-ul Spring.
Clasa JwtTokenProvider folosește o cheie secretă, JWT_SECRET, pentru a
semna JWT-urile. Această cheie secretă este injectată din fișierul de proprietăți al
aplicației, asigurând integritatea token-urilor.
Când este generat un token, este invocată metoda
generateToken(Authentication authentication). Această metodă primește un obiect
Authentication, extrage detaliile utilizatorului și folosește aceste informații pentru a
seta subiectul JWT-ului la numele utilizatorului. De asemenea, include ID-ul organizației
ca o revendicare. Token-ul este setat să expire după o anumită perioadă (1 săptămână
în acest caz) și este semnat cu algoritmul HS512 folosind JWT_SECRET.
Pentru a parcurge un JWT și a extrage numele de utilizator și ID-ul organizației,
sunt folosite metodele getUsernameFromJWT(String token) și
getOrganizationIdFromJWT(String token). Aceste metode construiesc un parser Jwts
cu JWT_SECRET, parcurg token-ul pentru a obține revendicările și apoi recuperează
subiectul sau revendicarea ID-ului organizației.
Metoda validateToken(String authToken) este folosită pentru a verifica dacă un
JWT dat este valid. Încearcă să parcurgă token-ul cu JWT_SECRET. Dacă parcurgerea
este reușită, token-ul este valid, iar dacă este aruncată o excepție, token-ul este invalid.
8
@Component
public class JwtTokenProvider {
@Value("${jwt.secret}")
private String JWT_SECRET;
private static final long JWT_EXPIRATION = 604800000L; // 1 week
private static final Logger logger =
Logger.getLogger(JwtTokenProvider.class.getName());
public String generateToken(Authentication authentication) {
UserDetailsImpl userDetails = (UserDetailsImpl)
authentication.getPrincipal();
Date now = new Date();
Date expiryDate = new Date(now.getTime() + JWT_EXPIRATION);
return Jwts.builder()
.setSubject(userDetails.getUsername())
.claim("organization_id",
userDetails.getOrganizationId())
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(SignatureAlgorithm.HS512, JWT_SECRET)
.compact();
}
public String getUsernameFromJWT(String token) {
logger.info("Token: {}" + token);
Claims claims = Jwts.parserBuilder()
.setSigningKey(JWT_SECRET)
.build()
.parseClaimsJws(token)
.getBody();
return claims.getSubject();
}
public Integer getOrganizationIdFromJWT(String token) {
Claims claims = Jwts.parserBuilder()
.setSigningKey(JWT_SECRET)
.build()
.parseClaimsJws(token)
.getBody();
return claims.get("organization_id", Integer.class);
}
public boolean validateToken(String authToken) {
try {
Jwts.parserBuilder().setSigningKey(JWT_SECRET).build().parseClaimsJws(aut
hToken);
return true;
} catch (Exception ex) {
logger.info("Invalid JWT token: " + ex);
return false;
}
}
}
9
Clasa AuthController
Clasa AuthController gestionează autentificarea utilizatorilor. Este responsabilă
pentru gestionarea funcționalității de login a aplicației.
Clasa primește o instanță de AuthenticationService, care este utilizată pentru a
autentifica utilizatorul. Aceasta se face folosind Guice. Constructorul clasei primește și
setează serviciul de autentificare:
@Inject
public AuthController(AuthenticationService authenticationService) {
this.authenticationService = authenticationService;
}
usernameField și passwordField sunt controale JavaFX care captează inputul
utilizatorului pentru numele de utilizator și parolă. Aceste câmpuri sunt annotate cu
@FXML, ceea ce permite injectarea lor de către JavaFX.
@FXML
private TextField usernameField;
@FXML
private PasswordField passwordField;
Metoda handleLogin() este responsabilă pentru procesul de login. Aceasta
apelează metoda login a AuthenticationService cu textul din usernameField și
passwordField ca argumente. Dacă utilizatorul este autentificat, SceneManager este
utilizat pentru a încărca scena principală a aplicației. Dacă utilizatorul nu este
autentificat, se afișează un mesaj de eroare.
@FXML
private void handleLogin() {
boolean isAuthenticated =
authenticationService.login(usernameField.getText(),
passwordField.getText());
}
if (isAuthenticated) {
try {
SceneManager.loadMainScene();
} catch (Exception ex) {
ex.printStackTrace();
}
} else {
Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setTitle("Login Failed");
alert.setHeaderText(null);
alert.setContentText("Username or password is incorrect.");
alert.showAndWait();
}
10
Clasa AuthenticationServiceImpl
Clasa AuthenticationServiceImpl este o clasa de servicii care gestionează
autentificarea utilizatorilor în aplicație. Aceasta comunică cu un API pentru a valida
acreditările utilizatorilor și token-urile JWT.
Clasa primește o instanță de HttpClient și un serviciu TokenManager.
HttpClient este utilizat pentru a trimite cereri HTTP către API, în timp ce TokenManager
gestionează token-urile JWT.
@Inject
public AuthenticationServiceImpl(HttpClient client,
org.chainoptim.desktop.core.user.service.TokenManager
tokenManagerService) {
this.client = client;
this.tokenManagerService = tokenManagerService;
}
Metoda login trimite o cerere POST către endpoint-ul /api/v1/login cu numele de
utilizator și parola utilizatorului. Dacă codul de răspuns este HTTP_OK, aceasta citește
token-ul JWT din corpul răspunsului și îl salvează folosind atât TokenManager, cât și
tokenManagerService. Dacă login-ul este de succes, metoda returnează true; în caz
contrar, returnează false.
public boolean login(String username, String password) {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://localhost:8080/api/v1/login"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(String.format
("{\"username\":\"%s\",\"password\":\"%s\"}", username, password)))
.build();
try {
HttpResponse<String> response = client.send(request,
HttpResponse.BodyHandlers.ofString());
}
if (response.statusCode() == HttpURLConnection.HTTP_OK) {
String responseBody = response.body();
ObjectMapper mapper = new ObjectMapper();
JsonNode jsonResponse = mapper.readTree(responseBody);
String jwtToken = jsonResponse.get("accessToken").asText();
TokenManager.saveToken(jwtToken);
tokenManagerService.saveToken(jwtToken);
return true;
} else {
return false;
}
} catch (IOException | InterruptedException ex) {
ex.printStackTrace();
return false;
}
11
Metoda validateJWTToken trimite o cerere POST către endpoint-ul
/api/v1/validate-token cu token-ul JWT. Returnează true dacă codul de răspuns este
200, indicând că token-ul este valid, și false în caz contrar.
public boolean validateJWTToken(String jwtToken) {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://localhost:8080/api/v1/validatetoken"))
.POST(HttpRequest.BodyPublishers.ofString(jwtToken))
.header("Content-Type", "application/json")
.build();
try {
HttpResponse<String> response = client.send(request,
HttpResponse.BodyHandlers.ofString());
return response.statusCode() == 200;
} catch (IOException | InterruptedException ex) {
ex.printStackTrace();
return false;
}
}
Metoda getUsernameFromJWTToken trimite o cerere POST către endpoint-ul
/api/v1/get-username-from-token cu token-ul JWT. Returnează numele de utilizator
din corpul răspunsului dacă codul de răspuns este 200, și un Optional gol în caz
contrar.
public Optional<String> getUsernameFromJWTToken(String jwtToken) {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://localhost:8080/api/v1/get-usernamefrom-token"))
.POST(HttpRequest.BodyPublishers.ofString(jwtToken))
.header("Content-Type", "application/json")
.build();
try {
HttpResponse<String> response = client.send(request,
HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 200) {
return response.body().describeConstable();
} else {
return Optional.empty();
}
} catch (IOException | InterruptedException ex) {
ex.printStackTrace(); return Optional.empty();
}
}
Metoda logout elimină token-ul JWT din stocare, setează utilizatorul curent în
TenantContext la null și invalidează cache-ul de vizualizări în NavigationServiceImpl.
public void logout() {
TokenManager.removeToken();
tokenManagerService.removeToken();
TenantContext.setCurrentUser(null);
NavigationServiceImpl.invalidateViewCache();
}
12
Prezentarea frontend-ului
Clasa MainApplication
Clasa MainApplication servește ca punct de intrare pentru o aplicație desktop.
Aplicația folosește biblioteca Guice de la Google pentru injecția de dependențe și
JavaFX pentru interfața grafică.
Această clasă extinde clasa Application din JavaFX, care este o clasă abstractă
ce reprezintă o aplicație JavaFX. Are trei metode importante: init(), start(Stage
primaryStage) și main(String[] args).
Metoda init() este apelată la începutul ciclului de viață al aplicației. În această
metodă, este creat un injector Guice cu AppModule și se obține o instanță a
AuthenticationServiceImpl din injector. Acest lucru este realizat cu următoarele linii
de cod:
@Override
public void init() throws Exception {
injector = Guice.createInjector(new AppModule());
authenticationService =
injector.getInstance(AuthenticationServiceImpl.class);
}
13
Metoda start(Stage primaryStage) este apelată după init(). Această metodă
setează scena principală a aplicației, încarcă fonturile Roboto și decide ce scenă să
încarce în funcție de validitatea unui token JWT. Dacă tokenul este valid, scena
principală este încărcată. În caz contrar, scena de autentificare este încărcată. Tokenul
JWT este obținut și validat cu următoarele linii de cod:
String jwtToken = TokenManager.getToken();
boolean isTokenValid = jwtToken != null &&
authenticationService.validateJWTToken(jwtToken);
if (isTokenValid) {
SceneManager.loadMainScene();
} else {
SceneManager.loadLoginScene();
}
Metoda main(String[] args) este punctul principal de intrare pentru toate
aplicațiile JavaFX. Aceasta afișează un mesaj de lansare și apelează metoda
launch(args) moștenită din clasa Application, care va apela ulterior metodele init() și
start().
public static void main(String[] args) {
System.out.println("Launching ChainOptim Desktop Application");
launch(args);
}
Clasa AppModule
Clasa AppModule este o clasă de configurare pentru Google's Guice, un cadru
ușor de injectare a dependențelor. Această clasă este responsabilă pentru configurarea
injecției de dependențe pentru aplicație.
Clasa extinde AbstractModule, o clasă de bază pentru modulele Guice.
Modulele în Guice sunt utilizate pentru a lega interfețele de implementările lor și pentru
a configura alte aspecte ale creării obiectelor.
Metoda configure() este suprascrisă pentru a defini aceste legături. De exemplu,
interfața NavigationService este legată de implementarea sa NavigationServiceImpl:
// Bind interfaces to implementations
// Core
// - Main
bind(NavigationService.class).to(NavigationServiceImpl.class);
// - Abstraction
bind(ControllerFactory.class).to(GuiceControllerFactory.class);
bind(ThreadRunner.class).to(JavaFXThreadRunner.class);
bind(FXMLLoaderService.class).to(FXMLLoaderServiceImpl.class).in(Singleto
n.class);
// - Notifications
14
bind(NotificationPersistenceService.class).to(NotificationPersistenceServ
iceImpl.class);
// - Settings
bind(UserSettingsService.class).to(UserSettingsServiceImpl.class);
// Features
// - Supplier
bind(SupplierService.class).to(SupplierServiceImpl.class);
bind(SupplierWriteService.class).to(SupplierWriteServiceImpl.class);
bind(SupplierOrdersService.class).to(SupplierOrdersServiceImpl.class);
bind(SupplierOrdersWriteService.class).to(SupplierOrdersWriteServiceImpl.
class);
bind(SupplierShipmentsService.class).to(SupplierShipmentsServiceImpl.clas
s);
. . .
// - Components
bind(ComponentService.class).to(ComponentServiceImpl.class);
// - Search
bind(SearchParams.class).to(SearchParamsImpl.class);
// - Toast
bind(ToastManager.class).to(ToastManagerImpl.class);
// - Caching
bind(new
TypeLiteral<CachingService<PaginatedResults<NotificationUser>>>() {})
.to(new
TypeLiteral<CachingServiceImpl<PaginatedResults<NotificationUser>>>() {})
.in(Singleton.class);
bind(new TypeLiteral<CachingService<PaginatedResults<Product>>>() {})
.to(new
TypeLiteral<CachingServiceImpl<PaginatedResults<Product>>>() {})
.in(Singleton.class);
Acest lucru înseamnă că ori de câte ori este necesară o instanță a
NavigationService, Guice va furniza o instanță a NavigationServiceImpl.
Unele servicii sunt legate ca singletonuri folosind metoda in(Singleton.class).
De exemplu:
// Singletons
bind(NavigationServiceImpl.class).asEagerSingleton();
bind(FallbackManager.class).in(Singleton.class);
bind(CurrentSelectionService.class).in(Singleton.class);
bind(NotificationManager.class).in(Singleton.class);
bind(SupplyChainSnapshotContext.class).in(Singleton.class);
bind(HttpClient.class).toInstance(HttpClient.newHttpClient());
Acest lucru asigură că Guice va crea, de exemplu, o singură instanță a
FallbackManager și o va reutiliza pentru toate injecțiile.
15
Clasa AppController
Clasa AppController gestionează conținutul principal afișat prin intermediul
SidebarController și NavigationService. În plus, este responsabilă pentru încărcarea
detaliilor utilizatorului curent, a setărilor utilizatorului și inițierea unei conexiuni
WebSocket pentru notificări în timp real.
Clasa este injectată cu mai multe servicii și controllere, inclusiv
NavigationService, AuthenticationService, UserService, UserSettingsService,
NotificationManager și SidebarController. Aceste dependențe sunt furnizate de
Guice.
@Inject
public AppController(NavigationService navigationService,
AuthenticationService authenticationService,
UserService userService,
UserSettingsService userSettingsService,
NotificationManager notificationManager,
SidebarController sidebarController) {
this.navigationService = navigationService;
this.authenticationService = authenticationService;
this.userService = userService;
this.userSettingsService = userSettingsService;
this.notificationManager = notificationManager;
this.sidebarController = sidebarController;
}
Metoda initialize() verifică utilizatorul curent din TenantContext. Dacă
utilizatorul nu este găsit, încearcă să recupereze un token JWT folosind TokenManager.
Dacă este găsit un token, extrage numele de utilizator din token și setează utilizatorul.
De asemenea, seteaza NavigationService.
public void initialize() {
navigationService.setMainContentArea(contentArea);
sidebarController.setNavigationService(navigationService);
loadStartUpView();
User currentUser = TenantContext.getCurrentUser();
if (currentUser != null) return;
String jwtToken = TokenManager.getToken();
if (jwtToken == null) return;
authenticationService.getUsernameFromJWTToken(jwtToken).ifPresent(this::f
etchAndSetUser);
}
Metoda fetchAndSetUser(String username) recuperează utilizatorul după
numele său de utilizator și îl setează în TenantContext. De asemenea, recuperează
setările utilizatorului și le setează în TenantSettingsContext. Odată ce utilizatorul și
16
setările sunt încărcate, zona de pornire este ascunsă, iar panoul principal al aplicației
devine vizibil.
private void fetchAndSetUser(String username) {
userService.getUserByUsername(username)
.thenApply(this::handleUserResponse)
.exceptionally(ex -> new Result<>());
}
private Result<User> handleUserResponse(Result<User> result) {
Platform.runLater(() -> {
if (result.getError() != null) {
return;
}
User user = result.getData();
user.getOrganization().setSubscriptionPlanTier(PRO);
TenantContext.setCurrentUser(user);
userSettingsService.getUserSettings(user.getId())
.thenAccept(result1 -> {
if (result1.getError() != null) return;
TenantSettingsContext.setCurrentUserSettings(result1.getData());
startUpArea.setVisible(false);
appPane.setVisible(true);
});
startWebSocket(user);
});
return result;
}
În cele din urmă, metoda startWebSocket(User user) inițiază o conexiune
WebSocket pentru notificări în timp real. Clientul WebSocket este creat cu un URI care
include ID-ul utilizatorului ca parametru și un consumator care adaugă notificările
primite UI.
private void startWebSocket(User user) {
String userIdParam = "?userId=" + user.getId();
System.out.println("Connecting to WebSocket with userId: " +
user.getId());
try {
URI serverUri = new URI("ws://localhost:8080/ws" + userIdParam);
Consumer<Notification> addMessageToUI =
notificationManager::showNotification;
NotificationWebSocketClient client = new
NotificationWebSocketClient(serverUri, addMessageToUI);
client.connect();
} catch (URISyntaxException e) {
e.printStackTrace();
}
}
17
Clasa NavigationServiceImpl
Clasa NavigationServiceImpl este o clasă de servicii responsabilă de
gestionarea navigării în cadrul aplicației. Aceasta încarcă vizualizări la cerere, le pune in
memoria cache și înregistrează istoricul navigării pentru a permite navigarea înapoi.
Clasa este injectată cu mai multe servicii. Aceste dependențe sunt furnizate de
Guice. Constructorul clasei NavigationServiceImpl primește aceste dependențe și le
setează în câmpurile clasei:
@Inject
public NavigationServiceImpl(FXMLLoaderService fxmlLoaderService,
ControllerFactory controllerFactory,
ThreadRunner threadRunner,
FallbackManager fallbackManager) {
this.fxmlLoaderService = fxmlLoaderService;
this.controllerFactory = controllerFactory;
this.threadRunner = threadRunner;
this.fallbackManager = fallbackManager;
}
Metoda switchView este utilizată pentru a schimba vizualizarea curentă.
Aceasta verifică dacă vizualizarea solicitată este deja vizualizarea curentă, și dacă nu,
resetează starea fallback, obține vizualizarea din memoria cache sau o încarcă și apoi o
afișează. De asemenea, gestionează istoricul navigării.
public <T> void switchView(String viewKey, boolean forward, T extraData)
{
if (Objects.equals(currentViewKey, viewKey)) {
return;
}
fallbackManager.reset();
Node view = viewCache.computeIfAbsent(viewKey, key ->
loadView(viewKey, extraData));
if (view != null) {
threadRunner.runLater(() ->
mainContentArea.getChildren().setAll(view));
handleHistory(forward);
currentViewKey = viewKey;
}
}
Metoda loadView este utilizată pentru a încărca o vizualizare. Aceasta extrage
cheia de bază din cheia vizualizării, obține calea către fișierul FXML al vizualizării,
setează un FXMLLoader cu calea vizualizării, apoi încarcă vizualizarea. Dacă controllerul vizualizării este o instanță de DataReceiver, setează datele suplimentare.
18
private <T> Node loadView(String viewKey, T extraData) {
String baseViewKey = findBaseKey(viewKey);
String viewPath = viewMap.get(baseViewKey);
if (viewPath == null) {
System.out.println("View path for " + baseViewKey + " not
found.");
return null;
}
FXMLLoader loader = fxmlLoaderService.setUpLoader(viewPath,
controllerFactory::createController);
try {
Node view = loader.load();
Object controller = loader.getController();
if (controller instanceof DataReceiver dataReceiver) {
dataReceiver.setData(extraData);
}
}
return view;
} catch (Exception e) {
e.printStackTrace();
return null;
}
Metoda goBack este utilizată pentru a naviga înapoi la vizualizarea anterioară.
Aceasta verifică dacă există chei de vizualizări anterioare, și dacă există, schimbă
vizualizarea la ultima vizualizare anterioară.
public void goBack() {
if (previousViewKeys != null && !previousViewKeys.isEmpty()) {
switchView(previousViewKeys.getLast(), false, null);
}
}
Principiul Fallback
Principiul fallback este un tipar de design
utilizat în dezvoltarea software pentru a oferi o
soluție alternativă sau funcționalitate atunci când
funcționalitatea principală eșuează sau nu este
disponibilă. În acest context, mecanismul fallback
este utilizat pentru a gestiona diferite stări ale
aplicației și pentru a afișa vizualizări adecvate în
funcție de aceste stări. Acest lucru asigură că
aplicația poate gestiona diverse scenarii într-un
mod elegant și poate oferi o experiență de utilizare lină.
19
Clasa FallbackManager
Clasa FallbackManager este o clasă care gestionează diverse stări de fallback
în aplicație. Utilizează proprietăți JavaFX pentru a reprezenta aceste stări, permițând o
legare ușoară la elementele UI.
Clasa definește mai multe instanțe de BooleanProperty și StringProperty,
fiecare reprezentând o stare de fallback diferită. Acestea includ errorMessage,
isLoading, noOrganization, noResults și isEmpty.
private final StringProperty errorMessage = new SimpleStringProperty("");
private final BooleanProperty isLoading = new
SimpleBooleanProperty(false);
private final BooleanProperty noOrganization = new
SimpleBooleanProperty(false);
private final BooleanProperty noResults = new
SimpleBooleanProperty(false);
private final BooleanProperty isEmpty = new SimpleBooleanProperty(true);
În constructor, proprietatea isEmpty este legată de negarea tuturor celorlalte
proprietăți boolean și de golirea proprietății errorMessage. Aceasta înseamnă că
isEmpty va fi true doar dacă toate celelalte stări sunt false și nu există niciun mesaj de
eroare.
public FallbackManager() {
isEmpty.bind(isLoadingProperty().not().and(noOrganizationProperty().not()
.and(noResultsProperty().not().and(errorMessageProperty().isEmpty()))));
}
Clasa oferă metode getter și setter pentru fiecare proprietate, precum și metode
pentru a obține proprietatea în sine. Acest lucru permite legarea directă la elementele
UI.
public String getErrorMessage() {
return errorMessage.get();
}
public void setErrorMessage(String errorMessage) {
this.errorMessage.set(errorMessage);
}
public StringProperty errorMessageProperty() {
return errorMessage;
}
public boolean isLoading() {
return isLoading.get();
}
public void setLoading(boolean isLoading) {
this.isLoading.set(isLoading);
}
20
public BooleanProperty isLoadingProperty() {
return isLoading;
}
public boolean isNoOrganization() {
return noOrganization.get();
}
public void setNoOrganization(boolean noOrganization) {
this.noOrganization.set(noOrganization);
}
public BooleanProperty noOrganizationProperty() {
return noOrganization;
}
public boolean isNoResults() {
return noResults.get();
}
public void setNoResults(boolean noResults) {
this.noResults.set(noResults);
}
public BooleanProperty noResultsProperty() {
return noResults;
}
Metoda reset setează toate stările la false și șterge mesajul de eroare, dar
numai dacă isEmpty este false. Acest lucru permite resetarea stării de fallback atunci
când este necesar.
public void reset() {
if (!isEmpty.get()) {
setErrorMessage("");
setLoading(false);
setNoOrganization(false);
setNoResults(false);
}
}
Clasa FallbackManagerController
Clasa FallbackManagerController este o clasă de controler care gestionează
afișarea vizualizărilor fallback în aplicație. Ascultă schimbările din starea
FallbackManager și actualizează vizualizarea corespunzător.
Clasa este injectată cu o instanță FallbackManager, care este utilizată pentru a
urmări starea actuală de fallback.
21
@Inject
public FallbackManagerController(FallbackManager fallbackManager) {
this.fallbackManager = fallbackManager;
}
Metoda initialize() configurează ascultători pentru proprietățile
FallbackManager. Ori de câte ori o proprietate se schimbă, este apelată metoda
updateView().
@FXML
public void initialize() {
setupChangeListeners();
updateView();
}
private void setupChangeListeners() {
fallbackManager.errorMessageProperty().addListener((obs, oldVal,
newVal) -> updateView());
fallbackManager.isLoadingProperty().addListener((obs, oldVal, newVal)
-> updateView());
fallbackManager.noOrganizationProperty().addListener((obs, oldVal,
newVal) -> updateView());
fallbackManager.noResultsProperty().addListener((obs, oldVal, newVal)
-> updateView());
}
Metoda updateView() determină vizualizarea corespunzătoare de afișat în
funcție de starea curentă a FallbackManager. Dacă nu există o stare de fallback (adică
viewPath este gol), se golește fallbackContentHolder. În caz contrar, se încarcă
vizualizarea corespunzătoare din cache sau din sistemul de fișiere, se setează orice
date necesare în controler și se afișează vizualizarea.
private void updateView() {
String viewPath = determineViewPathBasedOnState();
if (viewPath.isEmpty()) {
fallbackContentHolder.getChildren().clear();
fallbackContentHolder.setPrefSize(0, 0);
return;
}
if (!loadedViews.containsKey(viewPath)) {
loadFallbackView(viewPath);
}
Node view = loadedViews.get(viewPath);
Object controller = loadedControllers.get(viewPath);
if
(viewPath.equals("/org/chainoptim/desktop/shared/fallback/ErrorFallbackVi
ew.fxml")) {
((ErrorFallbackController)controller).initialize(fallbackManager.getError
Message());
}
fallbackContentHolder.getChildren().setAll(view);
}
22
Metoda determineViewPathBasedOnState() returnează calea vizualizării care
corespunde stării curente a FallbackManager.
private String determineViewPathBasedOnState() {
if (!fallbackManager.getErrorMessage().isEmpty()) {
return
"/org/chainoptim/desktop/shared/fallback/ErrorFallbackView.fxml";
} else if (fallbackManager.isLoading()) {
return
"/org/chainoptim/desktop/shared/fallback/LoadingFallbackView.fxml";
} else if (fallbackManager.isNoOrganization()) {
return
"/org/chainoptim/desktop/shared/fallback/NoOrganizationFallbackView.fxml"
;
} else if (fallbackManager.isNoResults()) {
return
"/org/chainoptim/desktop/shared/fallback/NoResultsFallbackView.fxml";
}
return "";
}
Metoda loadFallbackView(String viewPath) încarcă o vizualizare din sistemul
de fișiere, o stochează în cache și stochează controlerul acesteia.
private void loadFallbackView(String viewPath) {
try {
FXMLLoader loader = new
FXMLLoader(getClass().getResource(viewPath));
Node view = loader.load();
Object controller = loader.getController();
loadedViews.put(viewPath, view);
loadedControllers.put(viewPath, controller);
} catch (IOException e) {
e.printStackTrace();
}
}
Notificări – Toast Notification
Modelul Toast Notification este un tipar de proiectare utilizat frecvent în
dezvoltarea interfețelor grafice. Termenul toast provine de la efectul vizual al unui mesaj
care apare pe ecran, asemănător unei felii de pâine care sare dintr-un prăjitor de pâine.
O notificare toast este un mesaj mic și tranzitoriu care apare pe ecran pentru a
oferi feedback utilizatorului. Aceste notificări sunt utilizate de obicei pentru a afișa
informații scurte, care dispar automat și care nu necesită interacțiunea utilizatorului.
Ele sunt adesea folosite pentru a confirma că o acțiune a fost realizată (cum ar fi
salvarea unui fișier), pentru a notifica utilizatorul despre un eveniment al sistemului
23
(cum ar fi un mesaj primit) sau pentru a informa utilizatorul despre o eroare (de
exemplu, când un fișier nu se încarcă).
Caracteristicile principale ale notificărilor toast sunt:
1. Non-Blocante: Notificările toast nu întrerup activitatea utilizatorului. Ele apar pe
ecran pentru o durată stabilită și apoi dispar automat. Ele nu împiedică
utilizatorul să interacționeze cu restul aplicației cât timp sunt vizibile.
2. Temporizate: Notificările toast sunt afișate pentru o durată specifică și apoi sunt
închise automat. Durata poate varia în funcție de importanța și cantitatea de
informații din notificare.
3. Informative: Notificările toast sunt utilizate pentru a oferi feedback sau
informații. Ele nu sunt folosite pentru alerte critice care necesită atenție
imediată sau interacțiune.
4. Tranzitorii: Notificările toast nu sunt persistente. Odată ce dispar, ele sunt
complet eliminate. Ele nu lasă nicio urmă pe interfața utilizatorului.
În cadrul aplicației, clasele ToastManagerImpl și ToastController sunt folosite
pentru a gestiona și afișa notificările toast. Clasa ToastManagerImpl creează și
poziționează notificările toast și gestionează evenimentele din ciclul lor de viață. Clasa
ToastController configurează vizualizarea notificărilor toast și gestionează
interacțiunea utilizatorului cu notificările toast.
Clasa ToastController
Clasa ToastController gestionează afișarea notificărilor toast. Precum am
menționat anterior, notificările toast sunt mici ferestre pop-up care oferă feedback
despre o operațiune printr-un mesaj scurt afișat într-un popup.
Clasa menține o instanță de ToastInfo, care conține informații despre toast-ul
ce urmează să fie afișat, și o instanță de Runnable, care este o funcție de callback ce
se execută când toast-ul este închis.
Metoda initialize(ToastInfo toastInfo, Runnable closeCallback) este utilizată
pentru a inițializa controlerul cu ToastInfo și closeCallback. De asemenea, aceasta
apelează metodele initializeIcons() și initializeToast() pentru a configura iconițele și
toast-ul.
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ToastInfo {
24
}
private String title;
private String message;
private OperationOutcome operationOutcome;
private ToastInfo toastInfo;
private Runnable closeCallback;
public void initialize(ToastInfo toastInfo, Runnable closeCallback) {
this.toastInfo = toastInfo;
this.closeCallback = closeCallback;
}
initializeIcons();
initializeToast();
Metoda initializeIcons() încarcă iconițele utilizate în toast din sistemul de
fișiere.
private void initializeIcons() {
closeIcon = new
Image(Objects.requireNonNull(ToastController.class.getResourceAsStream("/
img/xmark-solid.png")));
successIcon = new
Image(Objects.requireNonNull(ToastController.class.getResourceAsStream("/
img/check-solid.png")));
errorIcon = new
Image(Objects.requireNonNull(ToastController.class.getResourceAsStream("/
img/xmark-solid-white.png")));
infoIcon = new
Image(Objects.requireNonNull(ToastController.class.getResourceAsStream("/
img/info-solid.png")));
warningIcon = new
Image(Objects.requireNonNull(ToastController.class.getResourceAsStream("/
img/triangle-exclamation-solid-white.png")));
}
Metoda initializeToast() configurează toast-ul. Aceasta setează titlul și mesajul
toast-ului, inițializează iconița de rezultat pe baza OperationOutcome din ToastInfo și
configurează butonul de închidere.
private void initializeToast() {
initializeOutcomeIcon();
titleLabel.setText(this.toastInfo.getTitle());
Text text = new Text(this.toastInfo.getMessage());
text.getStyleClass().setAll("toast-message");
messageTextFlow.getChildren().add(text);
}
initializeCloseButton();
25
Metoda initializeOutcomeIcon() setează iconița de rezultat pe baza
OperationOutcome din ToastInfo.
private void initializeOutcomeIcon() {
if (toastInfo == null) return;
ImageView iconView = new ImageView();
switch (toastInfo.getOperationOutcome()) {
case OperationOutcome.SUCCESS -> {
iconView.setImage(successIcon);
outcomeIconWrapper.getStyleClass().setAll("success-outcomeicon-wrapper");
}
case OperationOutcome.ERROR -> {
iconView.setImage(errorIcon);
outcomeIconWrapper.getStyleClass().setAll("error-outcomeicon-wrapper");
}
case OperationOutcome.INFO -> {
iconView.setImage(infoIcon);
outcomeIconWrapper.getStyleClass().setAll("info-outcome-iconwrapper");
}
case OperationOutcome.WARNING -> {
iconView.setImage(warningIcon);
outcomeIconWrapper.getStyleClass().setAll("warning-outcomeicon-wrapper");
}
default -> {}
}
iconView.setFitHeight(18);
iconView.setFitWidth(18);
}
outcomeIcon.setGraphic(iconView);
Metoda initializeCloseButton() configurează butonul de închidere. Aceasta
setează iconița butonului și configurează handler-ul de eveniment onAction pentru a
rula closeCallback.
private void initializeCloseButton() {
ImageView closeIconView = new ImageView(closeIcon);
closeIconView.setFitHeight(14);
closeIconView.setFitWidth(14);
closeButton.setGraphic(closeIconView);
closeButton.getStyleClass().setAll("no-style-button");
}
closeButton.setOnAction(e -> closeCallback.run());
Clasa ToastManagerImpl
Clasa este injectată cu o instanță de Stage, care reprezintă scena principală a
aplicației. De asemenea, menține o coadă de notificări toast active.
26
private final Stage mainStage;
private final Queue<Stage> activeToasts = new LinkedList<>();
@Inject
public ToastManagerImpl(Stage mainStage) {
this.mainStage = mainStage;
}
Metoda addToast(ToastInfo toastInfo) este utilizată pentru a crea și afișa o
nouă notificare toast. Aceasta creează un nou Stage pentru toast, îl poziționează și apoi
îl afișează.
Metoda createToastStage(ToastInfo toastInfo) inițializează vizualizarea toastului și configurează handler-ii de evenimente pentru momentul în care toast-ul este
afișat și ascuns. Când toast-ul este afișat, acesta se închide automat după un anumit
interval de timp. Când toast-ul este ascuns, acesta este eliminat din coada de toast-uri
active și pozițiile toast-urilor rămase sunt ajustate.
public void addToast(ToastInfo toastInfo) {
System.out.println("Showing toast: " + toastInfo);
Platform.runLater(() -> {
Stage popupStage = createToastStage(toastInfo);
positionAndShow(popupStage);
});
}
private Stage createToastStage(ToastInfo toastInfo) {
Stage popupStage = initializeToastView(toastInfo);
popupStage.setOnShown(e -> autoClose(popupStage,
timeToCloseSeconds.get(toastInfo.getOperationOutcome()) *
1000));
popupStage.setOnHidden(e -> {
activeToasts.remove(popupStage);
adjustToastsPositions();
});
return popupStage;
}
Metoda initializeToastView(ToastInfo toastInfo) încarcă vizualizarea toast-ului
dintr-un fișier FXML, configurează ToastController și setează scena stage-ului toastului.
private Stage initializeToastView(ToastInfo toastInfo) {
Stage popupStage = new Stage();
popupStage.initModality(Modality.NONE);
popupStage.initStyle(StageStyle.TRANSPARENT);
popupStage.setAlwaysOnTop(true);
FXMLLoader loader = new
FXMLLoader(getClass().getResource("/org/chainoptim/desktop/shared/toast/T
oastView.fxml"));
try {
Parent root = loader.load();
root.getStylesheets().add(Objects.requireNonNull(getClass()
27
.getResource("/css/toast.css")).toExternalForm());
ToastController popupController = loader.getController();
popupController.initialize(toastInfo, () ->
closeToast(popupStage));
}
Scene scene = new Scene(root, TOAST_WIDTH, TOAST_MIN_HEIGHT);
scene.setFill(Color.TRANSPARENT);
popupStage.setScene(scene);
} catch (IOException e) {
e.printStackTrace();
}
return popupStage;
Metoda positionAndShow(Stage popupStage) poziționează toast-ul pe ecran și
îl afișează. Poziția toast-ului este calculată în funcție de poziția și dimensiunea scenei
principale și de numărul de toast-uri active.
private void positionAndShow(Stage popupStage) {
double baseX = mainStage.getX() + mainStage.getWidth() - TOAST_WIDTH
- SPACE_BETWEEN - 30;
double baseY = mainStage.getY() + mainStage.getHeight() ((TOAST_MIN_HEIGHT + SPACE_BETWEEN) * (activeToasts.size()) + 1) - 150;
popupStage.setX(baseX);
popupStage.setY(baseY);
popupStage.show();
activeToasts.add(popupStage);
}
Caching
Caching este o tehnică utilizată pentru a stoca date într-o zonă de stocare
temporară, cunoscută sub numele de cache, astfel încât solicitările viitoare pentru
acele date să fie furnizate mai rapid. Datele stocate în cache pot fi rezultatul unei
calcule anterioare sau o copie a datelor stocate în altă parte.
Puncte cheie despre caching:
1. Îmbunătățirea Performanței: Caching-ul poate îmbunătăți semnificativ
performanța unei aplicații prin reducerea timpului necesar pentru a accesa
datele utilizate frecvent. În loc să recupereze datele din sursa originală (care ar
putea fi o operațiune lentă), aplicația poate recupera datele din cache, care este
de obicei mult mai rapid.
2. Reducerea Sarcinii: Stocând datele accesate frecvent în cache, o aplicație
poate reduce sarcina asupra sistemului sau serviciului de bază care furnizează
28
datele. Acest lucru poate fi deosebit de benefic în situațiile în care sistemul sau
serviciul de bază este intensiv în resurse sau are limite de utilizare.
3. Date Stagnante: Una dintre provocările caching-ului este gestionarea datelor
stagnante. Când datele originale se schimbă, datele stocate în cache devin
depășite sau stagnante. Există diverse strategii pentru a gestiona acest lucru,
cum ar fi setarea unei valori de expirare (TTL - Time To Live) pentru datele stocate
în cache, după care datele sunt eliminate automat din cache.
4. Politici de Evacuare a Cache-ului: Când cache-ul este plin, aplicația trebuie să
decidă care elemente să le elimine pentru a face loc pentru elementele noi.
Acest lucru este gestionat de o politică de evacuare a cache-ului. Politici
comune includ LRU (Least Recently Used), unde elementele utilizate cel mai
puțin recent sunt eliminate primele, și FIFO (First In, First Out), unde cele mai
vechi elemente sunt eliminate primele.
5. Tipuri de Caching: Există diferite tipuri de caching, inclusiv caching în memorie
(unde cache-ul este stocat în memoria principală a unui calculator), caching pe
disc (unde cache-ul este stocat pe un disc), caching distribuit (unde cache-ul
este răspândit pe mai multe noduri într-o rețea) și caching în browser (unde
conținutul web este stocat local pe calculatorul utilizatorului).
În contextul aplicației ChainOptim, interfața CachingService și implementarea sa
CachingServiceImpl oferă un mecanism generic de caching. Clasa CachedData
încapsulează datele care urmează să fie stocate în cache împreună cu timpul de
expirare, iar clasa CacheKeyBuilder furnizează metode pentru a construi chei pentru
caching. Aceste clase funcționează împreună pentru a furniza o soluție de caching care
îmbunătățește performanța aplicației prin reducerea timpului necesar pentru a accesa
datele utilizate frecvent.
Clasa CachedData
Clasa CachedData încapsulează datele ce urmează să fie cache-uite. Aceasta
include datele propriu-zise, timpul la care datele devin stagnante și timpul la care
datele au fost cache-uite.
Clasa utilizează adnotări Lombok pentru reducerea codului boilerplate.
Adnotarea generează metode get(), set(), equals(), hashCode(), și toString() pentru
câmpurile din clasă.
@Data
public class CachedData<T> {
private T data;
29
private float staleTimeMillis;
private Instant cachedAt;
Constructorul clasei primește datele și timpul în secunde după care datele devin
stagnante ca parametri. Acesta setează câmpul data cu datele furnizate, convertește
timpul stagnării în milisecunde și îl setează în câmpul staleTimeMillis, și setează
câmpul cachedAt cu momentul curent.
public CachedData(T data, float staleTime) {
this.data = data;
this.staleTimeMillis = staleTime * 1000;
this.cachedAt = Instant.now();
}
Metoda isStale() verifică dacă datele sunt stagnante. Acest lucru este realizat
fiind verificat dacă timpul curent este după momentul în care datele au fost cache-uite
plus timpul stagnării.
public boolean isStale() {
return Instant.now().isAfter(cachedAt.plusMillis((long)
staleTimeMillis));
}
Clasa CacheKeyBuilder
Clasa CacheKeyBuilder furnizează metode pentru construirea cheilor de cache.
Aceste chei sunt folosite pentru a identifica și a recupera date din cache.
Clasa are un constructor privat pentru a preveni instanțierea, deoarece este
destinată să fie folosită static.
Metoda buildAdvancedSearchKey este folosită pentru a construi o cheie de
cache pentru o operațiune de căutare avansată. Aceasta ia ca parametri o
funcționalitate (feature), o funcționalitate secundară (secondary feature), un ID
secundar (secondary ID) și un obiect SearchParams. Metoda construiește o cheie
concatenând acești parametri împreună cu proprietățile obiectului SearchParams.
Dacă obiectul SearchParams are filtre, acestea sunt convertite într-un șir JSON și
codate URL înainte de a fi adăugate la cheie.
public static String buildAdvancedSearchKey(String feature, String
secondaryFeature, String secondaryId, SearchParams searchParams) {
String key = feature + "/" + secondaryFeature + "/advanced/" +
secondaryId +
"?searchQuery=" + searchParams.getSearchQuery() +
"&sortBy=" + searchParams.getSortOption() +
"&ascending=" + searchParams.getAscending() +
"&page=" + searchParams.getPage() +
30
"&itemsPerPage=" + searchParams.getItemsPerPage();
if (!searchParams.getFiltersProperty().isEmpty()) {
String filtersJson;
try {
filtersJson = JsonUtil.getObjectMapper().writeValueAsString
(searchParams.getFiltersProperty());
filtersJson = URLEncoder.encode(filtersJson,
StandardCharsets.UTF_8);
} catch (Exception e) {
throw new RuntimeException("Error encoding filters to JSON",
e);
}
key += "&filters=" + filtersJson;
}
return key;
}
Metoda buildSecondaryFeatureKey este folosită pentru a construi o cheie de
cache pentru o funcționalitate secundară. Aceasta ia ca parametri o funcționalitate
principală (main feature), o funcționalitate secundară (secondary feature) și un ID al
funcționalității secundare (secondary feature ID). Metoda construiește o cheie
concatenând acești parametri. Aceasta metodă este utilizată pentru în diferite situații
precum obținerea istoricului producțiilor dintr-o fabrică.
public static String buildSecondaryFeatureKey(String mainFeature, String
secondaryFeature, Integer secondaryFeatureId) {
return mainFeature + "/" + secondaryFeature + "/" +
secondaryFeatureId.toString();
}
Clasa CachingServiceImpl
Clasa CachingServiceImplveste o implementare generică a serviciului de
caching. Aceasta oferă metode pentru a adăuga, a recupera și a elimina date din cache,
precum și pentru a verifica dacă datele au fost adăugate în cache și dacă sunt expirate.
De asemenea, clasei i se adaugă un serviciu de curățare programat pentru a elimina
intrările expirate din cache.
Când este creat un nou obiect CachingServiceImpl, constructorul este apelat.
Acesta inițializează variabila de instanță cache ca un nou ConcurrentHashMap.
Această hartă va fi folosită pentru a stoca datele cache-ate, cu cheile fiind cheile de
cache și valorile fiind obiectele CachedData.
Variabila de instanță executorService este inițializată ca un nou
ScheduledExecutorService cu un singur fir de execuție. Acest serviciu va fi folosit
31
pentru a programa sarcini care să ruleze la intervale fixe. În acest caz, este folosit
pentru a programa sarcina de curățare a cache-ului.
În cele din urmă, este apelată metoda scheduleCacheCleanup. Această
metodă programează metoda cleanupStaleEntries să ruleze la intervale fixe (la fiecare
30 de minute). Metoda cleanupStaleEntries elimină intrările expirate din cache.
private final Map<String, CachedData<T>> cache;
@Getter
private final ScheduledExecutorService executorService;
public CachingServiceImpl() {
cache = new ConcurrentHashMap<>();
executorService = Executors.newSingleThreadScheduledExecutor();
scheduleCacheCleanup();
}
Metoda add(String key, T data, float staleTime) este folosită pentru a adăuga
date în cache. Creează o nouă instanță CachedData cu datele și timpul de expirare
furnizate, și o pune în hartă cu cheia furnizată.
public void add(String key, T data, float staleTime) {
cache.put(key, new CachedData<>(data, staleTime));
}
Metoda get(String key) recuperează datele din cache. Returnează null dacă
datele nu sunt în cache sau dacă sunt expirate.
public T get(String key) {
CachedData<T> cachedData = cache.get(key);
if (cachedData == null || cachedData.isStale()) {
return null;
}
return cachedData.getData();
}
Metoda scheduleCacheCleanup() programează un serviciu de curățare care
rulează la intervale fixe. Serviciul de curățare elimină intrările expirate din cache.
private void scheduleCacheCleanup() {
executorService.scheduleAtFixedRate(this::cleanupStaleEntries, 30,
30, TimeUnit.MINUTES);
}
Metoda cleanupStaleEntries() este task-ul pe care îl rulează serviciul de
curățare. Elimină toate intrările din cache ale căror date sunt expirate.
public void cleanupStaleEntries() {
cache.entrySet().removeIf(entry -> entry.getValue().isStale());
}
32
Modul de implementare al căutării și sortării datelor
Clasa SearchParamsImpl este o implementare a interfeței SearchParams. Ea
reprezintă parametrii pentru o operațiune de căutare, incluzând interogarea de căutare,
filtrele, opțiunea de sortare, direcția de sortare, numărul paginii și numărul de elemente
pe pagină.
Clasa utilizează proprietăți JavaFX pentru a reprezenta acești parametri.
Proprietățile JavaFX sunt un tip special de variabile care pot fi observate, ceea ce
înseamnă că pot fi urmărite schimbările acestora. Acest lucru este deosebit de util întro interfață grafică, unde schimbările proprietăților pot fi reflectate automat în UI.
private final StringProperty searchQuery;
private final ObservableMap<String, String> filters;
private final StringProperty sortOption;
private final BooleanProperty ascending;
private final IntegerProperty page;
private final IntegerProperty itemsPerPage;
Constructorul clasei inițializează aceste proprietăți cu valori implicite.
Interogarea de căutare și opțiunea de sortare sunt inițializate ca șiruri de caractere
goale, respectiv "createdAt", filtrele ca o hartă goală, direcția de sortare ca falsă
(indicând ordinea descrescătoare), numărul paginii ca fiind 1 și numărul de elemente
pe pagină ca fiind 10.
public SearchParamsImpl() {
searchQuery = new SimpleStringProperty("");
filters = new SimpleMapProperty<>(FXCollections.observableMap(new
HashMap<>()));
sortOption = new SimpleStringProperty("createdAt");
ascending = new SimpleBooleanProperty(false);
page = new SimpleIntegerProperty(1);
itemsPerPage = new SimpleIntegerProperty(10);
}
Clasa oferă metode getter pentru proprietăți și valorile lor, precum și metode
setter pentru a actualiza valorile acestora. Când o metodă setter este apelată, aceasta
resetează numărul paginii la 1 și actualizează valoarea proprietății corespunzătoare.
Acest lucru se întâmplă deoarece schimbarea oricăruia dintre parametrii de căutare ar
trebui să înceapă căutarea de la prima pagină.
public void setSearchQuery(String searchQuery) {
this.page.set(1);
this.searchQuery.set(searchQuery);
}
33
Clasa SearchOptionsConfiguration
Clasa SearchOptionsConfiguration oferă opțiuni de căutare pentru diferite
funcționalități ale aplicației. Aceste opțiuni includ opțiuni de filtrare și sortare. Clasa are
un constructor privat pentru a preveni instanțierea, deoarece este destinată să fie
utilizată static.
Clasa definește mai multe obiecte SearchOptions ca variabile statice finale.
Fiecare dintre aceste obiecte reprezintă opțiunile de căutare pentru o funcționalitate
specifică. De exemplu, SUPPLIER_ORDER_OPTIONS reprezintă opțiunile de căutare
pentru funcționalitatea comenzilor furnizorului. Aceste opțiuni includ opțiuni de filtrare
precum "Order Date Start", "Quantity" și "Status" și opțiuni de sortare precum "Order
Date", "Estimated Delivery Date", "Delivery Date" și "Quantity".
private static final SearchOptions SUPPLIER_ORDER_OPTIONS = new
SearchOptions(
List.of(
new FilterOption(
new UIItem("Order Date Start", "orderDateStart"),
new ArrayList<>(),
FilterType.DATE
),
new FilterOption(
new UIItem("Quantity", "greaterThanQuantity"),
new ArrayList<>(),
FilterType.NUMBER
),
new FilterOption(
new UIItem("Status", "status"),
List.of(new UIItem("Delivered", "DELIVERED"), new
UIItem("Pending", "PENDING")),
FilterType.ENUM
)
),
Map.of(
"orderDate", "Order Date",
"estimatedDeliveryDate", "Estimated Delivery Date",
"deliveryDate", "Delivery Date",
"quantity", "Quantity"
)
);
Clasa definește, de asemenea, un SEARCH_OPTIONS_MAP care mapează
fiecare funcționalitate la obiectul său SearchOptions corespunzător. Această hartă
este utilizată pentru a recupera opțiunile de căutare pentru o anumită funcționalitate.
private static final Map<Feature, SearchOptions> SEARCH_OPTIONS_MAP =
Map.of(
Feature.SUPPLIER_ORDER, SUPPLIER_ORDER_OPTIONS,
Feature.SUPPLIER_SHIPMENT, SUPPLIER_SHIPMENT_OPTIONS,
Feature.FACTORY_INVENTORY, FACTORY_INVENTORY_OPTIONS,
Feature.CLIENT_ORDER, CLIENT_ORDER_OPTIONS,
Feature.CLIENT_SHIPMENT, CLIENT_SHIPMENT_OPTIONS
);
34
Metoda getSearchOptions este utilizată pentru a recupera opțiunile de căutare
pentru o anumită funcționalitate. Ea primește un obiect Feature ca parametru și
returnează obiectul SearchOptions corespunzător din SEARCH_OPTIONS_MAP.
public static SearchOptions getSearchOptions(Feature feature) {
return SEARCH_OPTIONS_MAP.get(feature);
}
Studiu de caz - Prezentare SupplierOrders
SupplierOrdersView.fxml
Fișierul SupplierOrdersView.fxml este un fișier XML care definește layout-ul
pentru SupplierOrders. Utilizează FXML, un limbaj bazat pe XML oferit de JavaFX,
pentru a defini interfața utilizatorului.
Atributul fx:controller specifică numele complet al clasei de control pentru
această vizualizare, care este SupplierOrdersController.
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<VBox xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="org.chainoptim.desktop.features.supplier.controller.Suppli
erOrdersController" >
<!-- Header -->
<StackPane fx:id="tableToolbarContainer"/>
<StackPane>
<!-- Content -->
<ScrollPane fx:id="supplierOrdersScrollPane" VBox.vgrow="ALWAYS"
fitToWidth="true">
<VBox style="-fx-background-color: purple;"
VBox.vgrow="ALWAYS" prefHeight="1000">
<TableView fx:id="tableView" styleClass="table-view"
VBox.vgrow="ALWAYS">
<columns>
<TableColumn fx:id="selectRowColumn"
prefWidth="50"/>
<TableColumn text="Order ID"
fx:id="orderIdColumn" minWidth="120"/>
<TableColumn text="Company ID"
fx:id="companyIdColumn" minWidth="150"/>
<TableColumn text="Supplier"
fx:id="supplierNameColumn" minWidth="150"/>
<TableColumn text="Component"
fx:id="componentNameColumn" minWidth="150"/>
<TableColumn text="Quantity"
35
fx:id="quantityColumn" minWidth="150"/>
<TableColumn text="Delivered Quantity"
fx:id="deliveredQuantityColumn" minWidth="150"/>
<TableColumn text="Status" fx:id="statusColumn"
minWidth="150"/>
<TableColumn text="Order Date"
fx:id="orderDateColumn" minWidth="150"/>
<TableColumn text="Estimated Delivery Date"
fx:id="estimatedDeliveryDateColumn" minWidth="200"/>
<TableColumn text="Delivery Date"
fx:id="deliveryDateColumn" minWidth="200"/>
</columns>
</TableView>
<StackPane fx:id="pageSelectorContainer" style="-fxbackground-color: yellow;"/>
</VBox>
</ScrollPane>
<!-- Fallback and Confirm Dialogs -->
<StackPane fx:id="fallbackContainer" styleClass="page-fallbackcontainer"/>
<StackPane fx:id="confirmUpdateDialogContainer" style="-fxpadding: 64px;"/>
<StackPane fx:id="confirmDeleteDialogContainer" style="-fxpadding: 64px;"/>
<StackPane fx:id="confirmCreateDialogContainer" style="-fxpadding: 64px;"/>
</StackPane>
</VBox>
Clasa SupplierOrdersController
SupplierOrdersController este o clasă de control care gestionează comenzile
furnizorilor. Aceasta oferă metode pentru adăugarea, editarea și ștergerea comenzilor și
actualizează interfața utilizatorului pe baza stării aplicației.
Clasa include, de asemenea, mai multe metode private pentru configurarea
coloanelor tabelului, setarea ascultătorilor, încărcarea ferestrelor de confirmare și
gestionarea răspunsurilor de la serviciile de backend. Aceste metode sunt utilizate
pentru a gestiona starea aplicației și pentru a actualiza interfața utilizatorului în mod
corespunzător.
Clasa are mai multe servicii, controlere și variabile de stare injectate prin
constructorul său. Acestea includ servicii pentru gestionarea comenzilor furnizorilor, un
încărcător de vizualizări comune, un manager de toasturi pentru afișarea mesajelor, un
manager de fallback pentru gestionarea erorilor și parametri de căutare pentru
gestionarea operațiunilor de căutare.
@Inject
public SupplierOrdersController(SupplierOrdersService
supplierOrdersService,
SupplierOrdersWriteService
36
supplierOrdersWriteService,
CommonViewsLoader commonViewsLoader,
SelectComponentLoader
selectComponentLoader,
}
ToastManager toastManager,
FallbackManager fallbackManager,
SearchParams searchParams) {
this.supplierOrdersService = supplierOrdersService;
this.supplierOrdersWriteService = supplierOrdersWriteService;
this.commonViewsLoader = commonViewsLoader;
this.toastManager = toastManager;
this.selectComponentLoader = selectComponentLoader;
this.fallbackManager = fallbackManager;
this.searchParams = searchParams;
Metoda setData este folosită pentru a inițializa controlerul cu datele furnizorului
și modul de căutare. Aceasta configurează bara de instrumente a tabelului, paginația și
încărcătorul de componente selectate, configurează coloanele tabelului, setează
ascultători și încarcă ferestrele de confirmare. Apoi încarcă comenzile furnizorilor în
funcție de modul de căutare.
@Override
public void setData(SearchData<Supplier> searchData) {
this.supplier = searchData.getData();
this.searchMode = searchData.getSearchMode();
searchParams.setItemsPerPage(20);
SearchOptions searchOptions = SearchOptionsConfiguration
.getSearchOptions(Feature.SUPPLIER_ORDER);
commonViewsLoader.loadFallbackManager(fallbackContainer);
tableToolbarController =
commonViewsLoader.initializeTableToolbar(tableToolbarContainer);
tableToolbarController.initialize(new ListHeaderParams
(searchMode, searchParams,
"Supplier Orders", "/img/box-solid.png",
Feature.SUPPLIER_ORDER,
searchOptions.getSortOptions(),
searchOptions.getFilterOptions(),
() -> loadSupplierOrders(searchMode == SearchMode.SECONDARY ?
supplier.getId() : null), null, null));
pageSelectorController =
commonViewsLoader.loadPageSelector(pageSelectorContainer);
selectComponentLoader.initialize();
TableConfigurer.configureTableView(tableView, selectRowColumn);
configureTableColumns();
setUpListeners();
loadConfirmDialogs();
loadSupplierOrders(searchMode == SearchMode.SECONDARY ?
supplier.getId() : null);
}
37
Clasa oferă metode pentru adăugarea unei noi comenzi (addNewOrder),
editarea rândurilor selectate (editSelectedRows) și anularea selecțiilor și editărilor
(cancelSelectionsAndEdit). Aceste metode actualizează interfața utilizatorului și
starea controlerului în mod corespunzător.
Clasa oferă, de asemenea, metode pentru gestionarea creării, actualizării și
ștergerii comenzilor furnizorilor. Aceste metode fac apeluri la serviciile de backend,
gestionează răspunsurile și actualizează interfața utilizatorului în mod corespunzător.
private void handleDeleteOrders(List<SupplierOrder> supplierOrders) {
List<Integer> ordersToRemoveIds = new ArrayList<>();
for (SupplierOrder order : supplierOrders) {
ordersToRemoveIds.add(order.getId());
}
fallbackManager.reset();
fallbackManager.setLoading(true);
supplierOrdersWriteService.deleteSupplierOrderInBulk(ordersToRemoveIds)
.thenApply(this::handleDeleteSupplierOrdersResponse)
.exceptionally(this::handleDeleteSupplierOrdersException);
}
private Result<List<Integer>> handleDeleteSupplierOrdersResponse
(Result<List<Integer>> result) {
Platform.runLater(() -> {
fallbackManager.setLoading(false);
if (result.getError() != null) {
ToastInfo toastInfo = new ToastInfo("Error", "There was an
error deleting the Supplier Orders.", OperationOutcome.ERROR);
toastManager.addToast(toastInfo);
return;
}
tableView.getItems().removeIf(tableData ->
result.getData().contains(tableData.getData().getId()));
closeConfirmDeleteDialog();
isEditMode.set(false);
tableView.refresh();
selectedRowsIndices.clear();
ToastInfo toastInfo = new ToastInfo("Success", "Supplier Orders
deleted successfully.", OperationOutcome.SUCCESS);
toastManager.addToast(toastInfo);
});
return result;
}
private Result<List<Integer>> handleDeleteSupplierOrdersException
(Throwable throwable) {
Platform.runLater(() -> {
ToastInfo toastInfo = new ToastInfo("Error", "There was an error
deleting the Supplier Orders.", OperationOutcome.ERROR);
toastManager.addToast(toastInfo);
});
return new Result<>();
}
38
private void updateUIOnSuccessfulOperation() {
isEditMode.set(false);
newOrderCount = 0;
tableView.getSelectionModel().clearSelection();
tableToolbarController.toggleButtonVisibilityOnCancel();
List<Integer> indicesToClear = new ArrayList<>(selectedRowsIndices);
for (Integer rowIndex : indicesToClear) {
TableData<SupplierOrder> tableRow = tableView.getItems()
.get(rowIndex);
tableRow.setSelected(false);
}
selectRowColumn.setEditable(true);
selectedRowsIndices.clear();
tableView.refresh();
}
Clasa SupplierOrdersServiceImpl
Clasa SupplierOrdersServiceImpl gestionează logica de business legată de
comenzile furnizorilor în aplicație.
Clasa are mai multe servicii și variabile de stare injectate prin constructorul său.
Acestea includ un serviciu de caching pentru stocarea rezultatelor paginate ale
comenzilor furnizorilor, un builder pentru crearea cererilor HTTP, un handler pentru
trimiterea cererilor HTTP și gestionarea răspunsurilor și un manager de tokenuri pentru
gestionarea tokenurilor utilizatorilor.
@Inject
public
SupplierOrdersServiceImpl(CachingService<PaginatedResults<SupplierOrder>>
cachingService,
RequestBuilder requestBuilder,
RequestHandler requestHandler,
TokenManager tokenManager) {
this.cachingService = cachingService;
this.requestBuilder = requestBuilder;
this.requestHandler = requestHandler;
this.tokenManager = tokenManager;
}
Metoda getSupplierOrdersAdvanced este utilizată pentru a obține comenzile
furnizorilor pe baza unor parametri de căutare. Aceasta construiește o cheie cache și o
cerere de citire, și verifică dacă rezultatele sunt deja în memoria cache și nu sunt
expirate. În funcție de searchMode, cacheKey și routeAddress se vor produce
rezultate de căutare diferite. Căutarea este făcută ori în funcție de organizație, fiind
generate toate comenzile furnizorilor din organizația respectivă, ori după furnizor, fiind
generate toate comenzile ale acelui furnizor.
public CompletableFuture<Result<PaginatedResults<SupplierOrder>>>
getSupplierOrdersAdvanced(
Integer entityId,
SearchMode searchMode,
39
) {
SearchParams searchParams
String rootAddress = "http://localhost:8080/api/v1/";
String cacheKey = CacheKeyBuilder.buildAdvancedSearchKey(
"supplier-orders",
searchMode == SearchMode.ORGANIZATION ? "organization" :
"supplier", entityId.toString(),
searchParams);
String routeAddress = rootAddress + cacheKey;
HttpRequest request = requestBuilder.buildReadRequest(routeAddress,
tokenManager.getToken());
if (cachingService.isCached(cacheKey) &&
!cachingService.isStale(cacheKey)) {
return CompletableFuture.completedFuture(new
Result<>(cachingService.get(cacheKey), null, HttpURLConnection.HTTP_OK));
}
return requestHandler.sendRequest(request, new
TypeReference<PaginatedResults<SupplierOrder>>() {}, supplierOrders -> {
cachingService.remove(cacheKey);
cachingService.add(cacheKey, supplierOrders, STALE_TIME);
});
}
Figură 7 - Prezentarea generală a coemnzilor plasate de către furnizori
40
Figură 8 - Prezentarea funcționalității de editare a tabelului
Figură 9 - Fereastra de confirmare înainte de updatarea unor comenzi (urmează ca în interiorul acestei ferestre să fie
afișate modificările aduse)
41
Figură 10 - Afișarea mesajului de succes cat și a notificării toast
Figură 11 - Prezentarea funcționalității de adăugare a unei noi comenzi
42
Figură 12 - Prezentarea funcționalității de ștergere a unei comenzi
Figură 13 - Comanda cu numărul 48 a fost ștearsă cu succes
43
0
You can add this document to your study collection(s)
Sign in Available only to authorized usersYou can add this document to your saved list
Sign in Available only to authorized users(For complaints, use another form )