1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 package eu.ehri.project.exporters.eac;
21
22 import com.google.common.base.Splitter;
23 import com.google.common.collect.ImmutableList;
24 import com.google.common.collect.ImmutableMap;
25 import com.google.common.collect.Lists;
26 import eu.ehri.project.api.Api;
27 import eu.ehri.project.definitions.Entities;
28 import eu.ehri.project.definitions.Isaar;
29 import eu.ehri.project.exporters.xml.AbstractStreamingXmlExporter;
30 import eu.ehri.project.models.AccessPoint;
31 import eu.ehri.project.models.DatePeriod;
32 import eu.ehri.project.models.HistoricalAgent;
33 import eu.ehri.project.models.HistoricalAgentDescription;
34 import eu.ehri.project.models.Link;
35 import eu.ehri.project.models.MaintenanceEvent;
36 import eu.ehri.project.models.MaintenanceEventAgentType;
37 import eu.ehri.project.models.MaintenanceEventType;
38 import eu.ehri.project.models.base.Accessible;
39 import eu.ehri.project.models.base.Described;
40 import eu.ehri.project.models.base.Description;
41 import eu.ehri.project.models.base.Named;
42 import eu.ehri.project.models.events.SystemEvent;
43 import eu.ehri.project.utils.LanguageHelpers;
44 import org.joda.time.DateTime;
45 import org.joda.time.format.DateTimeFormat;
46 import org.slf4j.Logger;
47 import org.slf4j.LoggerFactory;
48
49 import javax.xml.stream.XMLStreamWriter;
50 import java.util.List;
51 import java.util.Map;
52 import java.util.Optional;
53 import java.util.stream.Collectors;
54
55
56
57
58 public final class Eac2010Exporter extends AbstractStreamingXmlExporter<HistoricalAgent> implements EacExporter {
59
60 private static final Logger logger = LoggerFactory.getLogger(Eac2010Exporter.class);
61 private static final Map<String, String> NAMESPACES = namespaces(
62 "xlink", "http://www.w3.org/1999/xlink",
63 "xsi", "http://www.w3.org/2001/XMLSchema-instance"
64 );
65 private static final String DEFAULT_NAMESPACE = "urn:isbn:1-931666-33-4";
66
67 private final Api api;
68
69 private static final ImmutableMap<Isaar, String> descriptiveTextMappings = ImmutableMap.<Isaar, String>builder()
70 .put(Isaar.place, "place/placeEntry")
71 .put(Isaar.legalStatus, "legalStatus/term")
72 .put(Isaar.functions, "function/term")
73 .put(Isaar.occupation, "occupation/term")
74 .put(Isaar.mandates, "mandate/term")
75 .build();
76
77 private static final ImmutableMap<Isaar, String> pureTextMappings = ImmutableMap.<Isaar, String>builder()
78 .put(Isaar.structure, "structureOrGenealogy")
79 .put(Isaar.generalContext, "generalContext")
80 .put(Isaar.biographicalHistory, "biogHist")
81 .build();
82
83 private static final ImmutableMap<Isaar, String> nameMappings = ImmutableMap.of(
84 Isaar.lastName, "lastname",
85 Isaar.firstName, "forename");
86
87 public Eac2010Exporter(Api api) {
88 this.api = api;
89 }
90
91 @Override
92 public void export(XMLStreamWriter sw, HistoricalAgent agent, String langCode) {
93 comment(sw, resourceAsString("export-boilerplate.txt"));
94 root(sw, "eac-cpf", DEFAULT_NAMESPACE, attrs(), NAMESPACES, () -> {
95 attribute(sw, "http://www.w3.org/2001/XMLSchema-instance",
96 "schemaLocation", DEFAULT_NAMESPACE + "http://eac.staatsbibliothek-berlin.de/schema/cpf.xsd");
97 LanguageHelpers.getBestDescription(agent, Optional.empty(), langCode).ifPresent(desc -> {
98
99 addControlSection(sw, agent, desc);
100
101 tag(sw, "cpfDescription", () -> {
102
103 addIdentitySection(sw, agent, desc);
104
105 tag(sw, "description", () -> {
106
107 addDatesOfExistence(sw, desc);
108
109 for (Map.Entry<Isaar, String> entry : descriptiveTextMappings.entrySet()) {
110 addTextElements(sw, desc, entry.getKey().name(), entry.getValue());
111 }
112 for (Map.Entry<Isaar, String> entry : pureTextMappings.entrySet()) {
113 addPureTextElements(sw, desc, entry.getKey().name(), entry.getValue());
114 }
115 });
116
117 addRelations(sw, agent, desc, langCode);
118 });
119 });
120 });
121 }
122
123 private void addRelations(XMLStreamWriter sw, HistoricalAgent agent, Description desc, String langCode) {
124 List<Link> linkRels = ImmutableList.copyOf(agent.getLinks());
125 if (!linkRels.isEmpty()) {
126 tag(sw, "relations", () -> {
127 for (Link link : linkRels) {
128
129 tag(sw, "cpfRelation", attrs("cpfRelationType", "associative"), () -> {
130
131
132 getLinkEntityId(agent, link).ifPresent(id ->
133 attribute(sw, "http://www.w3.org/1999/xlink", "href", id)
134 );
135 getLinkName(agent, desc, link, langCode).ifPresent(name ->
136 tag(sw, "relationEntry", name)
137 );
138 getLinkDescription(link).ifPresent(name ->
139 tag(sw, ImmutableList.of("descriptiveNote", "p"), () -> cData(sw, name))
140 );
141 });
142 }
143 });
144 }
145 }
146
147 private void addPureTextElements(XMLStreamWriter sw, Description desc, String key, String path) {
148 Optional.ofNullable(desc.getProperty(key)).ifPresent(prop ->
149 tag(sw, ImmutableList.of(path, "p"), prop.toString())
150 );
151 }
152
153 private void addTextElements(XMLStreamWriter sw, Description desc, String key, String path) {
154 Optional.ofNullable(desc.getProperty(key)).ifPresent(prop -> {
155 List<String> keys = Splitter.on("/").splitToList(path);
156 for (Object value : coerceList(prop)) {
157 tag(sw, keys, value.toString());
158 }
159 });
160 }
161
162 private void addDatesOfExistence(XMLStreamWriter sw, Description desc) {
163 List<DatePeriod> allDates = ImmutableList.copyOf(desc.as(HistoricalAgentDescription.class)
164 .getDatePeriods());
165 List<DatePeriod> existence = allDates.stream()
166 .filter(d -> DatePeriod.DatePeriodType.existence.equals(d.getDateType()))
167 .collect(Collectors.toList());
168 if (existence.isEmpty() && !allDates.isEmpty()) {
169 existence.add(allDates.get(0));
170 }
171
172 String datesOfExistence = desc.getProperty("datesOfExistence");
173 if (!existence.isEmpty() || datesOfExistence != null) {
174 tag(sw, "existDates", () -> {
175 for (DatePeriod datePeriod : existence) {
176 tag(sw, "dateRange", () -> {
177 String startDate = datePeriod.getStartDate();
178 String endDate = datePeriod.getEndDate();
179 if (startDate != null) {
180 String startYear = String.valueOf(new DateTime(startDate).year().get());
181 tag(sw, "fromDate", startYear, attrs("standardDate", startYear));
182 }
183 if (endDate != null) {
184 String endYear = String.valueOf(new DateTime(endDate).year().get());
185 tag(sw, "toDate", endYear, attrs("standardDate", endYear));
186 }
187 });
188 }
189 if (existence.isEmpty() && datesOfExistence != null) {
190 tag(sw, "date", () -> cData(sw, datesOfExistence));
191 } else if (datesOfExistence != null) {
192 tag(sw, ImmutableList.of("descriptiveNote", "p"),
193 () -> cData(sw, datesOfExistence));
194 }
195 });
196 }
197 }
198
199 private void addIdentitySection(XMLStreamWriter sw, HistoricalAgent agent, Description desc) {
200 tag(sw, "identity", () -> {
201 tag(sw, "entityId", agent.getIdentifier());
202 tag(sw, "entityType", desc.<String>getProperty(Isaar.typeOfEntity));
203 tag(sw, "nameEntry", () -> {
204 tag(sw, "part", desc.getName());
205 tag(sw, "authorizedForm", "ehri");
206 });
207
208 for (Map.Entry<Isaar, String> entry : nameMappings.entrySet()) {
209 Optional.ofNullable(desc.<String>getProperty(entry.getKey())).ifPresent(value ->
210 tag(sw, ImmutableList.of("nameEntry", "part"),
211 value, attrs("localType", entry.getValue()))
212 );
213 }
214
215 Optional.ofNullable(desc.getProperty(Isaar.otherFormsOfName)).ifPresent(parNames -> {
216 List values = coerceList(parNames);
217 if (!values.isEmpty()) {
218 for (Object value : values) {
219 tag(sw, "nameEntry", () -> {
220 tag(sw, "part", value.toString());
221 tag(sw, "alternativeForm", "ehri");
222 });
223 }
224 }
225 });
226
227 Optional.ofNullable(desc.getProperty(Isaar.parallelFormsOfName)).ifPresent(parNames -> {
228 List values = coerceList(parNames);
229 if (!values.isEmpty()) {
230 tag(sw, "nameEntryParallel", () -> {
231 tag(sw, "nameEntry", () -> {
232 tag(sw, "part", desc.getName());
233 tag(sw, "preferredForm", "ehri");
234 });
235 for (Object value : values) {
236 tag(sw, ImmutableList.of("nameEntry", "part"), value.toString());
237 }
238 });
239 }
240 });
241 });
242 }
243
244 private void addControlSection(XMLStreamWriter sw, HistoricalAgent agent, Description desc) {
245 tag(sw, "control", () -> {
246 tag(sw, "recordId", agent.getId());
247 tag(sw, "otherRecordId", agent.getIdentifier(), attrs("localType", "yes"));
248 tag(sw, "maintenanceStatus", "revised");
249 tag(sw, "publicationStatus", "approved");
250 tag(sw, ImmutableList.of("maintenanceAgency", "agencyName"), "The EHRI Consortium");
251 tag(sw, "languageDeclaration", () -> {
252 tag(sw, "language", LanguageHelpers.codeToName(desc.getLanguageOfDescription()),
253 attrs("languageCode", desc.getLanguageOfDescription()));
254
255 tag(sw, "script", "Latin", attrs("scriptCode", "Latn"));
256 });
257 tag(sw, "conventionDeclaration", () -> {
258 tag(sw, "abbreviation", "ehri");
259 tag(sw, "citation", "EHRI Naming Policy");
260 });
261
262 addRevisionDesc(sw, agent, desc);
263
264 Optional.ofNullable(desc.getProperty(Isaar.source)).ifPresent(sources -> {
265 List sourceValues = coerceList(sources);
266 tag(sw, "sources", () -> {
267 for (Object value : sourceValues) {
268 tag(sw, "source", () -> tag(sw, "sourceEntry", value.toString()));
269 }
270 });
271 });
272 });
273 }
274
275 private void addRevisionDesc(XMLStreamWriter sw, HistoricalAgent agent, Description desc) {
276 tag(sw, "maintenanceHistory", () -> {
277 List<MaintenanceEvent> maintenanceEvents = Lists
278 .newArrayList(desc.getMaintenanceEvents());
279 for (MaintenanceEvent event : maintenanceEvents) {
280 tag(sw, "maintenanceEvent", () -> {
281 tag(sw, "eventType", event.getEventType().name());
282
283 tag(sw, "eventDateTime", event.<String>getProperty("date"));
284 tag(sw, "agentType", MaintenanceEventAgentType.human.name());
285 tag(sw, "agent", "EHRI");
286 String eventDesc = event.getProperty("source");
287 if (eventDesc != null && !eventDesc.trim().isEmpty()) {
288 tag(sw, "eventDescription", eventDesc);
289 }
290 });
291 }
292
293 List<List<SystemEvent>> systemEvents = ImmutableList.copyOf(
294 api.events().aggregateForItem(agent));
295 for (List<SystemEvent> events : Lists.reverse(systemEvents)) {
296 tag(sw, "maintenanceEvent", () -> {
297 SystemEvent event = events.get(0);
298
299 tag(sw, "eventType", MaintenanceEventType
300 .fromSystemEventType(event.getEventType()).name());
301 DateTime dateTime = new DateTime(event.getTimestamp());
302 tag(sw, "eventDateTime", DateTimeFormat.longDateTime().print(dateTime),
303 attrs("standardDateTime", dateTime.toString()));
304 tag(sw, "agentType", MaintenanceEventAgentType.human.name());
305 tag(sw, "agent", Optional.ofNullable(event.getActioner())
306 .map(Named::getName).orElse("EHRI"));
307 if (event.getLogMessage() != null && !event.getLogMessage().isEmpty()) {
308 tag(sw, "eventDescription", event.getLogMessage());
309 }
310 });
311 }
312
313
314 if (maintenanceEvents.isEmpty() && systemEvents.isEmpty()) {
315 logger.debug("No events found for element {}, using fallback", agent.getId());
316 tag(sw, "maintenanceEvent", () -> {
317 tag(sw, "eventType", MaintenanceEventType.created.name());
318 DateTime dateTime = DateTime.now();
319 tag(sw, "eventDateTime", DateTimeFormat.longDateTime().print(dateTime),
320 attrs("standardDateTime", dateTime.toString()));
321 tag(sw, "agentType", MaintenanceEventAgentType.machine.name());
322 tag(sw, "agent", agent.getId());
323 });
324 }
325 });
326 }
327
328 private Optional<String> getLinkDescription(Link link) {
329 String desc = link.getDescription();
330 if (desc == null) {
331 for (Accessible other : link.getLinkBodies()) {
332 if (other.getType().equals(Entities.ACCESS_POINT)) {
333 AccessPoint ap = other.as(AccessPoint.class);
334 desc = ap.getProperty("description");
335 }
336 }
337 }
338 if (desc != null && !desc.trim().isEmpty()) {
339 return Optional.of(desc);
340 }
341 return Optional.empty();
342 }
343
344 private Optional<String> getLinkName(Described entity,
345 Description description, Link link, String lang) {
346 for (Accessible other : link.getLinkBodies()) {
347
348
349
350 if (other.getType().equals(Entities.ACCESS_POINT)) {
351 AccessPoint ap = other.as(AccessPoint.class);
352 for (AccessPoint outAp : description.getAccessPoints()) {
353 if (outAp.equals(ap)) {
354 return Optional.of(ap.getName());
355 }
356 }
357 }
358 }
359 for (Accessible other : link.getLinkTargets()) {
360 if (!other.equals(entity)) {
361 return Optional.of(getEntityName(other.as(Described.class), lang));
362 }
363 }
364
365 return Optional.empty();
366 }
367
368 private Optional<String> getLinkEntityId(Described entity, Link link) {
369 for (Accessible other : link.getLinkTargets()) {
370 if (!other.equals(entity)) {
371 return Optional.of(other.getId());
372 }
373 }
374 return Optional.empty();
375 }
376
377 private String getEntityName(Described entity, String lang) {
378 return LanguageHelpers.getBestDescription(entity, lang)
379 .map(Description::getName)
380 .orElse(entity.getIdentifier());
381 }
382 }