Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ web_modules/

# Next.js build output
.next
out
/out

# Nuxt.js build / generate output
.nuxt
Expand Down
70 changes: 64 additions & 6 deletions src/main/java/it/robfrank/linklift/Application.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,27 @@
import com.arcadedb.remote.RemoteDatabase;
import io.javalin.Javalin;
import it.robfrank.linklift.adapter.in.web.AuthenticationController;
import it.robfrank.linklift.adapter.in.web.CollectionController;
import it.robfrank.linklift.adapter.in.web.GetContentController;
import it.robfrank.linklift.adapter.in.web.GetRelatedLinksController;
import it.robfrank.linklift.adapter.in.web.ListLinksController;
import it.robfrank.linklift.adapter.in.web.NewLinkController;
import it.robfrank.linklift.adapter.out.content.SimpleTextSummarizer;
import it.robfrank.linklift.adapter.out.event.SimpleEventPublisher;
import it.robfrank.linklift.adapter.out.http.HttpContentDownloader;
import it.robfrank.linklift.adapter.out.http.JsoupContentExtractor;
import it.robfrank.linklift.adapter.out.persitence.*;
import it.robfrank.linklift.adapter.out.persitence.ArcadeCollectionRepository;
import it.robfrank.linklift.adapter.out.persitence.CollectionPersistenceAdapter;
import it.robfrank.linklift.adapter.out.security.BCryptPasswordSecurityAdapter;
import it.robfrank.linklift.adapter.out.security.JwtTokenAdapter;
import it.robfrank.linklift.application.domain.event.*;
import it.robfrank.linklift.application.domain.service.*;
import it.robfrank.linklift.application.domain.service.CreateCollectionService;
import it.robfrank.linklift.application.domain.service.GetRelatedLinksService;
import it.robfrank.linklift.application.port.in.*;
import it.robfrank.linklift.application.port.in.CreateCollectionUseCase;
import it.robfrank.linklift.application.port.in.GetRelatedLinksUseCase;
import it.robfrank.linklift.application.port.out.ContentDownloaderPort;
import it.robfrank.linklift.config.DatabaseInitializer;
import it.robfrank.linklift.config.SecureConfiguration;
Expand Down Expand Up @@ -109,6 +117,15 @@ public static void main(String[] args) {
NewLinkUseCase newLinkUseCase = new NewLinkService(linkPersistenceAdapter, eventPublisher);
ListLinksUseCase listLinksUseCase = new ListLinksService(linkPersistenceAdapter, eventPublisher);

// Initialize Collection and Related Links components
ArcadeCollectionRepository collectionRepository = new ArcadeCollectionRepository(database);
CollectionPersistenceAdapter collectionPersistenceAdapter = new CollectionPersistenceAdapter(collectionRepository);
CreateCollectionUseCase createCollectionUseCase = new CreateCollectionService(collectionPersistenceAdapter);
GetRelatedLinksUseCase getRelatedLinksUseCase = new GetRelatedLinksService(linkPersistenceAdapter);

CollectionController collectionController = new CollectionController(createCollectionUseCase);
GetRelatedLinksController getRelatedLinksController = new GetRelatedLinksController(getRelatedLinksUseCase);

Comment on lines +120 to +128

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This block of initialization logic is duplicated in the start(int port) method (lines 292-300). This extends an existing code duplication issue in the Application class. To improve maintainability and reduce redundancy, consider refactoring the entire dependency injection setup into a private helper method that can be called from both main and start.

// Initialize controllers
NewLinkController newLinkController = new NewLinkController(newLinkUseCase);
ListLinksController listLinksController = new ListLinksController(listLinksUseCase);
Expand All @@ -122,33 +139,63 @@ public static void main(String[] args) {
.withLinkController(newLinkController)
.withListLinksController(listLinksController)
.withGetContentController(getContentController)
.withCollectionController(collectionController)
.withGetRelatedLinksController(getRelatedLinksController)
.build();

app.start(7070);
}

private static void configureEventSubscribers(SimpleEventPublisher eventPublisher, DownloadContentUseCase linkContentExtractorService) {
// Configure event subscribers - this is where different components can subscribe to events
// Configure event subscribers - this is where different components can
// subscribe to events
eventPublisher.subscribe(LinkCreatedEvent.class, event -> {
logger.atInfo().addArgument(() -> event.getLink().url()).addArgument(event.getUserId()).addArgument(event.getTimestamp()).log("Link created: {} for user: {} at {}");
logger
.atInfo()
.addArgument(() -> event.getLink().url())
.addArgument(event.getUserId())
.addArgument(event.getTimestamp())
.log("Link created: {} for user: {} at {}");
linkContentExtractorService.downloadContentAsync(new DownloadContentCommand(event.getLink().id(), event.getLink().url()));
});

eventPublisher.subscribe(LinksQueryEvent.class, event -> {
logger.atInfo().addArgument(() -> event.getQuery().page()).addArgument(() -> event.getQuery().size()).addArgument(event.getResultCount()).addArgument(() -> event.getQuery().userId()).addArgument(event.getTimestamp()).log("Links queried: page={}, size={}, results={} for user: {} at {}");
logger
.atInfo()
.addArgument(() -> event.getQuery().page())
.addArgument(() -> event.getQuery().size())
.addArgument(event.getResultCount())
.addArgument(() -> event.getQuery().userId())
.addArgument(event.getTimestamp())
.log("Links queried: page={}, size={}, results={} for user: {} at {}");
});

// User management events
eventPublisher.subscribe(CreateUserService.UserCreatedEvent.class, event -> {
logger.atInfo().addArgument(() -> event.username()).addArgument(() -> event.email()).addArgument(() -> LocalDateTime.now()).log("User created: {} ({}) at {}");
logger
.atInfo()
.addArgument(() -> event.username())
.addArgument(() -> event.email())
.addArgument(() -> LocalDateTime.now())
.log("User created: {} ({}) at {}");
});

eventPublisher.subscribe(AuthenticationService.UserAuthenticatedEvent.class, event -> {
logger.atInfo().addArgument(() -> event.username()).addArgument(() -> event.ipAddress()).addArgument(() -> event.timestamp()).log("User authenticated: {} from {} at {}");
logger
.atInfo()
.addArgument(() -> event.username())
.addArgument(() -> event.ipAddress())
.addArgument(() -> event.timestamp())
.log("User authenticated: {} from {} at {}");
});

eventPublisher.subscribe(AuthenticationService.TokenRefreshedEvent.class, event -> {
logger.atInfo().addArgument(() -> event.username()).addArgument(() -> event.ipAddress()).addArgument(() -> event.timestamp()).log("Token refreshed for user: {} from {} at {}");
logger
.atInfo()
.addArgument(() -> event.username())
.addArgument(() -> event.ipAddress())
.addArgument(() -> event.timestamp())
.log("Token refreshed for user: {} from {} at {}");
});

// Content download events
Expand Down Expand Up @@ -239,6 +286,15 @@ public Javalin start(int port) {
NewLinkUseCase newLinkUseCase = new NewLinkService(linkPersistenceAdapter, eventPublisher);
ListLinksUseCase listLinksUseCase = new ListLinksService(linkPersistenceAdapter, eventPublisher);

// Initialize Collection and Related Links components
ArcadeCollectionRepository collectionRepository = new ArcadeCollectionRepository(database);
CollectionPersistenceAdapter collectionPersistenceAdapter = new CollectionPersistenceAdapter(collectionRepository);
CreateCollectionUseCase createCollectionUseCase = new CreateCollectionService(collectionPersistenceAdapter);
GetRelatedLinksUseCase getRelatedLinksUseCase = new GetRelatedLinksService(linkPersistenceAdapter);

CollectionController collectionController = new CollectionController(createCollectionUseCase);
GetRelatedLinksController getRelatedLinksController = new GetRelatedLinksController(getRelatedLinksUseCase);

// Initialize controllers
NewLinkController newLinkController = new NewLinkController(newLinkUseCase);
ListLinksController listLinksController = new ListLinksController(listLinksUseCase);
Expand All @@ -252,6 +308,8 @@ public Javalin start(int port) {
.withLinkController(newLinkController)
.withListLinksController(listLinksController)
.withGetContentController(getContentController)
.withCollectionController(collectionController)
.withGetRelatedLinksController(getRelatedLinksController)
.build();

app.start(port);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package it.robfrank.linklift.adapter.in.web;

import io.javalin.http.Context;
import it.robfrank.linklift.adapter.in.web.error.ErrorResponse;
import it.robfrank.linklift.application.domain.model.Collection;
import it.robfrank.linklift.application.port.in.CreateCollectionCommand;
import it.robfrank.linklift.application.port.in.CreateCollectionUseCase;
import org.jspecify.annotations.NonNull;

public class CollectionController {

private final CreateCollectionUseCase createCollectionUseCase;

public CollectionController(CreateCollectionUseCase createCollectionUseCase) {
this.createCollectionUseCase = createCollectionUseCase;
}

public void createCollection(@NonNull Context ctx) {
var request = ctx.bodyAsClass(CreateCollectionRequest.class);
var userId = ctx.attribute("userId");

if (userId == null) {
ctx.status(401).json(ErrorResponse.builder().status(401).message("Unauthorized").build());
return;
}

var command = new CreateCollectionCommand(request.name(), request.description(), userId.toString(), request.query());

var collection = createCollectionUseCase.createCollection(command);
ctx.status(201).json(new CollectionResponse(collection));
}

public record CreateCollectionRequest(String name, String description, String query) {}

public record CollectionResponse(Collection data) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package it.robfrank.linklift.adapter.in.web;

import io.javalin.http.Context;
import it.robfrank.linklift.adapter.in.web.error.ErrorResponse;
import it.robfrank.linklift.application.domain.model.Link;
import it.robfrank.linklift.application.port.in.GetRelatedLinksUseCase;
import java.util.List;
import org.jspecify.annotations.NonNull;

public class GetRelatedLinksController {

private final GetRelatedLinksUseCase getRelatedLinksUseCase;

public GetRelatedLinksController(GetRelatedLinksUseCase getRelatedLinksUseCase) {
this.getRelatedLinksUseCase = getRelatedLinksUseCase;
}

public void getRelatedLinks(@NonNull Context ctx) {
var linkId = ctx.pathParam("linkId");
var userId = ctx.attribute("userId");

if (userId == null) {
ctx.status(401).json(ErrorResponse.builder().status(401).message("Unauthorized").build());
return;
}
Comment on lines +22 to +25

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This userId == null check is redundant. The RequireAuthentication handler is configured for this endpoint in WebBuilder, and it already ensures that an authenticated user exists. If userId were null, the RequireAuthentication handler would have already thrown an AuthenticationException, preventing this code from being reached. Removing this check will make the code cleaner and align better with the framework's security mechanisms.


var links = getRelatedLinksUseCase.getRelatedLinks(linkId, userId.toString());
ctx.json(new RelatedLinksResponse(links));
}

public record RelatedLinksResponse(List<Link> data) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ public class SimpleTextSummarizer implements ContentSummarizerPort {
// Calculate word frequencies
Map<String, Integer> wordFrequencies = new HashMap<>();
Arrays.stream(textContent.toLowerCase().split("\\W+"))
.filter(w -> w.length() > 3) // Filter short words
.forEach(w -> wordFrequencies.merge(w, 1, Integer::sum));
.filter(w -> w.length() > 3) // Filter short words
.forEach(w -> wordFrequencies.merge(w, 1, Integer::sum));

// Score sentences
Map<String, Double> sentenceScores = new HashMap<>();
Expand All @@ -55,11 +55,13 @@ public class SimpleTextSummarizer implements ContentSummarizerPort {
}

// Select top sentences
List<String> topSentences = sentenceScores.entrySet().stream()
.sorted(Map.Entry.<String, Double>comparingByValue().reversed())
.limit(5) // Take top 5 sentences
.map(Map.Entry::getKey)
.collect(Collectors.toList());
List<String> topSentences = sentenceScores
.entrySet()
.stream()
.sorted(Map.Entry.<String, Double>comparingByValue().reversed())
.limit(5) // Take top 5 sentences
.map(Map.Entry::getKey)
.collect(Collectors.toList());

// Reorder sentences as they appear in the text
StringBuilder summary = new StringBuilder();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,36 +8,36 @@

public class SimpleEventPublisher implements DomainEventPublisher {

private final List<DomainEventSubscriber<?>> subscribers = new CopyOnWriteArrayList<>();
private final List<DomainEventSubscriber<?>> subscribers = new CopyOnWriteArrayList<>();

@Override
public void publish(DomainEvent event) {
subscribers.forEach(subscriber -> subscriber.handleEvent(event));
}
@Override
public void publish(DomainEvent event) {
subscribers.forEach(subscriber -> subscriber.handleEvent(event));
}

public <T extends DomainEvent> void subscribe(Class<T> eventType, Consumer<T> eventHandler) {
subscribers.add(new DomainEventSubscriber<>(eventType, eventHandler));
}
public <T extends DomainEvent> void subscribe(Class<T> eventType, Consumer<T> eventHandler) {
subscribers.add(new DomainEventSubscriber<>(eventType, eventHandler));
}

public void clear() {
subscribers.clear();
}
public void clear() {
subscribers.clear();
}

private static class DomainEventSubscriber<T extends DomainEvent> {
private static class DomainEventSubscriber<T extends DomainEvent> {

private final Class<T> eventType;
private final Consumer<T> eventHandler;
private final Class<T> eventType;
private final Consumer<T> eventHandler;

public DomainEventSubscriber(Class<T> eventType, Consumer<T> eventHandler) {
this.eventType = eventType;
this.eventHandler = eventHandler;
}
public DomainEventSubscriber(Class<T> eventType, Consumer<T> eventHandler) {
this.eventType = eventType;
this.eventHandler = eventHandler;
}

@SuppressWarnings("unchecked")
public void handleEvent(DomainEvent event) {
if (eventType.isInstance(event)) {
eventHandler.accept((T) event);
}
}
@SuppressWarnings("unchecked")
public void handleEvent(DomainEvent event) {
if (eventType.isInstance(event)) {
eventHandler.accept((T) event);
}
}
}
}
Loading
Loading