Skip to content

Commit

Permalink
#32: Client detection (#57)
Browse files Browse the repository at this point in the history
  • Loading branch information
shurshurik authored Sep 27, 2024
1 parent b47d69b commit 298f4e3
Show file tree
Hide file tree
Showing 10 changed files with 234 additions and 41 deletions.
Original file line number Diff line number Diff line change
@@ -1,17 +1,7 @@
package vpnrouter.api.client;

public interface ClientDetectionService {
boolean isInProgress();

void detectAndSave(CompletionListener completionListener);

interface CompletionListener {

void onAlreadyRunning();

void onNewClientsNotFound();

void onNewClientsFound(int newClientsCount);

void onFailure(Exception exception);
}
void detectAndSave();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package vpnrouter.api.event.concrete.client;

import lombok.Data;
import vpnrouter.api.event.Event;

@Data
public class ClientDetectionClientsFoundEvent implements Event {
private final int newClientsCount;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package vpnrouter.api.event.concrete.client;

import vpnrouter.api.event.Event;

public class ClientDetectionClientsNotFoundEvent implements Event {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package vpnrouter.api.event.concrete.client;

import lombok.Data;
import vpnrouter.api.event.Event;

@Data
public class ClientDetectionFailureEvent implements Event {
private final Exception exception;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package vpnrouter.api.event.concrete.client;

import vpnrouter.api.event.Event;

public class ClientDetectionStartedEvent implements Event {
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,6 @@ public class ClientDetectionScheduler {

@Scheduled(fixedRateString = "PT30m")
public void detectAndSave() {
clientDetectionService.detectAndSave(new ClientDetectionService.CompletionListener() {
@Override
public void onAlreadyRunning() {
}

@Override
public void onNewClientsNotFound() {
}

@Override
public void onNewClientsFound(int newClientsCount) {
}

@Override
public void onFailure(Exception exception) {
}
});
clientDetectionService.detectAndSave();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
import org.springframework.stereotype.Service;
import vpnrouter.api.client.ClientDetectionService;
import vpnrouter.api.event.concrete.GeneralUpdateEvent;
import vpnrouter.api.event.concrete.client.ClientDetectionClientsFoundEvent;
import vpnrouter.api.event.concrete.client.ClientDetectionClientsNotFoundEvent;
import vpnrouter.api.event.concrete.client.ClientDetectionFailureEvent;
import vpnrouter.api.event.concrete.client.ClientDetectionStartedEvent;
import vpnrouter.core.service.client.Client;
import vpnrouter.core.service.client.ClientRepository;
import vpnrouter.core.service.event.EventPublisher;
Expand All @@ -29,26 +33,31 @@ public class ClientDetectionServiceImpl implements ClientDetectionService {
private final EventPublisher eventPublisher;

@Override
public void detectAndSave(CompletionListener completionListener) {
public boolean isInProgress() {
return detectionInProgress.get();
}

@Override
public void detectAndSave() {
if (detectionInProgress.compareAndSet(false, true)) {
executor.submit(() -> {
try {
executeTask(completionListener);
executeTask();
} catch (Exception e) {
log.warn("Detection failure: {}", e.getMessage(), e);
completionListener.onFailure(e);
eventPublisher.publish(new ClientDetectionFailureEvent(e));
} finally {
detectionInProgress.set(false);
}
});
log.info("Detection task submitted");
} else {
log.info("Detection already running");
completionListener.onAlreadyRunning();
}
}

private void executeTask(CompletionListener completionListener) {
private void executeTask() {
eventPublisher.publish(new ClientDetectionStartedEvent());
log.info("Detecting and saving clients");
var detectedIpAddresses = clientDetector.detectIpAddresses();
var existingIpAddresses = clientRepository.findAll().stream()
Expand All @@ -63,10 +72,10 @@ private void executeTask(CompletionListener completionListener) {
}
if (newClientsCount == 0) {
log.info("Detection completed: new clients not found");
completionListener.onNewClientsNotFound();
eventPublisher.publish(new ClientDetectionClientsNotFoundEvent());
} else {
log.info("Detection completed: {} new clients found", newClientsCount);
completionListener.onNewClientsFound(newClientsCount);
eventPublisher.publish(new ClientDetectionClientsFoundEvent(newClientsCount));
eventPublisher.publish(new GeneralUpdateEvent());
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@
import com.vaadin.flow.component.applayout.AppLayout;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.dependency.CssImport;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.spring.annotation.UIScope;
import lombok.RequiredArgsConstructor;
import vpnrouter.web.ui.AddClientPage;
import vpnrouter.web.ui.clients.detection.ClientDetectionComponentFactory;

@UIScope
@Route("")
Expand All @@ -17,19 +20,24 @@
public class ClientsPage extends AppLayout {

private final ClientsGridFactory clientsGridFactory;
private final ClientDetectionComponentFactory clientDetectionComponentFactory;

@Override
public void onAttach(AttachEvent event) {
var grid = clientsGridFactory.build();
var layout = new VerticalLayout(grid, buildAddClientButton());
var clientDetectionComponent = clientDetectionComponentFactory.build();
var layout = new VerticalLayout(
new HorizontalLayout(buildAddClientButton(), clientDetectionComponent.getStartButton()),
clientDetectionComponent.getProgressBar(),
grid
);
layout.setHeightFull();
setContent(layout);
}

private Button buildAddClientButton() {
return new Button(
"Add client",
event -> getUI().ifPresent(ui -> ui.navigate(AddClientPage.class))
);
var button = new Button(VaadinIcon.PLUS.create());
button.addClickListener(event -> getUI().ifPresent(ui -> ui.navigate(AddClientPage.class)));
return button;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package vpnrouter.web.ui.clients.detection;

import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.progressbar.ProgressBar;
import lombok.Getter;

@Getter
public class ClientDetectionComponent {

private final Button startButton;
private final ProgressBar progressBar;

public ClientDetectionComponent(State initialState, Runnable onDetectionStartListener) {
this.startButton = buildStartButton(onDetectionStartListener);
this.progressBar = buildProgressBar();
setState(initialState);
}

private Button buildStartButton(Runnable onDetectionStartListener) {
var clientDetectionButton = new Button(VaadinIcon.REFRESH.create());
clientDetectionButton.addClickListener(event -> onDetectionStartListener.run());
return clientDetectionButton;
}

private ProgressBar buildProgressBar() {
var progressBar = new ProgressBar();
progressBar.setIndeterminate(true);
return progressBar;
}

public void setState(State state) {
startButton.setEnabled(state == State.IDLE);
progressBar.setVisible(state == State.IN_PROGRESS);
}

public enum State {
IDLE,
IN_PROGRESS,
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package vpnrouter.web.ui.clients.detection;

import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.UIDetachedException;
import com.vaadin.flow.component.notification.Notification;
import lombok.EqualsAndHashCode;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import vpnrouter.api.client.ClientDetectionService;
import vpnrouter.api.event.EventSubscriber;
import vpnrouter.api.event.EventSubscriberRegistry;
import vpnrouter.api.event.concrete.client.ClientDetectionClientsFoundEvent;
import vpnrouter.api.event.concrete.client.ClientDetectionClientsNotFoundEvent;
import vpnrouter.api.event.concrete.client.ClientDetectionFailureEvent;
import vpnrouter.api.event.concrete.client.ClientDetectionStartedEvent;
import vpnrouter.web.ui.clients.detection.ClientDetectionComponent.State;

@Component
@RequiredArgsConstructor
public class ClientDetectionComponentFactory {

private final ClientDetectionService clientDetectionService;
private final EventSubscriberRegistry eventSubscriberRegistry;

public ClientDetectionComponent build() {
var initialState = clientDetectionService.isInProgress() ? State.IN_PROGRESS : State.IDLE;
var clientDetectionComponent = new ClientDetectionComponent(initialState, clientDetectionService::detectAndSave);
registerHandlers(clientDetectionComponent);
return clientDetectionComponent;
}

private void registerHandlers(ClientDetectionComponent clientDetectionComponent) {
eventSubscriberRegistry.addSubscriber(
ClientDetectionStartedEvent.class,
new DetectionStartedEventHandler(UI.getCurrent(), clientDetectionComponent)
);
eventSubscriberRegistry.addSubscriber(
ClientDetectionClientsFoundEvent.class,
new ClientsFoundEventHandler(UI.getCurrent(), clientDetectionComponent)
);
eventSubscriberRegistry.addSubscriber(
ClientDetectionClientsNotFoundEvent.class,
new ClientsNotFoundEventHandler(UI.getCurrent(), clientDetectionComponent)
);
eventSubscriberRegistry.addSubscriber(
ClientDetectionFailureEvent.class,
new DetectionFailureEventHandler(UI.getCurrent(), clientDetectionComponent)
);
}

@RequiredArgsConstructor
@EqualsAndHashCode(of = "ui")
private class DetectionStartedEventHandler implements EventSubscriber<ClientDetectionStartedEvent> {

private final UI ui;
private final ClientDetectionComponent clientDetectionComponent;

@Override
public void receive(ClientDetectionStartedEvent event) {
try {
ui.access(() -> {
clientDetectionComponent.setState(State.IN_PROGRESS);
});
} catch (UIDetachedException e) {
eventSubscriberRegistry.removeSubscriber(ClientDetectionStartedEvent.class, this);
}
}
}

@RequiredArgsConstructor
@EqualsAndHashCode(of = "ui")
private class ClientsFoundEventHandler implements EventSubscriber<ClientDetectionClientsFoundEvent> {

private final UI ui;
private final ClientDetectionComponent clientDetectionComponent;

@Override
public void receive(ClientDetectionClientsFoundEvent event) {
try {
var newClientsCount = event.getNewClientsCount();
ui.access(() -> {
Notification.show("%d new clients found".formatted(newClientsCount));
clientDetectionComponent.setState(State.IDLE);
});
} catch (UIDetachedException e) {
eventSubscriberRegistry.removeSubscriber(ClientDetectionClientsFoundEvent.class, this);
}
}
}

@RequiredArgsConstructor
@EqualsAndHashCode(of = "ui")
private class ClientsNotFoundEventHandler implements EventSubscriber<ClientDetectionClientsNotFoundEvent> {

private final UI ui;
private final ClientDetectionComponent clientDetectionComponent;

@Override
public void receive(ClientDetectionClientsNotFoundEvent event) {
try {
ui.access(() -> {
Notification.show("No new clients found");
clientDetectionComponent.setState(State.IDLE);
});
} catch (UIDetachedException e) {
eventSubscriberRegistry.removeSubscriber(ClientDetectionClientsNotFoundEvent.class, this);
}
}
}

@RequiredArgsConstructor
@EqualsAndHashCode(of = "ui")
private class DetectionFailureEventHandler implements EventSubscriber<ClientDetectionFailureEvent> {

private final UI ui;
private final ClientDetectionComponent clientDetectionComponent;

@Override
public void receive(ClientDetectionFailureEvent event) {
try {
var exception = event.getException();
ui.access(() -> {
clientDetectionComponent.setState(State.IDLE);
throw new RuntimeException(exception);
});
} catch (UIDetachedException e) {
eventSubscriberRegistry.removeSubscriber(ClientDetectionFailureEvent.class, this);
}
}
}
}

0 comments on commit 298f4e3

Please sign in to comment.