T1: Deduplicator compile fix + unit tests [checkpoint]

This commit is contained in:
Artur Kruszewski
2026-07-07 22:20:08 +02:00
commit 38a93d0c4d
114 changed files with 6942 additions and 0 deletions
@@ -0,0 +1,216 @@
package com.developx.szpitale.load;
import com.developx.szpitale.model.*;
import org.neo4j.driver.Driver;
import org.neo4j.driver.Session;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
import java.util.stream.Collectors;
public class GraphLoader {
private static final Logger log = LoggerFactory.getLogger(GraphLoader.class);
private static final int BATCH_SIZE = 1000;
private final Driver driver;
public GraphLoader(Driver driver) {
this.driver = driver;
}
public LoadStats load(List<CanonicalDataset> datasets) {
LoadStats stats = new LoadStats();
try (Session session = driver.session()) {
for (CanonicalDataset dataset : datasets) {
stats.addVoivodeship(dataset.voivodeship());
loadHospitals(session, dataset.hospitals(), stats);
loadPeople(session, dataset.people(), stats);
loadRoles(session, dataset.roles(), stats);
loadAffiliations(session, dataset.affiliations(), stats);
loadMandates(session, dataset.mandates(), stats);
}
}
return stats;
}
private void loadHospitals(Session session, List<Hospital> hospitals, LoadStats stats) {
List<Map<String, Object>> batch = new ArrayList<>();
for (Hospital h : hospitals) {
batch.add(Map.of(
"id", h.id(),
"name", h.name(),
"shortName", h.shortName(),
"city", h.city(),
"voivodeship", h.voivodeship(),
"legalForm", h.legalForm() != null ? h.legalForm().name() : null,
"foundingBody", h.foundingBody(),
"supervisoryBodyType", h.supervisoryBodyType() != null ? h.supervisoryBodyType().name() : null,
"website", h.website(),
"sourceUrls", h.sourceUrls()
));
}
for (int i = 0; i < batch.size(); i += BATCH_SIZE) {
List<Map<String, Object>> subList = batch.subList(i, Math.min(i + BATCH_SIZE, batch.size()));
String cypher = "UNWIND $batch AS h " +
"MERGE (hospital:Hospital {id: h.id}) " +
"SET hospital += {name:h.name, shortName:h.shortName, city:h.city, " +
" voivodeship:h.voivodeship, legalForm:h.legalForm, " +
" foundingBody:h.foundingBody, supervisoryBodyType:h.supervisoryBodyType, " +
" website:h.website} " +
"MERGE (v:Voivodeship {slug: h.voivodeship}) " +
"MERGE (hospital)-[:W_WOJEWODZTWIE]->(v)";
session.run(cypher, Map.of("batch", subList));
stats.addHospitals(subList.size());
}
}
private void loadPeople(Session session, List<Person> people, LoadStats stats) {
List<Map<String, Object>> batch = new ArrayList<>();
for (Person p : people) {
batch.add(Map.of(
"id", p.id(),
"fullName", p.fullName(),
"displayName", p.displayName(),
"titles", p.titles() != null ? p.titles() : Collections.emptyList(),
"sourceUrls", p.sourceUrls()
));
}
for (int i = 0; i < batch.size(); i += BATCH_SIZE) {
List<Map<String, Object>> subList = batch.subList(i, Math.min(i + BATCH_SIZE, batch.size()));
String cypher = "UNWIND $batch AS p " +
"MERGE (person:Person {id: p.id}) " +
"SET person += {fullName:p.fullName, displayName:p.displayName, " +
" titles:p.titles, sourceUrls:p.sourceUrls}";
session.run(cypher, Map.of("batch", subList));
stats.addPeople(subList.size());
}
}
private void loadRoles(Session session, List<Role> roles, LoadStats stats) {
Map<String, List<Role>> byRelType = new LinkedHashMap<>();
for (Role r : roles) {
String relType = roleTypeToRelName(r);
byRelType.computeIfAbsent(relType, k -> new ArrayList<>()).add(r);
}
for (Map.Entry<String, List<Role>> entry : byRelType.entrySet()) {
String relType = entry.getKey();
List<Role> items = entry.getValue();
for (int i = 0; i < items.size(); i += BATCH_SIZE) {
List<Role> subList = items.subList(i, Math.min(i + BATCH_SIZE, items.size()));
String cypher = "UNWIND $batch AS r " +
"MATCH (h:Hospital {id: r.hospitalId}) " +
"MATCH (p:Person {id: r.personId}) " +
"MERGE (h)-[rrel:`" + escapeBacktick(relType) + "`]->(p) " +
"SET rrel += {roleType: r.roleType, roleLabel: r.roleLabel, " +
" organ: r.organ, status: r.status, " +
" validFrom: r.validFrom, validTo: r.validTo, note: r.note}";
session.run(cypher, Map.of("batch", subList.stream()
.map(r -> Map.of(
"hospitalId", r.hospitalId(),
"personId", r.personId(),
"roleType", r.roleType().name(),
"roleLabel", r.roleLabel(),
"organ", r.organ().name(),
"status", r.status().name(),
"validFrom", r.validFrom(),
"validTo", r.validTo(),
"note", r.note()
))
.collect(Collectors.toList())));
stats.addRoles(subList.size());
}
}
}
private void loadAffiliations(Session session, List<Affiliation> affiliations, LoadStats stats) {
for (int i = 0; i < affiliations.size(); i += BATCH_SIZE) {
List<Affiliation> subList = affiliations.subList(
i, Math.min(i + BATCH_SIZE, affiliations.size()));
String cypher = "UNWIND $batch AS a " +
"MATCH (p:Person {id: a.personId}) " +
"MERGE (party:Party {name: a.name}) SET party.type = a.type " +
"MERGE (p)-[m:CZLONEK_PARTII]->(party) " +
"SET m += {confidence: a.confidence, note: a.note, sourceUrls: a.sourceUrls}";
session.run(cypher, Map.of("batch", subList.stream()
.map(a -> Map.of(
"personId", a.personId(),
"name", a.name(),
"type", a.type().name(),
"confidence", a.confidence().name(),
"note", a.note(),
"sourceUrls", a.sourceUrls()
))
.collect(Collectors.toList())));
stats.addAffiliations(subList.size());
}
}
private void loadMandates(Session session, List<Mandate> mandates, LoadStats stats) {
if (mandates.isEmpty()) return;
for (int i = 0; i < mandates.size(); i += BATCH_SIZE) {
List<Mandate> subList = mandates.subList(
i, Math.min(i + BATCH_SIZE, mandates.size()));
String cypher = "UNWIND $batch AS m " +
"MATCH (p:Person {id: m.personId}) " +
"MERGE (g:GovBody {name: m.body}) " +
" SET g.type = m.mandateType, g.voivodeship = m.voivodeship, g.term = m.term " +
"MERGE (p)-[:PELNI_MANDAT {mandateType: m.mandateType, term: m.term}]->(g)";
session.run(cypher, Map.of("batch", subList.stream()
.map(m -> Map.of(
"personId", m.personId(),
"body", m.body(),
"mandateType", m.mandateType().name(),
"term", m.term(),
"voivodeship", m.voivodeship(),
"sourceUrls", m.sourceUrls()
))
.collect(Collectors.toList())));
stats.addMandates(subList.size());
}
}
private String roleTypeToRelName(Role r) {
String organ = r.organ().name();
String role = r.roleType().name();
if ("DYREKCJA".equals(organ) && "DYREKTOR".equals(role)) return "DYREKTOR";
if ("DYREKCJA".equals(organ) && ("ZASTEPCA_DYREKTORA".equals(role) || "GLOWNY_KSIEGOWY".equals(role) || "PRZELOZONA_PIELEGNIARKOW".equals(role))) return "ZASTEPCA_DYREKTORA";
if ("PRZEWODNICZACY_ORGANU".equals(role)) return "PRZEWODNICZY_ORGANOWI";
return "CZLONEK_ORGANU";
}
private String escapeBacktick(String s) {
return s.replace("`", "\\`");
}
public static class LoadStats {
public int hospitalCount;
public int personCount;
public int roleCount;
public int affiliationCount;
public int mandateCount;
public final List<String> voivodeships = new ArrayList<>();
public void addVoivodeship(String v) { voivodeships.add(v); }
public void addHospitals(int count) { hospitalCount += count; }
public void addPeople(int count) { personCount += count; }
public void addRoles(int count) { roleCount += count; }
public void addAffiliations(int count) { affiliationCount += count; }
public void addMandates(int count) { mandateCount += count; }
@Override
public String toString() {
return String.format(
"Load stats - voivodeships: %s, hospitals: %d, people: %d, roles: %d, affiliations: %d, mandates: %d",
String.join(", ", voivodeships), hospitalCount, personCount, roleCount, affiliationCount, mandateCount
);
}
}
}