Skip to content
Draft
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
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@


<dependencies>
<dependency>
<groupId>com.google.inject</groupId>
<artifactId>guice</artifactId>
<version>7.0.0</version>
</dependency>

<dependency>
<groupId>pl.edu.icm</groupId>
<artifactId>cermine</artifactId>
Expand Down
10 changes: 6 additions & 4 deletions src/main/java/com/cerca/App.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import com.cerca.controller.MainController;
import com.cerca.view.MainView;
import com.google.inject.Guice;
import com.google.inject.Injector;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;
Expand All @@ -10,13 +12,13 @@
public class App extends Application {
@Override
public void start(Stage stage) {
Injector injector = Guice.createInjector(new AppModule());

// Initialize MVC Components
MainView view = new MainView();
new MainController(view);

MainView view = injector.getInstance(MainView.class);
injector.getInstance(MainController.class);

var iconStream = getClass().getResourceAsStream("/images/app_icon.png");


if (iconStream != null) {
Image icon = new Image(iconStream);
Expand Down
25 changes: 25 additions & 0 deletions src/main/java/com/cerca/AppModule.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.cerca;

import com.cerca.service.ConfigService;
import com.cerca.service.extraction.*;
import com.google.inject.AbstractModule;
import com.google.inject.Provides;
import com.google.inject.Singleton;
import com.google.inject.name.Named;

/**
* Dependency Injection Container
*/
public class AppModule extends AbstractModule {
@Override
protected void configure() {
bind(PdfExtractor.class).to(CerminePdfExtractor.class).in(Singleton.class);
bind(TextExtractor.class).to(GrobidTextExtractor.class).in(Singleton.class);
}

@Provides
@Named("USER_EMAIL")
public String provideUserEmail(ConfigService configService) {
return configService.getProperty("USER_EMAIL");
}
}
68 changes: 40 additions & 28 deletions src/main/java/com/cerca/controller/MainController.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,19 @@
import java.util.concurrent.CompletionException;

import com.cerca.model.ReferenceItem;
import com.cerca.service.CermineService;
import com.cerca.service.extraction.*;
import com.cerca.service.ConfigService;
import com.cerca.service.CrossrefService;
import com.cerca.service.CsvService;
import com.cerca.service.lookup.CrossrefService;
import com.cerca.service.report.CsvService;
import com.cerca.service.LogService;
import com.cerca.service.OpenAlexService;
import com.cerca.service.ReportService;
import com.cerca.service.SemanticScholarService;
import com.cerca.service.ZenodoService;
import com.cerca.utils.ReferenceParser;
import com.cerca.service.lookup.OpenAlexService;
import com.cerca.service.report.ReportService;
import com.cerca.service.lookup.SemanticScholarService;
import com.cerca.service.lookup.ZenodoService;
import com.cerca.view.MainView;

import com.google.inject.Inject;
import com.google.inject.Singleton;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
Expand All @@ -48,11 +49,13 @@
* @author Lidiany Cerqueira
*
*/
@Singleton
public class MainController {

private final MainView view;
private final ObservableList<ReferenceItem> data;
private final CermineService cermineService;
private final PdfExtractor pdfExtractor;
private final TextExtractor textExtractor;
private final CrossrefService crossrefService;
private final CsvService csvService;
private final ReportService reportService;
Expand All @@ -63,22 +66,32 @@ public class MainController {
private final OpenAlexService openAlexService;
private final ConfigService configService;

public MainController(MainView view) {
@Inject
public MainController(MainView view,
LogService logService,
ConfigService configService,
PdfExtractor pdfExtractor,
TextExtractor textExtractor,
CrossrefService crossrefService,
OpenAlexService openAlexService,
ZenodoService zenodoService,
SemanticScholarService semScholarService,
CsvService csvService,
ReportService reportService) {
this.view = view;
this.data = FXCollections.observableArrayList();
this.cermineService = new CermineService();
this.csvService = new CsvService();
this.reportService = new ReportService();
this.logService = new LogService();
this.crossrefService = new CrossrefService(logService);
this.zenodoService = new ZenodoService(logService);
this.openAlexService = new OpenAlexService(logService);

this.semScholarService = new SemanticScholarService(logService);
this.configService = new ConfigService(logService);
this.semScholarService.setApiKey(configService.getProperty("SEMANTIC_SCHOLAR_API_KEY"));
this.crossrefService.setEmail(configService.getProperty("USER_EMAIL"));
this.openAlexService.setEmail(configService.getProperty("USER_EMAIL"));
this.pdfExtractor = pdfExtractor;
this.textExtractor = textExtractor;
this.csvService = csvService;
this.reportService = reportService;
this.logService = logService;
this.crossrefService = crossrefService;
this.zenodoService = zenodoService;
this.openAlexService = openAlexService;

this.semScholarService = semScholarService;
this.configService = configService;

view.getTable().setItems(data);

setupDragAndDrop();
Expand Down Expand Up @@ -135,7 +148,7 @@ private void processPdf(File file) {

CompletableFuture.supplyAsync(() -> {
try {
return cermineService.extractReferences(file);
return pdfExtractor.extractReferences(file);
} catch (Exception e) {

throw new CompletionException(e);
Expand Down Expand Up @@ -415,9 +428,9 @@ private void loadManualReferences(String text) {
if (line.trim().length() < 5)
continue;

ReferenceParser.ParsedData parsedData = ReferenceParser.parse(line);
ParsedData parsedData = textExtractor.parse(line);

ReferenceItem item = new ReferenceItem(idCounter++, "WAITING", parsedData.authors, parsedData.title, line,
ReferenceItem item = new ReferenceItem(idCounter++, "WAITING", parsedData.authors(), parsedData.title(), line,
"");

data.add(item);
Expand All @@ -428,7 +441,7 @@ private void loadManualReferences(String text) {
}

public void openSettingsDialog() {
String currentKey = this.semScholarService.getApiKey();
String currentKey = configService.getProperty("SEMANTIC_SCHOLAR_API_KEY");

Dialog<String> dialog = new Dialog<>();
dialog.setTitle("CERCA Settings");
Expand Down Expand Up @@ -487,7 +500,6 @@ public void openSettingsDialog() {

result.ifPresent(newKey -> {
configService.setProperty("SEMANTIC_SCHOLAR_API_KEY", newKey);
this.semScholarService.setApiKey(newKey);
logService.log("INFO", "API Key updated successfully!");
});
}
Expand Down
8 changes: 6 additions & 2 deletions src/main/java/com/cerca/service/ConfigService.java
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
package com.cerca.service;

import java.io.File;
import com.google.inject.Inject;
import com.google.inject.Singleton;

import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Properties;

@Singleton
public class ConfigService {

private static final Path CONFIG_FILE = Paths.get("config.properties");
private final LogService logger;
private Properties properties;


@Inject
public ConfigService(LogService logger) {
this.logger = logger;
this.properties = new Properties();
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/com/cerca/service/LogService.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.cerca.service;


import com.google.inject.Singleton;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
Expand All @@ -14,6 +16,7 @@
*
* @author Lidiany Cerqueira
*/
@Singleton
public class LogService {

private static final Path LOG_FILE = Paths.get("cerca_audit.log");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.cerca.service;
package com.cerca.service.extraction;

import com.cerca.model.ReferenceItem;
import pl.edu.icm.cermine.ContentExtractor;
Expand All @@ -23,7 +23,7 @@
* Privacy note: PDF files are processed locally.
* @author Lidiany Cerqueira
*/
public class CermineService {
public class CerminePdfExtractor implements PdfExtractor {

public List<ReferenceItem> extractReferences(File pdfFile) throws Exception {
List<ReferenceItem> results = new ArrayList<>();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* @author Lidiany Cerqueira
*/
package com.cerca.utils;
package com.cerca.service.extraction;

import pl.edu.icm.cermine.bibref.CRFBibReferenceParser;
import pl.edu.icm.cermine.bibref.model.BibEntry;
Expand All @@ -10,29 +10,20 @@

import java.util.List;

public class ReferenceParser {
public class CermineTextExtractor implements TextExtractor {

private static CRFBibReferenceParser parser;
private CRFBibReferenceParser parser;

static {
public CermineTextExtractor() {
try {
parser = CRFBibReferenceParser.getInstance();
this.parser = CRFBibReferenceParser.getInstance();
} catch (AnalysisException e) {
System.err.println("CERMINE Model Error: " + e.getMessage());
}
}

public static class ParsedData {
public String authors;
public String title;

public ParsedData(String authors, String title) {
this.authors = authors;
this.title = title;
}
};
}

public static ParsedData parse(String rawReference) {
@Override
public ParsedData parse(String rawReference) {
if (rawReference == null || rawReference.trim().isEmpty()) {
return new ParsedData("Unknown", "Unknown");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package com.cerca.service.extraction;

import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathFactory;
import java.io.IOException;
import java.io.StringReader;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

/**
* Call GROBID to extract a reference from a citation.
* <p>
* To run GROBID in a Docker container, call
* <code>docker run --rm -p 8070:8070 grobid/grobid:0.8.2-full</code>.
*/
public class GrobidTextExtractor implements TextExtractor {
private static final String GROBID_URL = "http://localhost:8070";

private final HttpClient httpClient;

public GrobidTextExtractor() {
this.httpClient = HttpClient.newHttpClient();
}

@Override
public ParsedData parse(String rawReference) {
try {
String responseXml = callGrobid(rawReference);
return parseTei(responseXml, rawReference);
} catch (Exception e) {
throw new RuntimeException("Failed to parse reference with GROBID", e);
}
}

private String callGrobid(String citation) throws IOException, InterruptedException {
String body = "citations=" + URLEncoder.encode(citation, StandardCharsets.UTF_8);

HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(GROBID_URL + "/api/processCitation"))
.header("Content-Type", "application/x-www-form-urlencoded")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();

HttpResponse<String> response =
httpClient.send(request, HttpResponse.BodyHandlers.ofString());

if (response.statusCode() != 200) {
throw new RuntimeException("GROBID returned status " + response.statusCode());
}

return response.body();
}

private ParsedData parseTei(String xml, String rawReference) throws Exception {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(false);

Document doc = factory.newDocumentBuilder()
.parse(new InputSource(new StringReader(xml)));

var xpath = XPathFactory.newInstance().newXPath();

// --- Authors ---
NodeList surnameNodes = (NodeList) xpath.evaluate("//author/persName/surname", doc, XPathConstants.NODESET);

List<String> authors = new ArrayList<>();
for (int i = 0; i < surnameNodes.getLength(); i++) {
authors.add(surnameNodes.item(i).getTextContent().trim());
}

String authorsJoined = String.join(", ", authors);

// --- Title (analytic preferred) ---
String title = xpath.evaluate("//analytic/title/text()", doc).trim();

if (title.isEmpty()) {
title = xpath.evaluate("//monogr/title/text()", doc).trim();
}

// --- DOI ---
String doi = xpath.evaluate("//idno[@type='DOI']/text()", doc).trim();

return new ParsedData(authorsJoined, title);
}
}
4 changes: 4 additions & 0 deletions src/main/java/com/cerca/service/extraction/ParsedData.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.cerca.service.extraction;

public record ParsedData (String authors, String title) {
}
Loading