Coverage Summary for Class: Block (org.kitodo.production.model.bibliography.course)
Class |
Class, %
|
Method, %
|
Line, %
|
Block |
100%
(1/1)
|
37,8%
(14/37)
|
33,6%
(72/214)
|
/*
* (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.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import org.apache.commons.lang3.tuple.Pair;
import org.kitodo.production.helper.Helper;
import org.kitodo.production.model.bibliography.course.metadata.CountableMetadata;
/**
* The class Block is a bean class that represents an interval of time in the
* course of appearance of a newspaper within which it wasn’t suspended. A Block
* instance handles one or more Issue objects.
*/
public class Block {
/**
* The field course holds a reference to the course this block is in.
*/
private final Course course;
/**
* The field variant may hold a variant identifier that can be used to
* distinguish different blocks during the buildup of a course of appearance
* from individual issues.
*
* <p>
* Given a newspaper appeared three times a week for a period of time, and
* then changed to being published six times a week without changing its
* heading, and this change shall be represented by different blocks, the
* variant identifier can be used to distinguish the blocks. Otherwise, both
* time ranges would be represented in one combined block, what would be
* factual correct but would result in a multitude of exceptions, which
* could be undesired.
*/
private final String variant;
/**
* The field firstAppearance holds the date representing the first day of
* the period of time represented by this block. The date is treated as
* inclusive.
*/
private LocalDate firstAppearance;
/**
* The field lastAppearance holds the date representing the last day of the
* period of time represented by this block. The date is treated as
* inclusive.
*/
private LocalDate lastAppearance;
/**
* The field issues holds the issues that have appeared during the period of
* time represented by this block.
*/
private List<Issue> issues;
/**
* Metadata associated with this block.
*/
private final List<CountableMetadata> metadata = new ArrayList<>();
/**
* Default constructor. Creates a Block object without any data.
*
* @param course
* course this block is in
*/
public Block(Course course) {
this.course = course;
this.variant = null;
this.firstAppearance = null;
this.lastAppearance = null;
this.issues = new ArrayList<>();
}
/**
* Constructor for a block with a given variant identifier.
*
* @param course
* course this block is in
* @param variant
* a variant identifier (may be null)
*/
public Block(Course course, String variant) {
this.course = course;
this.variant = variant;
this.firstAppearance = null;
this.lastAppearance = null;
this.issues = new ArrayList<>();
}
/**
* Adds an Issue to this block if it is not already
* present.
*
* @param issue
* Issue to add
* @return true if the set was changed
*/
public boolean addIssue(Issue issue) {
clearProcessesIfNecessary(issue);
return issues.add(issue);
}
/**
* Add a new issue to this block.
*/
public Issue addIssue() {
Issue issue = new Issue(course);
addIssue(issue);
return issue;
}
/**
* Adds a metadata entry to this block.
*
* @param countableMetadata
* metadata to add
*/
public void addMetadata(CountableMetadata countableMetadata) {
metadata.add(0, countableMetadata);
}
/**
* Adds a metadata entry to this block.
*
* @param index
* insert position
* @param countableMetadata
* metadata to add
*/
public void addMetadata(CountableMetadata index, CountableMetadata countableMetadata) {
metadata.add(metadata.indexOf(index) + 1, countableMetadata);
}
/**
* When a course of appearance has been loaded from a file or the processes
* list has already been generated, it already contains issues which must be
* deleted in the case that an issue is added to or removed from the course
* of appearance which is producing issues in the selected time range. If
* the time range cannot be evaluated because either of the variables is
* null, we go the safe way and delete, too.
*
* @param issue
* issue to add or delete
*/
private void clearProcessesIfNecessary(Issue issue) {
try {
if (issue.countIndividualIssues(firstAppearance, lastAppearance) > 0) {
course.clearProcesses();
}
} catch (RuntimeException e) {
// if firstAppearance or lastAppearance is null
course.clearProcesses();
}
}
/**
* Creates and returns a copy of this Block.
*
* @param course
* Course this block belongs to
* @return a copy of this
*/
public Block clone(Course course) {
Block copy = new Block(course);
copy.firstAppearance = firstAppearance;
copy.lastAppearance = lastAppearance;
ArrayList<Issue> copiedIssues = new ArrayList<>(Math.max(issues.size(), 10));
for (Issue issue : issues) {
copiedIssues.add(issue.clone(course));
}
copy.issues = copiedIssues;
return copy;
}
/**
* Determines how many stampings of issues physically appeared without
* generating a list of IndividualIssue objects.
*
* @return the count of issues
*/
public long countIndividualIssues() {
if (Objects.isNull(firstAppearance) || Objects.isNull(lastAppearance)) {
return 0;
}
long numberOfIndividualIssues = 0;
for (LocalDate day = firstAppearance; !day.isAfter(lastAppearance); day = day.plusDays(1)) {
for (Issue issue : getIssues()) {
if (issue.isMatch(day)) {
numberOfIndividualIssues += 1;
}
}
}
return numberOfIndividualIssues;
}
/**
* Deletes a metadata entry from this block.
*
* @param metadata
* entry to remove
*/
public void deleteMetadata(CountableMetadata metadata) {
this.metadata.remove(metadata);
}
/**
* Returns the list of issues contained in this block.
*
* @return the list of issues from this Block
*/
public List<Issue> getIssues() {
return new ArrayList<>(issues);
}
/**
* Generates a list of {@code IndividualIssue} objects for a given day, each
* of them representing a stamping of one physically appeared issue.
*
* @param date
* date to generate issues for
* @return a List of IndividualIssue objects, each of them representing one
* physically appeared issue
*/
public List<IndividualIssue> getIndividualIssues(LocalDate date) {
if (!isMatch(date)) {
return Collections.emptyList();
}
ArrayList<IndividualIssue> result = new ArrayList<>(issues.size());
List<Issue> issues = new ArrayList<>();
for (Issue issue : getIssues()) {
if (issue.isMatch(date)) {
issues.add(issue);
}
}
Integer sorting = issues.size() > 1 ? 1 : null;
for (Issue issue : issues) {
result.add(new IndividualIssue(this, issue, date, Objects.isNull(sorting) ? null : sorting++));
}
return result;
}
/**
* Returns an issue from the Block by the issue’s
* heading, or null if the block doesn’t contain an issue with that heading.
*
* @param heading
* Heading of the issue to look for
* @return Issue with that heading
*/
public Issue getIssue(String heading) {
for (Issue issue : issues) {
if (heading.equals(issue.getHeading())) {
return issue;
}
}
return null;
}
/**
* Returns the date the regularity of this
* block begins with.
*
* @return the date of first appearance
*/
public LocalDate getFirstAppearance() {
return firstAppearance;
}
/**
* Get the date where this block first appeared.
* PrimeFaces 7 requires a java.util.Date object for the datePicker components.
*
* @return date of first appearance as java.util.Date
*/
public Date getFirstAppearanceDate() {
if (Objects.nonNull(firstAppearance)) {
return Date.from(firstAppearance.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant());
} else {
return null;
}
}
/**
* Returns the index of the issue.
*
* @param issue
* issue whose index is to be returned
* @return the index of the issue
*/
public int getIssueIndex(Issue issue) {
return issues.indexOf(issue);
}
/**
* Returns the date the regularity of this block ends with.
*
* @return the date of last appearance
*/
public LocalDate getLastAppearance() {
return lastAppearance;
}
/**
* Get the date where this block last appeared.
* PrimeFaces 7 requires a java.util.Date object for the datePicker components.
*
* @return date of last appearance as java.util.Date
*/
public Date getLastAppearanceDate() {
if (Objects.nonNull(lastAppearance)) {
return Date.from(lastAppearance.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant());
} else {
return null;
}
}
/**
* Returns the metadata assigned to this block.
*
* @return the metadata
*/
public Collection<CountableMetadata> getMetadata() {
return metadata;
}
/**
* Returns all metadata counters from this block for the given metadataType
* that starts on the given day.
*
* @param metadataType
* metadataType to compare
* @param issue
* creation point to compare
* @param create
* if the metadata was created (else deleted)
* @return true, if there is such a counter
*/
public CountableMetadata getMetadata(String metadataType, Pair<LocalDate, Issue> issue, boolean create) {
for (CountableMetadata metaDatum : metadata) {
if (metaDatum.matches(metadataType, issue, create)) {
return metaDatum;
}
}
return null;
}
/**
* Returns true, if there is a counter in this block for the given
* metadataType that starts on the given day.
*
* @param issue
* creation point to compare
* @param create
* if the metadata was created (else deleted)
* @return true, if there is such a counter
*/
public Iterable<CountableMetadata> getMetadata(Pair<LocalDate, Issue> issue, Boolean create) {
List<CountableMetadata> result = new ArrayList<>();
for (CountableMetadata metaDatum : metadata) {
if (metaDatum.matches(null, issue, create)) {
result.add(metaDatum);
}
}
return result;
}
/**
* Returns whether the block is in an empty state or not.
*
* @return whether the block is dataless
*/
public boolean isEmpty() {
return Objects.isNull(firstAppearance) && Objects.isNull(lastAppearance) && (Objects.isNull(issues) || issues.isEmpty());
}
/**
* Can be used to find out whether the given variant string equals the
* variant assigned to this block in a NullPointerException-safe way.
*
* @param variant
* variant to compare against
* @return whether the given string is equals to the assigned variant
*/
public boolean isIdentifiedBy(String variant) {
return Objects.isNull(variant) && Objects.isNull(this.variant)
|| Objects.nonNull(this.variant) && this.variant.equals(variant);
}
/**
* Returns whether a given LocalDate comes within the
* limits of this block. Defaults to false if either the argument or one of
* the fields to compare against is null.
*
* @param date
* a LocalDate to examine
* @return whether the date is within the limits of this block
*/
public boolean isMatch(LocalDate date) {
if (Objects.isNull(firstAppearance) || Objects.isNull(lastAppearance)) {
return false;
}
try {
return !date.isBefore(firstAppearance) && !date.isAfter(lastAppearance);
} catch (IllegalArgumentException e) {
return false;
}
}
/**
* Recalculates 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 (Issue issue : issues) {
issue.recalculateRegularity(firstAppearance, lastAppearance);
}
}
/**
* Removes the specified Issue from this Block if
* it is present.
*
* @param issue
* Issue to be removed from the set
*/
public void removeIssue(Issue issue) {
clearProcessesIfNecessary(issue);
issues.remove(issue);
}
/**
* Check if block has issues with same heading.
* @return 'true' if duplicates are found anf 'false' if not.
*/
public boolean checkIssuesWithSameHeading() {
List<String> issuesTitles = issues.stream().map(Issue::getHeading).collect(Collectors.toList());
List<String> titles = new ArrayList<>();
for (String title : issuesTitles) {
if (titles.contains(title)) {
Helper.setErrorMessage("duplicatedTitles", " (Block " + (course.indexOf(this) + 1) + ")" );
return true;
} else {
titles.add(title);
}
}
return false;
}
/**
* Sets a LocalDate as day of first
* appearance for this Block.
*
* @param firstAppearance
* date of first appearance
* @throws IllegalArgumentException
* if the date would overlap with another block
*/
public void setFirstAppearance(LocalDate firstAppearance) {
try {
prohibitOverlaps(firstAppearance, Objects.nonNull(lastAppearance) ? lastAppearance : firstAppearance);
} catch (IllegalArgumentException e) {
Helper.setErrorMessage(e.getMessage());
}
try {
if (!this.firstAppearance.equals(firstAppearance)) {
course.clearProcesses();
}
} catch (NullPointerException e) {
if (this.firstAppearance == null ^ firstAppearance == null) {
course.clearProcesses();
}
}
this.firstAppearance = firstAppearance;
if (Objects.isNull(lastAppearance)) {
lastAppearance = firstAppearance;
}
}
/**
* Set the date where this block first appeared.
* PrimeFaces 7 passes a java.util.Date object from the datePicker components.
*
* @param firstAppearance the first date of appearance as java.util.Date
*/
public void setFirstAppearanceDate(Date firstAppearance) {
if (Objects.nonNull(firstAppearance)) {
firstAppearance.setHours(5);
setFirstAppearance(firstAppearance.toInstant().atZone(ZoneId.systemDefault()).toLocalDate());
}
}
/**
* Sets a LocalDate as day of last appearance
* for this Block.
*
* @param lastAppearance
* date of last appearance
* @throws IllegalArgumentException
* if the date would overlap with another block
*/
public void setLastAppearance(LocalDate lastAppearance) {
try {
prohibitOverlaps(Objects.nonNull(firstAppearance) ? firstAppearance : lastAppearance, lastAppearance);
} catch (IllegalArgumentException e) {
Helper.setErrorMessage(e.getMessage());
}
try {
if (!this.lastAppearance.equals(lastAppearance)) {
course.clearProcesses();
}
} catch (NullPointerException e) {
if (this.lastAppearance == null ^ lastAppearance == null) {
course.clearProcesses();
}
}
this.lastAppearance = lastAppearance;
if (Objects.isNull(firstAppearance)) {
firstAppearance = lastAppearance;
}
}
/**
* Set the date where this block last appeared.
* PrimeFaces 7 passes a java.util.Date object from the datePicker components.
*
* @param lastAppearance the last date of appearance as java.util.Date
*/
public void setLastAppearanceDate(Date lastAppearance) {
if (Objects.nonNull(lastAppearance)) {
lastAppearance.setHours(5);
setLastAppearance(lastAppearance.toInstant().atZone(ZoneId.systemDefault()).toLocalDate());
}
}
/**
* Sets two LocalDate instances as days of
* first and last appearance for this Block.
*
* @param firstAppearance
* date of first appearance
* @param lastAppearance
* date of last appearance
* @throws IllegalArgumentException
* if the date would overlap with another block
*/
public void setPublicationPeriod(LocalDate firstAppearance, LocalDate lastAppearance) {
prohibitOverlaps(firstAppearance, lastAppearance);
try {
if (!this.firstAppearance.equals(firstAppearance)) {
course.clearProcesses();
}
} catch (NullPointerException e) {
if (this.firstAppearance == null ^ firstAppearance == null) {
course.clearProcesses();
}
}
try {
if (!this.lastAppearance.equals(lastAppearance)) {
course.clearProcesses();
}
} catch (NullPointerException e) {
if (this.lastAppearance == null ^ lastAppearance == null) {
course.clearProcesses();
}
}
this.firstAppearance = firstAppearance;
this.lastAppearance = lastAppearance;
}
/**
* Tests an not yet set time range for this
* block whether it doesn’t overlap with other titles in this course and can
* be set. (Because this method is called prior to setting a new value as a
* field value, it doesn’t take the values from the classes’ fields even
* though it isn’t static.) If the given dates would cause an overlapping,
* an IllegalArgumentException will be thrown.
*
* @param from
* date of first appearance to check
* @param until
* date of last appearance to check
* @throws IllegalArgumentException
* if the check fails
*/
private void prohibitOverlaps(LocalDate from, LocalDate until) {
for (Block block : course) {
if (!block.equals(this)
&& (Objects.nonNull(from) && Objects.nonNull(until))
&& (Objects.nonNull(block.getFirstAppearance()) && Objects.nonNull(block.getLastAppearance()))
&& (block.getFirstAppearance().isBefore(until) && !block.getLastAppearance().isBefore(from)
|| (block.getLastAppearance().isAfter(from) && !block.getFirstAppearance().isAfter(until)))) {
throw new IllegalArgumentException(
'(' + block.variant + ") " + block.firstAppearance + " - " + block.lastAppearance);
}
}
}
/**
* Provides returns a string that contains a concise
* but informative representation of this block that is easy for a person to
* read.
*
* @return a string representation of the block
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
StringBuilder result = new StringBuilder();
if (Objects.nonNull(firstAppearance)) {
result.append(firstAppearance.toString());
}
result.append(" - ");
if (Objects.nonNull(lastAppearance)) {
result.append(lastAppearance.toString());
}
result.append(" [");
boolean first = true;
for (Issue issue : issues) {
if (!first) {
result.append(", ");
}
result.append(issue.toString());
first = false;
}
result.append("]");
return result.toString();
}
/**
* Provides returns a string that contains a textual
* representation of this block that is easy for a person to read.
*
* @param dateConverter
* a DateTimeFormatter for formatting the local dates
* @return a string to identify the block
*/
public String toString(DateTimeFormatter dateConverter) {
StringBuilder result = new StringBuilder();
if (Objects.nonNull(firstAppearance)) {
result.append(dateConverter.format(firstAppearance));
}
result.append(" − ");
if (Objects.nonNull(lastAppearance)) {
result.append(dateConverter.format(lastAppearance));
}
return result.toString();
}
/**
* Returns a hash code for the object which depends on the content of its
* variables. Whenever Block objects are held in HashSet objects, a
* hashCode() is essentially necessary.
*
* <p>
* The method was generated by Eclipse using right-click → Source → Generate
* hashCode() and equals()…. If you will ever change the classes’ fields,
* just re-generate it.
*
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
final int prime = 31;
int hashCode = 1;
hashCode = prime * hashCode + ((firstAppearance == null) ? 0 : firstAppearance.hashCode());
hashCode = prime * hashCode + ((issues == null) ? 0 : issues.hashCode());
hashCode = prime * hashCode + ((lastAppearance == null) ? 0 : lastAppearance.hashCode());
hashCode = prime * hashCode + ((variant == null) ? 0 : variant.hashCode());
return hashCode;
}
/**
* Returns whether two individual issues are equal; the decision depends on
* the content of its variables.
*
* <p>
* The method was generated by Eclipse using right-click → Source → Generate
* hashCode() and equals()…. If you will ever change the classes’ fields,
* just re-generate it.
*
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj instanceof Block) {
Block other = (Block) obj;
if (Objects.isNull(firstAppearance)) {
if (Objects.nonNull(other.firstAppearance)) {
return false;
}
} else if (!firstAppearance.equals(other.firstAppearance)) {
return false;
}
if (Objects.isNull(issues)) {
if (Objects.nonNull(other.issues)) {
return false;
}
} else if (!issues.equals(other.issues)) {
return false;
}
if (Objects.isNull(lastAppearance)) {
if (Objects.nonNull(other.lastAppearance)) {
return false;
}
} else if (!lastAppearance.equals(other.lastAppearance)) {
return false;
}
if (Objects.isNull(variant)) {
return Objects.isNull(other.variant);
} else {
return variant.equals(other.variant);
}
}
return false;
}
}