nl.vpro.domain.media.ScheduleEvent Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of media-domain Show documentation
Show all versions of media-domain Show documentation
The basic domain classes for 'media', the core of POMS. Also, the 'update' XML bindings for it.
It also contains some closely related domain classes like the enum to contain NICAM kijkwijzer settings.
package nl.vpro.domain.media;
import lombok.Setter;
import lombok.Singular;
import java.io.Serial;
import java.io.Serializable;
import java.time.*;
import java.util.*;
import jakarta.persistence.*;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import jakarta.xml.bind.Unmarshaller;
import jakarta.xml.bind.annotation.*;
import jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.*;
import org.hibernate.validator.constraints.time.DurationMin;
import org.meeuw.functional.TriFunction;
import com.fasterxml.jackson.annotation.*;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.google.common.collect.Range;
import nl.vpro.domain.*;
import nl.vpro.domain.media.bind.NetToString;
import nl.vpro.domain.media.support.*;
import nl.vpro.jackson2.*;
import nl.vpro.persistence.LocalDateToDateConverter;
import nl.vpro.xml.bind.*;
import static jakarta.persistence.CascadeType.ALL;
import static nl.vpro.domain.TextualObjects.sorted;
@Entity
@IdClass(ScheduleEventIdentifier.class)
@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
@FilterDef(name = MediaObjectFilters.SE_DELETED_FILTER)
@Filter(name = MediaObjectFilters.SE_DELETED_FILTER, condition = "(select m.workflow from mediaobject m where m.id = mediaobject_id and m.mergedTo_id is null) NOT IN ('MERGED', 'DELETED')")
@XmlAccessorType(XmlAccessType.NONE)
@XmlType(name = "scheduleEventType", propOrder = {
"titles",
"descriptions",
"repeat",
"memberOf",
"avAttributes",
"textSubtitles",
"textPage",
"guideDate",
"startInstant",
"offset",
"duration",
"poProgID",
"poSeriesIDLegacy",
"primaryLifestyle",
"secondaryLifestyle"
})
@JsonPropertyOrder({
"titles",
"descriptions",
"channel",
"startInstant",
"guideDate",
"duration",
"midRef",
"poProgID",
"repeat",
"memberOf",
"avAttributes",
"textSubtitles",
"textPage",
"offset",
"poSeriesIDLegacy",
"primaryLifestyle",
"secondaryLifestyle"
})
public class ScheduleEvent implements Serializable, Identifiable,
Comparable,
TextualObject,
Child {
@Serial
private static final long serialVersionUID = 2107980433596776633L;
@Id
@Enumerated(EnumType.STRING)
@NotNull
protected Channel channel;
@Id
@NotNull
protected Instant start;
protected Instant effectiveStart;
@Setter
@ManyToOne
@Valid
protected Net net;
@Column(nullable = false, name = "guideDay", columnDefinition="Date")
@Convert(converter = LocalDateToDateConverter.class)
protected LocalDate guideDay;
@Setter
@Embedded
protected Repeat repeat;
@Setter
@Deprecated
protected String memberOf;
@Setter
@OneToOne(orphanRemoval = true, cascade = ALL)
protected AVAttributes avAttributes;
@Setter
protected String textSubtitles;
@Setter
protected String textPage;
@Setter
@Column(name = "start_offset")
@JsonSerialize(using = DurationToJsonTimestamp.Serializer.class)
@JsonDeserialize(using = DurationToJsonTimestamp.Deserializer.class)
@JsonInclude(JsonInclude.Include.NON_NULL)
protected Duration offset;
@Setter
@JsonSerialize(using = DurationToJsonTimestamp.Serializer.class)
@JsonDeserialize(using = DurationToJsonTimestamp.Deserializer.class)
@JsonInclude(JsonInclude.Include.NON_NULL)
@DurationMin // negative durations don't make sense
protected Duration duration;
@Setter
protected String imi;
protected String guci;
@Setter
@Transient
protected String urnRef;
@ManyToOne(fetch = FetchType.EAGER, optional = false)
@JsonBackReference
protected Program mediaObject;
@Setter
@Enumerated(EnumType.STRING)
@Deprecated
protected ScheduleEventType type;
@Setter
@Embedded
@Column(name = "primary")
@XmlElement
protected Lifestyle primaryLifestyle;
@Setter
@Embedded
@Column(name = "secondary")
@XmlElement
protected SecondaryLifestyle secondaryLifestyle;
@Setter
@Transient
protected String midRef;
@Setter
protected String poSeriesID;
@OneToMany(mappedBy = "parent", orphanRemoval = true, cascade = ALL)
@Valid
@XmlElement(name = "title")
@JsonProperty("titles")
protected Set<@Valid @NotNull ScheduleEventTitle> titles = new TreeSet<>();
@OneToMany(mappedBy = "parent", orphanRemoval = true, cascade = ALL)
@Valid
@XmlElement(name = "description")
@JsonProperty("descriptions")
protected Set<@Valid @NotNull ScheduleEventDescription> descriptions = new TreeSet<>();
public ScheduleEvent() {
}
public ScheduleEvent(Channel channel, Instant start, Duration duration) {
this(channel, null, guideLocalDate(start), start, duration, null);
}
public ScheduleEvent(Channel channel, Net net, Instant start, Duration duration) {
this(channel, net, guideLocalDate(start), start, duration, null);
}
public ScheduleEvent(Channel channel, LocalDate guideDay, Instant start, Duration duration) {
this(channel, null, guideDay, start, duration, null);
}
public ScheduleEvent(Channel channel, LocalDateTime start, Duration duration) {
this(channel, null, Schedule.guideDay(start), start.atZone(Schedule.ZONE_ID).toInstant(), duration, null);
}
public ScheduleEvent(Channel channel, Net net, LocalDate guideDay, Instant start, Duration duration) {
this(channel, net, guideDay, start, duration, null);
}
public ScheduleEvent(Channel channel, Instant start, Duration duration, Program media) {
this(channel, null, guideLocalDate(start), start, duration, media);
}
public ScheduleEvent(Channel channel, Net net, Instant start, Duration duration, Program media) {
this(channel, net, guideLocalDate(start), start, duration, media);
}
public ScheduleEvent(
@NonNull Channel channel,
@Nullable Net net,
@Nullable LocalDate guideDay,
@NonNull Instant start,
@NonNull Duration duration,
@Nullable Program media) {
this(channel, net, guideDay, start, duration, null, media, null, null, null, null, null, null, null, null, null, null, null);
}
@lombok.Builder(builderClassName = "Builder")
private ScheduleEvent(
@NonNull Channel channel,
@Nullable Net net,
@Nullable LocalDate guideDay,
@NonNull Instant start,
@NonNull Duration duration,
String midRef,
@Nullable Program media,
@Nullable Repeat repeat,
@Nullable Lifestyle primaryLifestyle,
@Nullable SecondaryLifestyle secondaryLifestyle,
@Singular Set titles,
@Singular Set descriptions,
@Nullable AVAttributes avAttributes,
@Nullable String textPage,
@Nullable String textSubtitles,
@Nullable String guci,
@Nullable Instant effectiveStart,
@Nullable Duration offset
) {
this.channel = channel;
this.net = net;
this.guideDay = guideDay == null ? guideLocalDate(start) : guideDay;
this.start = start;
this.duration = duration;
this.repeat = Repeat.nullIfDefault(repeat);
this.midRef = midRef;
this.primaryLifestyle = primaryLifestyle;
this.secondaryLifestyle = secondaryLifestyle;
if (titles != null) {
for (ScheduleEventTitle st : titles) {
st.setParent(this);
this.titles.add(st);
}
}
if (descriptions != null) {
for (ScheduleEventDescription sd : descriptions) {
sd.setParent(this);
this.descriptions.add(sd);
}
}
this.avAttributes = avAttributes;
this.textPage = textPage;
this.textSubtitles = textSubtitles;
this.guci = guci;
this.effectiveStart = effectiveStart;
if (effectiveStart != null && offset == null) {
this.offset = Duration.between(start, effectiveStart);
} else {
this.offset = offset;
}
setParent(media);
}
public ScheduleEvent(ScheduleEvent source) {
this(source, source.mediaObject);
}
public ScheduleEvent(ScheduleEvent source, Program parent) {
this.channel = source.channel;
this.start = source.start;
copyFrom(source);
this.mediaObject = parent;
}
/**
* @since 8.2
*/
public void copyFrom(ScheduleEvent source) {
copyFrom(source, null);
}
/**
* @since 8.2
*/
public void copyFrom(ScheduleEvent source, @Nullable OwnerType ownerType) {
this.net = source.net;
this.guideDay = source.guideDay;
this.repeat = Repeat.copy(source.repeat);
this.memberOf = source.memberOf;
this.avAttributes = AVAttributes.copy(source.avAttributes);
this.textSubtitles = source.textSubtitles;
this.textPage = source.textPage;
this.offset = source.offset;
this.duration = source.duration;
this.imi = source.imi;
this.guci = source.guci;
this.type = source.type;
this.primaryLifestyle = Lifestyle.copy(source.primaryLifestyle);
this.secondaryLifestyle = SecondaryLifestyle.copy(source.secondaryLifestyle);
this.poSeriesID = source.poSeriesID;
this.effectiveStart = source.effectiveStart;
if (ownerType == null) {
TextualObjects.copyAndRemove(source, this);
} else {
TextualObjects.copyAndRemove(source, this, ownerType);
}
}
public static ScheduleEvent copy(ScheduleEvent source) {
if (source == null) {
return null;
}
return copy(source, source.mediaObject);
}
public static ScheduleEvent copy(ScheduleEvent source, Program parent) {
if (source == null) {
return null;
}
return new ScheduleEvent(source, parent);
}
public static ScheduleEvent of(Instant start) {
return ScheduleEvent.builder().start(start).build();
}
private static Date guideDay(Date start) {
if (start == null) {
return null;
}
return Date.from(guideLocalDate(start).atTime(Schedule.START_OF_SCHEDULE).atZone(Schedule.ZONE_ID).toInstant());
}
private static LocalDate guideLocalDate(Date start) {
if (start == null) {
return null;
}
return guideLocalDate(start.toInstant());
}
private static LocalDate guideLocalDate(Instant start) {
return Schedule.guideDay(start);
}
private static Duration duration(Date duration) {
if (duration == null) {
return null;
}
return Duration.ofMillis(duration.getTime());
}
private static Instant instant(Date instant) {
if (instant == null) {
return null;
}
return instant.toInstant();
}
@XmlElement
public Repeat getRepeat() {
return repeat;
}
@JsonView({Views.Publisher.class})
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
public boolean isRerun() {
if (repeat == null) {
return false;
} else {
return repeat.isRerun;
}
}
/**
* @deprecated Last non null value is from 2015
*/
@XmlElement
@Deprecated
public String getMemberOf() {
return memberOf;
}
/**
* I think in principle some av-attributes (like the aspect ratio) may vary for different schedule events.
*/
@XmlElement
public AVAttributes getAvAttributes() {
return avAttributes;
}
@XmlElement
public String getTextSubtitles() {
return textSubtitles;
}
@XmlElement
public String getTextPage() {
return textPage;
}
@XmlElement(name = "guideDay")
@XmlJavaTypeAdapter(ZonedLocalDateXmlAdapter.class)
@XmlSchemaType(name = "date")
@JsonDeserialize(using = StringZonedLocalDateToJsonTimestamp.Deserializer.class)
@JsonSerialize(using = StringZonedLocalDateToJsonTimestamp.Serializer.class)
public LocalDate getGuideDate() {
return guideDay;
}
public void setGuideDate(LocalDate guideDate) {
this.guideDay = guideDate;
}
@XmlElement(name = "start")
@XmlSchemaType(name = "dateTime")
@XmlJavaTypeAdapter(InstantXmlAdapter.class)
@JsonDeserialize(using = StringInstantToJsonTimestamp.Deserializer.class)
@JsonSerialize(using = StringInstantToJsonTimestamp.Serializer.class)
public Instant getStartInstant() {
return start;
}
public void setStartInstant(Instant start) {
this.start = start;
}
@JsonView({Views.Publisher.class})
// Because of other 'start' fields (e.g. in segment, it is mapped to _long_). This field is mapped to date in ES. In ES fields with same name must have same mapping.
@JsonProperty(access = JsonProperty.Access.READ_ONLY, value = "eventStart")
protected Instant getEventStart() {
return getStartInstant();
}
public Instant getStopInstant() {
return start.plus(getDuration());
}
public void setStopInstant(Instant stop) {
this.duration = Duration.between(start, stop);
}
@XmlTransient
public Instant getRealStartInstant() {
if (start == null) {
return null;
}
if (offset == null) {
return start;
}
return start.plus(offset);
}
@XmlElement
@XmlJavaTypeAdapter(DurationXmlAdapter.class)
@JsonIgnore
public Duration getOffset() {
return offset;
}
/**
* @since 4.3
*/
@XmlElement(name = "duration")
@XmlJavaTypeAdapter(DurationXmlAdapter.class)
@JsonIgnore
public Duration getDuration() {
return duration;
}
@XmlAttribute
public Channel getChannel() {
return channel;
}
public void setChannel(Channel channel) {
if (this.channel != null && channel != null && channel != this.channel) {
throw new IllegalStateException();
}
this.channel = channel;
}
@XmlAttribute
@JsonSerialize(using = NetToString.class)
@JsonInclude(JsonInclude.Include.NON_NULL)
public Net getNet() {
return net;
}
@XmlAttribute
public String getImi() {
return imi;
}
@XmlAttribute(required = true)
public String getUrnRef() {
if (urnRef == null && mediaObject != null) {
return mediaObject.getUrn();
}
return urnRef;
}
@XmlAttribute(required = true)
public String getMidRef() {
if (this.midRef == null && mediaObject != null) {
return mediaObject.getMid();
}
return midRef;
}
@Override
@XmlTransient
public Program getParent() {
return mediaObject;
}
@Override
public void setParent(Program mediaObject) {
if (this.mediaObject != null) {
this.mediaObject.removeScheduleEvent(this);
}
this.mediaObject = mediaObject;
if (mediaObject != null) {
mediaObject.addScheduleEvent(this);
}
}
@XmlTransient
@Override
public ScheduleEventIdentifier getId() {
if (channel != null && start != null) {
return createId();
} else {
// schedule event has no proper id (yet?)
return null;
}
}
protected ScheduleEventIdentifier createId() {
ScheduleEventIdentifier id = new ScheduleEventIdentifier(); // avoid @NonNull validation
id.start = start;
id.channel = channel;
return id;
}
@XmlAttribute
@Deprecated
public ScheduleEventType getType() {
return type;
}
@XmlElement
public String getPoProgID() {
return getMidRef();
}
public void setPoProgID(String poProgID) {
setMidRef(poProgID);
}
@XmlTransient
public String getPoSeriesID() {
return poSeriesID;
}
@XmlElement(name = "poSeriesID")
public String getPoSeriesIDLegacy() {
return null;
}
public void setPoSeriesIDLegacy(String poSeriesID) {
this.poSeriesID = poSeriesID;
}
public void clearMediaObject() {
if (this.mediaObject != null) {
this.mediaObject.removeScheduleEvent(this);
this.mediaObject = null;
}
}
public Lifestyle getPrimaryLifestyle() {
return primaryLifestyle;
}
public SecondaryLifestyle getSecondaryLifestyle() {
return secondaryLifestyle;
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder();
sb.append("ScheduleEvent");
sb.append("{channel=").append(channel);
if (start != null) {
sb.append(", start=").append(start.atZone(Schedule.ZONE_ID).toLocalDateTime());
}
if (mediaObject != null) {
sb.append(", mediaObject=").append(mediaObject.getMid() == null ? "(no mid)" : mediaObject.getMid()); // it seems that the title may be lazy, so just show mid of media object.
}
if (repeat != null && repeat.isRerun) {
sb.append(", RERUN");
}
sb.append('}');
return sb.toString();
}
/**
* Schedule events are sorted by start, if those are equal then on channel
*/
@Override
public int compareTo(ScheduleEvent o) {
if (o == this) {
return 0;
}
return createId().compareTo(o.createId());
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
ScheduleEvent that = (ScheduleEvent) o;
if (getId() != null) {
return Objects.equals(getId(), that.getId());
} else {
return hashCode() == that.hashCode();
}
}
@Override
public int hashCode() {
if (getId() != null) {
return getId().hashCode();
} else {
return Objects.hash(channel, start, duration, net, imi);
}
}
void afterUnmarshal(Unmarshaller unmarshaller, Object parent) {
if (parent instanceof Program) {
this.mediaObject = (Program) parent;
}
if (guideDay == null && start != null) {
guideDay = guideLocalDate(start);
}
}
/**
* The titles associated with the schedule event.
*/
@Override
public SortedSet getTitles() {
if (titles == null) {
titles = new TreeSet<>();
}
return sorted(titles);
}
@Override
public void setTitles(SortedSet titles) {
this.titles = titles;
}
@Override
public TriFunction getOwnedTitleCreator() {
return (value, ownerType, textualType) -> new ScheduleEventTitle(ScheduleEvent.this, value, ownerType, textualType);
}
@Override
public TriFunction getOwnedDescriptionCreator() {
return (value, ownerType, textualType) -> new ScheduleEventDescription(ScheduleEvent.this, value, ownerType, textualType);
}
@Override
public ScheduleEvent addTitle(ScheduleEventTitle title) {
title.setParent(this);
return TextualObject.super.addTitle(title);
}
@Override
public SortedSet getDescriptions() {
if (descriptions == null) {
descriptions = new TreeSet<>();
}
return sorted(descriptions);
}
@Override
public void setDescriptions(SortedSet descriptions) {
this.descriptions = descriptions;
}
@Override
public ScheduleEvent addDescription(ScheduleEventDescription description) {
description.setParent(this);
return TextualObject.super.addDescription(description);
}
/**
* Overriden to help hibernate search (see MediaSearchMappingFactory)
*/
@Override
public String getMainTitle() {
return TextualObject.super.getMainTitle();
}
/**
* Overriden to help hibernate search (see MediaSearchMappingFactory)
*/
@Override
public String getSubTitle() {
return TextualObject.super.getSubTitle();
}
/**
* Overriden to help hibernate search (see MediaSearchMappingFactory)
*/
@Override
public String getMainDescription() {
return TextualObject.super.getMainDescription();
}
public Range asRange() {
return Range.closedOpen(start, start.plus(duration));
}
public void setRange(Range range) {
this.start = range.lowerEndpoint();
this.duration = Duration.between(range.lowerEndpoint(), range.upperEndpoint());
}
public static class Builder {
public Builder localStart(int year, int month, int day, int hour, int minute) {
return localStart(LocalDateTime.of(year, month, day, hour, minute));
}
public Builder localStart(LocalDateTime localDateTime) {
return start(localDateTime.atZone(Schedule.ZONE_ID).toInstant());
}
public Builder rerun(boolean b) {
return repeat(b ? Repeat.rerun() : Repeat.original());
}
public Builder rerun(String text) {
return repeat(Repeat.rerun(text));
}
public Builder mainTitle(String title) {
return title(ScheduleEventTitle.builder().title(title).type(TextualType.MAIN).owner(OwnerType.BROADCASTER).build());
}
public Builder mainDescription(String description) {
return description(ScheduleEventDescription.builder().title(description).type(TextualType.MAIN).owner(OwnerType.BROADCASTER).build());
}
}
}