View Javadoc

1   /*
2    * Copyright 2015 Data Archiving and Networked Services (an institute of
3    * Koninklijke Nederlandse Akademie van Wetenschappen), King's College London,
4    * Georg-August-Universitaet Goettingen Stiftung Oeffentlichen Rechts
5    *
6    * Licensed under the EUPL, Version 1.1 or – as soon they will be approved by
7    * the European Commission - subsequent versions of the EUPL (the "Licence");
8    * You may not use this work except in compliance with the Licence.
9    * You may obtain a copy of the Licence at:
10   *
11   * https://joinup.ec.europa.eu/software/page/eupl
12   *
13   * Unless required by applicable law or agreed to in writing, software
14   * distributed under the Licence is distributed on an "AS IS" basis,
15   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   * See the Licence for the specific language governing
17   * permissions and limitations under the Licence.
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   * Export EAC 2010 XML.
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                     // FIXME: Harmonise this attribute
129                     tag(sw, "cpfRelation", attrs("cpfRelationType", "associative"), () -> {
130 
131                         // Look for a body which is an access point
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"); // FIXME?
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                 // NB: Assume script is Latin!!!
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                     // TODO: Normalise and put standardDateTime attribute here?
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             // We must provide a default event
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             // We only use an access point body for the name of this link
348             // if the access point is on the current entity (otherwise the
349             // link will have the same name as our current item.)
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()); // Fallback
381     }
382 }