All Downloads are FREE. Search and download functionalities are using the official Maven repository.

nl.vpro.domain.media.Program Maven / Gradle / Ivy

Go to download

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.

There is a newer version: 8.3.1
Show newest version
package nl.vpro.domain.media;

import lombok.*;
import lombok.extern.slf4j.Slf4j;

import java.io.Serial;
import java.time.Instant;
import java.util.*;

import jakarta.persistence.Entity;
import jakarta.persistence.*;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import jakarta.xml.bind.annotation.*;

import org.checkerframework.checker.nullness.qual.NonNull;
import org.hibernate.annotations.*;

import com.fasterxml.jackson.annotation.*;

import nl.vpro.domain.media.exceptions.CircularReferenceException;
import nl.vpro.domain.media.support.OwnerType;
import nl.vpro.domain.media.support.Workflow;
import nl.vpro.domain.user.*;

import static jakarta.persistence.CascadeType.MERGE;
import static nl.vpro.domain.TextualObjects.sorted;
import static nl.vpro.domain.media.MediaObjectFilters.*;
import static nl.vpro.domain.media.MediaObjectFilters.MR_EMBARGO_FILTER_CONDITION;

/**
 * The main feature that distinguishes a Program from a generic media entity is its ability
 * to become an episode of other media entities. This association type is a functional
 * equivalent of the memberOf association, but complementary, and has its own representation
 * in XML or JSON.
 * 

* A program can have a {@link nl.vpro.domain.media.ProgramType} when it's a movie or strand * program. A strand programs has the ability to become an episode of other strand programs * as opposed to strand groups. *

* Another important distinction is that only programs may contain {@link Segment}s. Also, they themselves know of their segments, * and the XML and JSON representations on programs normally contain all their segments too. * * @author roekoe */ @Entity @XmlRootElement(name = "program") @XmlAccessorType(XmlAccessType.NONE) @XmlType(name = "programType", propOrder = { "scheduleEvents", "episodeOf", "segments", "poProgTypeLegacy" }) @JsonTypeName("program") @Slf4j public final class Program extends MediaObject { @Serial private static final long serialVersionUID = 6174884273805175998L; public static MediaBuilder.ProgramBuilder builder() { return MediaBuilder.program(); } /** * Unset some default values, to ensure that roundtripping will result same object * @since 5.11 */ @JsonCreator static Program jsonCreator() { return builder().workflow(null).creationDate((Instant) null).build(); } @OneToMany(mappedBy = "mediaObject", orphanRemoval = false, // When true, 'hijacking' events doesn't work properly. (see RCRS integeration test in api-tests) cascade={MERGE}) @SortNatural // Caching doesn't work properly because ScheduleEventRepository may touch this // @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) Set<@NotNull @Valid ScheduleEvent> scheduleEvents; // DRS I found that the 'hardcoded' mediaobject alias in the filter below changes when // relational fields are added; I had to change the alias from mediaobjec_9 to mediaobjec_11 // when I added field publicationRule below. Needs to be fixed, not sure how... @OneToMany(orphanRemoval = true) @JoinTable( name = "program_episodeof", inverseJoinColumns = @JoinColumn(name = "id") ) @org.hibernate.annotations.Cascade({ org.hibernate.annotations.CascadeType.ALL }) @SortNatural //@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) @Filter(name = MR_DELETED_FILTER, condition = MR_DELETED_FILTER_CONDITION) @Filter(name = MR_EMBARGO_FILTER, condition = MR_EMBARGO_FILTER_CONDITION) @Filter(name = MR_PUBLICATION_FILTER, condition = MR_PUBLICATION_FILTER_CONDITION) Set episodeOf = new TreeSet<>(); @Size.List({@Size(max = 255), @Size(min = 1)}) private String poProgType; @Setter @Enumerated(EnumType.STRING) @Column(nullable = false) @NotNull(message = "no program type given") private ProgramType type; @OneToMany(mappedBy = "parent", orphanRemoval = false) // no implicit orphan removal, the segment may be subject to 'stealing'. @org.hibernate.annotations.Cascade({ org.hibernate.annotations.CascadeType.ALL }) //@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) // TODO: These filters are horrible @Filter(name = PUBLICATION_FILTER, deduceAliasInjectionPoints = false, aliases = { @SqlFragmentAlias(alias = "segment", table = "MediaObject") }, condition = "(({segment}.publishstart is null or {segment}.publishstart < now())" + "and ({segment}.publishstop is null or {segment}.publishstop > now()))") @Filter(name = DELETED_FILTER, aliases = { @SqlFragmentAlias(alias = "segment", table = "MediaObject") }, deduceAliasInjectionPoints = false, condition = "({segment}.workflow NOT IN ('MERGED', 'FOR_DELETION', 'DELETED') and ({segment}.mergedTo_id is null))") private Set segments; @XmlTransient @Getter(AccessLevel.PACKAGE) @Setter(AccessLevel.PACKAGE) private Boolean pdAuthorityImported; public Program() { } public Program(String mid, long id) { this(id); this.mid = mid; } public Program(String mid) { super(); this.mid = mid; } public Program(long id) { super(id); } public Program(AVType avType, ProgramType type) { this.avType = avType; this.type = type; } @SuppressWarnings("CopyConstructorMissesField") public Program(Program source) { super(source); source.getEpisodeOf().forEach(ref -> this.createEpisodeOf((Group)ref.getGroup(), ref.getNumber(), ref.getOwner())); source.getSegments().forEach(segment -> this.addSegment(Segment.copy(segment))); this.type = source.type; this.poProgType = source.poProgType; source.getScheduleEvents() .forEach(scheduleevent -> this.addScheduleEvent(ScheduleEvent.copy(scheduleevent, this))); } public static Program copy(Program source) { if(source == null) { return null; } return new Program(source); } public boolean hasScheduleEvents() { return scheduleEvents != null && !scheduleEvents.isEmpty(); } @XmlElementWrapper(name = "scheduleEvents") @XmlElement(name = "scheduleEvent") @JsonProperty("scheduleEvents") @JsonInclude(JsonInclude.Include.NON_EMPTY) @JsonManagedReference public SortedSet getScheduleEvents() { if (scheduleEvents == null) { scheduleEvents = new TreeSet<>(); } // return Collections.unmodifiableSortedSet(scheduleEvents); Would be // nice for hibernate, but jaxb gets confused (run ScheduleTest) return sorted(scheduleEvents); } public void setScheduleEvents(SortedSet scheduleEvents) { this.scheduleEvents = scheduleEvents; invalidateSortDate(); } MediaObject addScheduleEvent(ScheduleEvent scheduleEvent) { if (scheduleEvent != null) { if (scheduleEvents == null) { scheduleEvents = new TreeSet<>(); } boolean wasNew = scheduleEvents.add(scheduleEvent); if (! wasNew) { log.debug("Didn't add {}", scheduleEvent); } invalidateSortDate(); } return this; } boolean removeScheduleEvent(ScheduleEvent scheduleEvent) { if (scheduleEvents != null) { return scheduleEvents.remove(scheduleEvent); } return false; } private static boolean isEmpty(Collection collection) { return collection == null || collection.isEmpty(); } public Boolean isEpisodeOfLocked() { if(episodeOf != null) { for(MemberRef memberRef : episodeOf) { MediaObject owner = memberRef.getGroup(); if(owner instanceof Group && ((Group)owner).isEpisodesLocked()) { return true; } } } return false; } @Override void addAncestors(SortedSet set) { super.addAncestors(set); if (isEpisode()) { for (MemberRef memberRef : episodeOf) { if (! memberRef.isVirtual()) { final MediaObject reference = memberRef.getGroup(); if (reference != null && !set.contains(reference)) { set.add(reference); reference.addAncestors(set); } } } } } @Override protected Set getVirtualMemberRefs() { Set result = super.getVirtualMemberRefs(); if (episodeOf != null) { for (MemberRef memberRef : episodeOf) { if (memberRef.isVirtual()) { result.add(memberRef); } } } return result; } @XmlElement @JsonInclude(JsonInclude.Include.NON_EMPTY) @JsonManagedReference public SortedSet<@NonNull MemberRef> getEpisodeOf() { if(this.episodeOf == null) { this.episodeOf = new TreeSet<>(); } for (MemberRef r : episodeOf) { r.setRefType(MemberRefType.episodeOf); } return sorted(episodeOf); } public void setEpisodeOf(SortedSet episodeOf) { this.episodeOf = episodeOf; for (MemberRef r : episodeOf) { r.setMember(this); } } public MemberRef findEpisodeOfRef(long refId) { for(MemberRef memberRef : episodeOf) { if(memberRef.getId().equals(refId)) { return memberRef; } } return null; } public MemberRef findEpisodeOfRef(MediaObject owner) { return MemberRefs.findRef(episodeOf, owner).orElse(null); } public MemberRef findEpisodeOfRef(MediaObject owner, Integer number) { return MemberRefs.findRef(episodeOf, owner, number).orElse(null); } public MemberRef findEpisodeOf(Long episodeRefId) { for(MemberRef episodeRef : episodeOf) { if(episodeRefId.equals(episodeRef.getId())) { return episodeRef; } } return null; } public boolean isEpisode() { return episodeOf != null && episodeOf.size() > 0; } public boolean isEpisodeOf(MediaObject owner) { return MemberRefs.isOf(episodeOf, owner); } @Override public boolean hasAncestor(MediaObject ancestor) { if(super.hasAncestor(ancestor)) { return true; } if(!isEpisode()) { return false; } for(MemberRef memberRef : episodeOf) { if (memberRef.getGroup() != null) { if (Objects.equals(memberRef.getGroup(), ancestor) || memberRef.getGroup().hasAncestor(ancestor)) { return true; } } } return false; } @Override protected void findAncestry(MediaObject ancestor, List ancestors) { super.findAncestry(ancestor, ancestors); if(ancestors.isEmpty() && isEpisode()) { for(MemberRef memberRef : episodeOf) { if(Objects.equals(memberRef.getGroup(), ancestor)) { ancestors.add(ancestor); return; } if (memberRef.getGroup() != null) { memberRef.getGroup().findAncestry(ancestor, ancestors); if (!ancestors.isEmpty()) { ancestors.add(memberRef.getGroup()); return; } } } } } MemberRef createEpisodeOf(Group group, Integer episodeNr, OwnerType owner) throws CircularReferenceException { if(group == null) { throw new IllegalArgumentException("Must supply an owning group, not null."); } if(! ProgramType.EPISODES.contains(this.getType())) { throw new IllegalArgumentException( String.format("%1$s of type %2$s can not become an episode of %3$s with type %4$s (should be one of %5$s)", this, this.getType(), group, group.getType(), ProgramType.EPISODES)); } if(! group.getType().canContainEpisodes()) { throw new IllegalArgumentException("Must supply a group type " + GroupType.EPISODE_CONTAINERS + " when adding episodes."); } if(group.hasAncestor(this)) { throw new CircularReferenceException(this, group, group.findAncestry(this)); } MemberRef memberRef = new MemberRef(this, group, episodeNr, owner); if(episodeOf == null) { episodeOf = new TreeSet<>(); } episodeOf.add(memberRef); return memberRef; } boolean removeEpisodeOf(MediaObject owner) { boolean success = false; if(episodeOf != null) { Iterator it = episodeOf.iterator(); while(it.hasNext()) { MemberRef memberRef = it.next(); if(Objects.equals(memberRef.getGroup(), owner)) { it.remove(); success = true; } } } return success; } boolean removeEpisodeOf(MemberRef memberRef) { if(episodeOf != null) { Iterator it = episodeOf.iterator(); while(it.hasNext()) { MemberRef existing = it.next(); if(existing.equals(memberRef)) { it.remove(); descendantOf = null; return true; } } } return false; } public String getPoProgType() { return poProgType; } @XmlElement(name = "poProgType") public String getPoProgTypeLegacy() { return null; } public void setPoProgTypeLegacy(String poProgType) { this.poProgType = (poProgType == null || poProgType.length() < 255) ? poProgType : poProgType.substring(255); } @XmlAttribute(required = true) @Override public ProgramType getType() { return type; } @Override public void setMediaType(MediaType type) { setType(type == null ? null : (ProgramType) type.getSubType()); } @XmlElementWrapper(name = "segments") @XmlElement(name = "segment") @JsonProperty("segments") @JsonInclude(JsonInclude.Include.NON_EMPTY) @NonNull public SortedSet getSegments() { if(segments == null) { segments = new TreeSet<>(); } return sorted(segments); } public void setSegments(SortedSet segments) { this.segments = segments; } public Segment findSegment(Long id) { if(segments == null) { return null; } for(Segment segment : segments) { if(id.equals(segment.getId())) { return segment; } } return null; } Optional findSegment(Segment segment) { return getSegments().stream().filter(existing -> existing.equals(segment)).findFirst(); } public Program addSegment(Segment segment) { if(segment != null) { segment.setParent(this); if(isEmpty(segment.getBroadcasters()) && !isEmpty(broadcasters)) { for(Broadcaster broadcaster : broadcasters) { segment.addBroadcaster(broadcaster); } } if(isEmpty(segment.getPortals()) && !isEmpty(getPortals())) { for(Portal portal : getPortals()) { segment.addPortal(portal); } } if(isEmpty(segment.getThirdParties()) && !isEmpty(getThirdParties())) { for(ThirdParty thirdParty : getThirdParties()) { segment.addThirdParty(thirdParty); } } if(segments == null) { segments = new TreeSet<>(); } segments.add(segment); } return this; } public boolean deleteSegment(Segment segment) { if(segments == null) { return false; } return findSegment(segment).map((existing) -> { existing.setWorkflow(Workflow.FOR_DELETION); return true; } ).orElse(false); } @Override protected String getUrnPrefix() { return ProgramType.URN_PREFIX; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy