Coverage Summary for Class: Course (org.kitodo.production.model.bibliography.course)
Class |
Class, %
|
Method, %
|
Line, %
|
Course |
100%
(1/1)
|
31,2%
(10/32)
|
17,5%
(54/308)
|
/*
* (c) Kitodo. Key to digital objects e. V. <contact@kitodo.org>
*
* This file is part of the Kitodo project.
*
* It is licensed under GNU General Public License version 3 or later.
*
* For the full copyright and license information, please read the
* GPL3-License.txt file that was distributed with this source code.
*/
package org.kitodo.production.model.bibliography.course;
import java.io.IOException;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.MonthDay;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.kitodo.production.helper.XMLUtils;
import org.kitodo.production.model.bibliography.course.metadata.CountableMetadata;
import org.kitodo.production.model.bibliography.course.metadata.RecoveredMetadata;
import org.kitodo.production.services.data.ImportService;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
/**
* The class Course represents the course of appearance of a newspaper.
*
* <p>
* A course of appearance consists of one or more blocks of time. Interruptions
* in the course of appearance can be modeled by subsequent blocks.
*/
public class Course extends ArrayList<Block> {
/**
* Attribute {@code after="…"} used in the XML representation of a course of
* appearance.
*
* <p>
* Newspapers, especially bigger ones, can have several issues that, e.g.,
* may differ in time of publication (morning issue, evening issue, …) or
* geographic distribution (Edinburgh issue, London issue, …). Normally,
* when parsing the XML file, the issues are created in their order of first
* appearance. However, if you want to enforce a different order, you can
* define it here.
*
* <p>
* The attribute {@code after="…"} holds the names of issues that shall be
* ordered before this issue. Several issues are to be separated by white
* space. Issue names containing white space must be enclosed in double
* replaced by two subsequent apostrophes ("{@code ''}", 2× U+0027).
*
* <p>
* Not-yet-mentioned issues are created before, though maybe empty.
*/
private static final String ATTRIBUTE_AFTER = "after";
/**
* Attribute {@code date="…"} used in the XML representation of a course of
* appearance.
*/
private static final String ATTRIBUTE_DATE = "date";
/**
* Attribute {@code increment="…"} used in the XML representation of a
* course of appearance.
*
* <p>
* The attribute {@code increment="…"} can have as value one of the
* {@link Granularity} values, in lower case. It indicates when the counter
* shall be incremented.
*/
private static final String ATTRIBUTE_INCREMENT = "increment";
/**
* Attribute {@code issue="…"} used in the XML representation of a course of
* appearance.
*
* <p>
* The attribute {@code issue="…"} holds the name of the issue. Newspapers,
* especially bigger ones, can have several issues that, e.g., may differ in
* time of publication (morning issue, evening issue, …) or geographic
* distribution (Edinburgh issue, London issue, …).
*/
private static final String ATTRIBUTE_ISSUE_HEADING = "issue";
/**
* Attribute {@code metadataType="…"} used in the XML representation of a
* course of appearance.
*
* <p>
* The attribute {@code metadataType="…"} holds the name of the metadata
* type that this counter will be written to.
*/
private static final String ATTRIBUTE_METADATA_TYPE = "metadataType";
/**
* Attribute {@code value="…"} used in the XML representation of a course of
* appearance.
*
* <p>
* The attribute {@code value="…"} holds the counter start value.
*/
private static final String ATTRIBUTE_VALUE = "value";
/**
* Attribute {@code index="…"} used in the XML representation of a course of
* appearance.
*
* <p>
* The attribute {@code index="…"} is optional. It may be used to
* distinguish different blocks if needed and can be omitted if only one
* block is used.
*/
private static final String ATTRIBUTE_VARIANT = "index";
/**
* Attribute {@code yearBegin="…"} used in the XML representation of a
* course of appearance.
*
* <p>
* The attribute {@code yearBegin="…"} is optional. It may be used to
* indicate a year begin different from the first of January, as it may be
* used for school years, business years, or seasons.
*/
private static final String ATTRIBUTE_YEAR_BEGIN = "yearBegin";
/**
* Attribute {@code yearTerm="…"} used in the XML representation of a course
* of appearance.
*
* <p>
* The attribute {@code yearTerm="…"} is optional. It may be used to
* indicate the type of year that begins with a date different from the
* first of January, values maybe like “business year”, “season”, or “school
* year”.
*/
private static final String ATTRIBUTE_YEAR_TERM = "yearTerm";
/**
* Element {@code <appeared>} used in the XML representation of a
* course of appearance.
*
* <p>
* Each {@code <appeared>} element represents one issue that
* physically appeared. It has the attributes {@code issue="…"}
* (required, may be empty) and {@code date="…"} (required) and cannot
* hold child elements.
*/
private static final String ELEMENT_APPEARED = "appeared";
/**
* Element {@code <course>} used in the XML representation of a
* course of appearance.
*
* <p>
* {@code <course>} is the root element of the XML
* representation. It can hold two children,
* {@code <description>} (output only, optional) and
* {@code <processes>} (required).
*/
private static final String ELEMENT_COURSE = "course";
/**
* Element {@code <description>} used in the XML representation
* of a course of appearance.
*
* <p>
* {@code <description>} holds a verbal, human-readable
* description of the course of appearance, which is generated only and
* doesn’t have an effect on input.
*/
private static final String ELEMENT_DESCRIPTION = "description";
/**
* Element {@code <metadata>} used in the XML representation of a course of
* appearance.
*
* <p>
* {@code <metadata>} declares an auto-counting metadata value assigned to
* the issue it is used in. The counter will start counting until it is
* replaced by another counter. A counter value of {@code ""} disables the
* counter.
*/
private static final String ELEMENT_METADATA = "metadata";
/**
* Element {@code <process>} used in the XML representation of a course of
* appearance.
*
* <p>
* Each {@code <process>} element represents one process to be generated in
* Production. It can hold {@code <title>} elements (of any quantity).
*/
private static final String ELEMENT_PROCESS = "process";
/**
* Element {@code <processes>} used in the XML representation of
* a course of appearance.
*
* <p>
* Each {@code <processes>} element represents the processes to
* be generated in Production. It can hold
* {@code <process>} elements (of any quantity).
*/
private static final String ELEMENT_PROCESSES = "processes";
/**
* Element {@code <title>} used in the XML representation of a
* course of appearance. Each {@code <title>} element represents
* a block in time the appeared issues belong to. It has the optional
* attribute {@code index="…"} and can hold
* {@code <appeared>} elements (of any quantity).
*
* <p>
* Note: In the original design, the element was intended to model title
* name changes. This was given up later, but for historical reasons, the
* XML element’s name is still “title”. For the original design, see
* https://github.com/kitodo/kitodo-production/issues/51#issuecomment-38035674
*/
private static final String ELEMENT_BLOCK = "title";
/**
* January the 1ˢᵗ.
*/
public static final MonthDay FIRST_OF_JANUARY = MonthDay.of(1, 1);
private static final int WEEKDAY_PAGES = 40;
private static final int SUNDAY_PAGES = 240;
/**
* List of Lists of Issues, each representing a process.
*/
private final transient List<List<IndividualIssue>> processes = new ArrayList<>();
private final transient Map<String, Block> resolveByBlockVariantCache = new HashMap<>();
private boolean processesAreVolatile = true;
/**
* The name of the year, such as “business year”, “fiscal year”, or
* “season”.
*/
private String yearName = "";
/**
* The first day of the year.
*/
private MonthDay yearStart = MonthDay.of(1, 1);
/**
* Default constructor, creates an empty course. Must be made explicit since
* we offer other constructors, too.
*/
public Course() {
super();
}
/**
* Constructor to create a course from an XML source.
*
* @param xml
* XML document data structure
* @throws NoSuchElementException
* if ELEMENT_COURSE or ELEMENT_PROCESSES cannot be found
* @throws IllegalArgumentException
* if the dates of two blocks do overlap
* @throws NullPointerException
* if a mandatory element is absent
*/
public Course(Document xml) {
super();
processesAreVolatile = false;
Element rootNode = XMLUtils.getFirstChildWithTagName(xml, ELEMENT_COURSE);
String yearBegin = rootNode.getAttribute(ATTRIBUTE_YEAR_BEGIN);
if (!yearBegin.isEmpty()) {
LocalDate dateTime = LocalDate.parse(yearBegin,
DateTimeFormatter.ofPattern("--MM-dd").withLocale(DateTimeFormatter.ISO_DATE.getLocale()));
yearStart = MonthDay.of(dateTime.getMonthValue(), dateTime.getDayOfMonth());
}
yearName = rootNode.getAttribute(ATTRIBUTE_YEAR_TERM);
Element processesNode = XMLUtils.getFirstChildWithTagName(rootNode, ELEMENT_PROCESSES);
int initialCapacity = 10;
Map<LocalDate, IndividualIssue> lastIssueForDate = new HashMap<>();
List<RecoveredMetadata> recoveredMetadata = new LinkedList<>();
for (Node processNode = processesNode.getFirstChild(); processNode != null; processNode = processNode
.getNextSibling()) {
if (!(processNode instanceof Element) || !processNode.getNodeName().equals(ELEMENT_PROCESS)) {
continue;
}
initialCapacity = processProcessNode(initialCapacity, lastIssueForDate, recoveredMetadata, processNode);
}
processRecoveredMetadata(recoveredMetadata);
recalculateRegularityOfIssues();
processesAreVolatile = true;
}
private int processProcessNode(int initialCapacity, Map<LocalDate, IndividualIssue> lastIssueForDate,
List<RecoveredMetadata> recoveredMetadata, Node processNode) {
List<IndividualIssue> process = new ArrayList<>(initialCapacity);
for (Node blockNode = processNode.getFirstChild(); blockNode != null; blockNode = blockNode
.getNextSibling()) {
if (!(blockNode instanceof Element) || !blockNode.getNodeName().equals(ELEMENT_BLOCK)) {
continue;
}
processBlockNode(lastIssueForDate, recoveredMetadata, process, blockNode);
}
processes.add(process);
initialCapacity = (int) Math.round(1.1 * process.size());
return initialCapacity;
}
private void processBlockNode(Map<LocalDate, IndividualIssue> lastIssueForDate,
List<RecoveredMetadata> recoveredMetadata, List<IndividualIssue> process,
Node blockNode) {
String variant = ((Element) blockNode).getAttribute(ATTRIBUTE_VARIANT);
for (Node issueNode = blockNode.getFirstChild(); issueNode != null; issueNode = issueNode
.getNextSibling()) {
if (!(issueNode instanceof Element) || !issueNode.getNodeName().equals(ELEMENT_APPEARED)) {
continue;
}
String issue = ((Element) issueNode).getAttribute(ATTRIBUTE_ISSUE_HEADING);
String date = ((Element) issueNode).getAttribute(ATTRIBUTE_DATE);
if (date == null) {
throw new NullPointerException(ATTRIBUTE_DATE);
}
String after = ((Element) issueNode).getAttribute(ATTRIBUTE_AFTER);
List<String> before = Objects.isNull(after) ? Collections.emptyList()
: splitAtSpaces(after);
LocalDate localDate = LocalDate.parse(date);
IndividualIssue individualIssue = addAddition(variant, before, issue, localDate);
IndividualIssue previousIssue = lastIssueForDate.get(localDate);
if (previousIssue != null) {
Integer sortingNumber = previousIssue.getSortingNumber();
if (sortingNumber == null) {
sortingNumber = 1;
previousIssue.setSortingNumber(sortingNumber);
}
individualIssue.setSortingNumber(sortingNumber + 1);
}
lastIssueForDate.put(localDate, individualIssue);
process.add(individualIssue);
findToBeRecoveredMetadata(recoveredMetadata, issueNode, issue, date);
}
}
private void processRecoveredMetadata(List<RecoveredMetadata> recoveredMetadata) {
Map<Pair<Block, String>, CountableMetadata> last = new HashMap<>();
for (RecoveredMetadata metaDatum : recoveredMetadata) {
Block foundBlock = null;
Issue foundIssue = null;
BLOCK: for (Block block : this) {
for (IndividualIssue individualIssue : block.getIndividualIssues(metaDatum.getDate())) {
if (individualIssue.getHeading().equals(metaDatum.getIssue())) {
foundBlock = block;
foundIssue = individualIssue.getIssue();
break BLOCK;
}
}
}
CountableMetadata previousMetadata = last.get(Pair.of(foundBlock, metaDatum.getMetadataType()));
if (previousMetadata != null) {
previousMetadata.setDelete(Pair.of(metaDatum.getDate(), foundIssue));
}
CountableMetadata metadata = new CountableMetadata(foundBlock, Pair.of(metaDatum.getDate(), foundIssue));
metadata.setMetadataType(metaDatum.getMetadataType());
metadata.setStartValue(metaDatum.getValue());
metadata.setStepSize(metaDatum.getStepSize());
foundBlock.addMetadata(metadata);
last.put(Pair.of(foundBlock, metaDatum.getMetadataType()), metadata);
}
}
private void findToBeRecoveredMetadata(List<RecoveredMetadata> recoveredMetadata, Node issueNode,
String issue, String date) {
for (Node metadataNode = issueNode
.getFirstChild(); metadataNode != null; metadataNode = metadataNode.getNextSibling()) {
if (!(metadataNode instanceof Element)
|| !metadataNode.getNodeName().equals(ELEMENT_METADATA)) {
continue;
}
RecoveredMetadata recovered = new RecoveredMetadata(LocalDate.parse(date), issue);
recovered.setMetadataType(((Element) metadataNode).getAttribute(ATTRIBUTE_METADATA_TYPE));
if (recovered.getMetadataType() == null) {
throw new NullPointerException(ATTRIBUTE_METADATA_TYPE);
}
recovered.setValue(((Element) metadataNode).getAttribute(ATTRIBUTE_VALUE));
if (recovered.getValue() == null) {
throw new NullPointerException(ATTRIBUTE_VALUE);
}
String increment = ((Element) metadataNode).getAttribute(ATTRIBUTE_INCREMENT);
try {
recovered.setStepSize(Granularity.valueOf(increment.toUpperCase()));
} catch (IllegalArgumentException e) {
recovered.setStepSize(null);
}
recoveredMetadata.add(recovered);
}
}
/**
* Appends the specified block to the end of this course.
*
* @param block
* block to be appended to this course
* @return true (as specified by Collection.add(E))
* @see java.util.ArrayList#add(java.lang.Object)
*/
@Override
public boolean add(Block block) {
super.add(block);
if (block.countIndividualIssues() > 0) {
processes.clear();
}
return true;
}
/**
* Adds a LocalDate to the set of additions of the issue identified by
* issueHeading in the block optionally identified by a variant. Note that
* in case that the date is outside the time range of the described block,
* the time range will be expanded. Do not use this function in contexts
* where there is one or more issues in the block that have a regular
* appearance set, because in this case the regularly appeared issues in the
* expanded block will show up later, too, which is probably not what you
* want.
*
* @param variant
* block identifier (may be null)
* @param beforeIssues
* issues to be existing before this one
* @param issueHeading
* heading of the issue this issue is of
* @param date
* date to add
* @return an IndividualIssue representing the added issue
* @throws IllegalArgumentException
* if the date would cause the block to overlap with another
* block
*/
private IndividualIssue addAddition(String variant, List<String> beforeIssues, String issueHeading,
LocalDate date) {
Block block = get(variant);
if (block == null) {
block = new Block(this, variant);
try {
block.setFirstAppearance(date);
block.setLastAppearance(date);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(e.getMessage() + ", (" + variant + ") " + date, e);
}
add(block);
} else {
if (block.getFirstAppearance().isAfter(date)) {
block.setFirstAppearance(date);
}
if (block.getLastAppearance().isBefore(date)) {
block.setLastAppearance(date);
}
}
for (String issueBefore : beforeIssues) {
Issue issue = block.getIssue(issueBefore);
if (issue == null) {
issue = new Issue(this, issueBefore);
block.addIssue(issue);
}
}
Issue issue = block.getIssue(issueHeading);
if (issue == null) {
issue = new Issue(this, issueHeading);
block.addIssue(issue);
}
issue.addAddition(date);
return new IndividualIssue(block, issue, date, null);
}
/**
* Deletes the process list. This is
* necessary if the processes must be regenerated because the data structure
* they will be derived from has changed, or if they only had been added
* temporarily to be able to retrieve an XML file containing values.
*/
public void clearProcesses() {
if (processesAreVolatile) {
processes.clear();
}
}
/**
* Determines how many stampings of
* issues physically appeared without generating a list of IndividualIssue
* objects.
*
* @return the count of issues
*/
public long countIndividualIssues() {
long numberOfIndividualIssues = 0;
for (Block block : this) {
numberOfIndividualIssues += block.countIndividualIssues();
}
return numberOfIndividualIssues;
}
/**
* Returns the block identified by the optionally given variant, or null if
* no block with the given variant can be found.
*
* @param variant
* the variant of the block (may be null)
* @return the block identified by the given variant, or null if no block
* can be found
*/
private Block get(String variant) {
if (resolveByBlockVariantCache.containsKey(variant)) {
Block potentialResult = resolveByBlockVariantCache.get(variant);
if (potentialResult.isIdentifiedBy(variant)) {
return potentialResult;
} else {
resolveByBlockVariantCache.remove(variant);
}
}
for (Block candidate : this) {
if (candidate.isIdentifiedBy(variant)) {
resolveByBlockVariantCache.put(variant, candidate);
return candidate;
}
}
return null;
}
/**
* Generates a list of IndividualIssue
* objects, each of them representing a stamping of one physically appeared
* issue.
*
* @return a LinkedHashSet of IndividualIssue objects, each of them
* representing one physically appeared issue
*/
public Set<IndividualIssue> getIndividualIssues() {
LinkedHashSet<IndividualIssue> individualIssues = new LinkedHashSet<>();
LocalDate lastAppearance = getLastAppearance();
LocalDate firstAppearance = getFirstAppearance();
if (Objects.nonNull(firstAppearance)) {
for (LocalDate day = firstAppearance; !day.isAfter(lastAppearance); day = day.plusDays(1)) {
for (Block block : this) {
individualIssues.addAll(block.getIndividualIssues(day));
}
}
}
return individualIssues;
}
/**
* Returns the date the regularity of this
* course of appearance starts with.
*
* @return the date of first appearance
*/
public LocalDate getFirstAppearance() {
if (super.isEmpty()) {
return null;
}
LocalDate firstAppearance = super.get(0).getFirstAppearance();
for (int index = 1; index < super.size(); index++) {
LocalDate otherFirstAppearance = super.get(index).getFirstAppearance();
if (otherFirstAppearance.isBefore(firstAppearance)) {
firstAppearance = otherFirstAppearance;
}
}
return firstAppearance;
}
/**
* Returns the date the regularity of this
* course of appearance ends with.
*
* @return the date of last appearance
*/
public LocalDate getLastAppearance() {
if (super.isEmpty()) {
return null;
}
LocalDate lastAppearance = super.get(0).getLastAppearance();
for (int index = 1; index < super.size(); index++) {
LocalDate otherLastAppearance = super.get(index).getLastAppearance();
if (otherLastAppearance.isAfter(lastAppearance)) {
lastAppearance = otherLastAppearance;
}
}
return lastAppearance;
}
/**
* Returns the number of processes into
* which the course of appearance will be split.
*
* @return the number of processes
*/
public int getNumberOfProcesses() {
return processes.size();
}
/**
* Returns the name of the year. The name of the year is optional and maybe
* empty. Typical values are “Business year”, “Fiscal year”, or “Season”.
*
* @return the name of the year
*/
public String getYearName() {
return yearName;
}
/**
* Returns the beginning of the year. Typically, this is the 1ˢᵗ of January,
* but it can be changed here to other days as well. The beginning of the
* year must parse and must not not be empty.
*
* @return the beginning of the year
*/
public MonthDay getYearStart() {
return yearStart;
}
/**
* Calculates a guessed number of pages for a course of appearance of a
* newspaper, presuming each issue having 40 pages and Sunday issues having
* six times that size because most people buy the Sunday issue most often
* and therefore advertisers buy the most space on that day.
*
* @return a guessed total number of pages for the full course of appearance
*/
public long guessTotalNumberOfPages() {
long totalNumberOfPages = 0;
for (Block block : this) {
LocalDate lastAppearance = block.getLastAppearance();
for (LocalDate day = block.getFirstAppearance(); !day.isAfter(lastAppearance); day = day.plusDays(1)) {
for (Issue issue : block.getIssues()) {
if (issue.isMatch(day)) {
totalNumberOfPages += day.getDayOfWeek() != DayOfWeek.SUNDAY ? WEEKDAY_PAGES
: SUNDAY_PAGES;
}
}
}
}
return totalNumberOfPages;
}
/**
* Returns the processes to create from the
* course of appearance.
*
* @return the processes
*/
public List<List<IndividualIssue>> getProcesses() {
return processes;
}
/**
* Iterates over the array of blocks and returns the
* first one that matches a given date. Since there shouldn’t be overlapping
* blocks, there should be at most one block for which this is true. If no
* matching block is found, it will return null.
*
* @param date
* a LocalDate to examine
* @return the block on which this date is represented, if any
*/
public Block isMatch(LocalDate date) {
for (Block block : this) {
if (block.isMatch(date)) {
return block;
}
}
return null;
}
/**
* Joins a list of strings to a string of whitespace-separated tokens,
* surrounding tokens containing spaces with quotes.
*
* @param input
* string to tokenize
* @return list of split strings
*/
private static String joinQuoting(Collection<String> input) {
StringBuilder result = new StringBuilder(16 * input.size());
boolean first = true;
for (String item : input) {
if (first) {
first = false;
} else {
result.append(' ');
}
boolean hasSpace = item.indexOf(' ') > -1;
if (hasSpace) {
result.append('"');
}
result.append(item.replaceAll("\"", "''"));
if (hasSpace) {
result.append('"');
}
}
return result.toString();
}
/**
* Recalculates for all blocks of this Course for each Issue the daysOfWeek
* of its regular appearance within the interval of time of the block. This
* is especially sensible to detect the underlying regularity after lots of
* issues whose existence is known have been added one by one as additions
* to the underlying issue(s).
*/
public void recalculateRegularityOfIssues() {
for (Block block : this) {
block.recalculateRegularityOfIssues();
}
}
/**
* Removes the element at the specified position in
* this list. Shifts any subsequent elements to the left (subtracts one from
* their indices). Additionally, any references to the object held in the
* map used for resolving are being removed so that the object can be
* garbage-collected.
*
* @param index
* the index of the element to be removed
* @return the element that was removed from the list
* @throws IndexOutOfBoundsException
* if the index is out of range (index < 0 || index >= size())
* @see java.util.ArrayList#remove(int)
*/
@Override
public Block remove(int index) {
Block block = super.remove(index);
resolveByBlockVariantCache.entrySet().removeIf(entry -> entry.getValue() == block);
if (block.countIndividualIssues() > 0) {
processes.clear();
}
return block;
}
/**
* Splits a string of whitespace-separated tokens, considering tokens
* surrounded by quotes as one.
*
* @param input
* string to tokenize
* @return list of split strings
*/
private static List<String> splitAtSpaces(String input) {
List<String> result = new ArrayList<>();
Matcher matcher = Pattern.compile("([^\"]\\S*|\".+?\")\\s*").matcher(input);
while (matcher.find()) {
result.add(matcher.group(1).replaceFirst("^\"(.*)\"$", "$1").replaceAll("''", "\""));
}
return result;
}
/**
* Calculates the processes depending on the given BreakMode.
*
* @param mode
* how the course shall be broken into issues
*/
public void splitInto(Granularity mode) {
int initialCapacity = 10;
Integer lastMark = null;
List<IndividualIssue> process = null;
processes.clear();
for (IndividualIssue issue : getIndividualIssues()) {
Integer mark = issue.getBreakMark(mode, yearStart);
if (!mark.equals(lastMark) && process != null) {
initialCapacity = (int) Math.round(1.1 * process.size());
processes.add(process);
process = null;
}
if (process == null) {
process = new ArrayList<>(initialCapacity);
}
process.add(issue);
lastMark = mark;
}
if (process != null) {
processes.add(process);
}
}
/**
* Transforms a course of appearance to XML.
*
* @return XML as String
*/
public Document toXML() throws IOException {
Document xml = XMLUtils.newDocument();
Element courseNode = xml.createElement(ELEMENT_COURSE);
if (!yearStart.equals(FIRST_OF_JANUARY)) {
courseNode.setAttribute(ATTRIBUTE_YEAR_BEGIN, yearStart.toString());
}
if (!yearName.isEmpty()) {
courseNode.setAttribute(ATTRIBUTE_YEAR_TERM, yearName);
}
Element description = xml.createElement(ELEMENT_DESCRIPTION);
description.appendChild(xml.createTextNode(StringUtils.join(CourseToGerman.asReadableText(this), "\n\n")));
courseNode.appendChild(description);
courseNode.appendChild(processesToXml(xml));
xml.appendChild(courseNode);
return xml;
}
private Element processesToXml(Document xml) {
Element processesNode = xml.createElement(ELEMENT_PROCESSES);
Set<Pair<Integer, String>> afterDeclarations = new HashSet<>();
for (List<IndividualIssue> process : processes) {
Element processNode = xml.createElement(ELEMENT_PROCESS);
Element blockNode = null;
int previous = -1;
for (IndividualIssue issue : process) {
blockNode = issueToXml(xml, afterDeclarations, processNode, blockNode, previous, issue);
}
if (blockNode != null) {
processNode.appendChild(blockNode);
}
processesNode.appendChild(processNode);
}
return processesNode;
}
private Element issueToXml(Document xml, Set<Pair<Integer, String>> afterDeclarations, Element processNode,
Element blockNode, int previous, IndividualIssue issue) {
int index = issue.indexIn(this);
if (index != previous && blockNode != null) {
processNode.appendChild(blockNode);
blockNode = null;
}
if (blockNode == null) {
blockNode = xml.createElement(ELEMENT_BLOCK);
blockNode.setAttribute(ATTRIBUTE_VARIANT, Integer.toString(index + 1));
}
Element issueNode = xml.createElement(ELEMENT_APPEARED);
issueNode.setAttribute(ATTRIBUTE_ISSUE_HEADING, issue.getHeading());
issueNode.setAttribute(ATTRIBUTE_DATE, issue.getDate().toString());
addMetadataToIssue(xml, issue, issueNode);
Pair<Integer, String> afterDeclaration = Pair.of(index, issue.getHeading());
if (!afterDeclarations.contains(afterDeclaration)) {
List<String> issuesBefore = issue.getIssuesBefore();
if (!issuesBefore.isEmpty()) {
issueNode.setAttribute(ATTRIBUTE_AFTER, joinQuoting(issuesBefore));
}
afterDeclarations.add(afterDeclaration);
}
blockNode.appendChild(issueNode);
previous = index;
return blockNode;
}
private void addMetadataToIssue(Document xml, IndividualIssue issue, Element issueNode) {
Pair<LocalDate, Issue> issueId = Pair.of(issue.getDate(), issue.getIssue());
Map<String, CountableMetadata> metadata = new HashMap<>();
for (Block block : this) {
for (CountableMetadata metaDatum : block.getMetadata(issueId, false)) {
metadata.put(metaDatum.getMetadataType(), metaDatum);
}
}
for (Block block : this) {
for (CountableMetadata metaDatum : block.getMetadata(issueId, true)) {
metadata.put(metaDatum.getMetadataType(), metaDatum);
}
}
for (Entry<String, CountableMetadata> entry : metadata.entrySet()) {
Element metadataNode = xml.createElement(ELEMENT_METADATA);
metadataNode.setAttribute(ATTRIBUTE_METADATA_TYPE, entry.getKey());
CountableMetadata metaDatum = entry.getValue();
if (metaDatum.matches(metaDatum.getMetadataType(), issueId, false)) {
metadataNode.setAttribute(ATTRIBUTE_VALUE, "");
} else {
metadataNode.setAttribute(ATTRIBUTE_VALUE, ImportService.getProcessDetailValue(metaDatum.getMetadataDetail()));
if (metaDatum.getStepSize() != null) {
metadataNode.setAttribute(ATTRIBUTE_INCREMENT, metaDatum.getStepSize().toString().toLowerCase());
}
}
issueNode.appendChild(metadataNode);
}
}
/**
* Sets the year name of the course.
*
* @param yearName
* the yearName to set
*/
public void setYearName(String yearName) {
this.yearName = yearName;
}
/**
* Sets the year start of the course.
*
* @param yearStart
* the yearStart to set
*/
public void setYearStart(MonthDay yearStart) {
this.yearStart = yearStart;
}
}