nl.vpro.domain.media.update.MediaUpdate Maven / Gradle / Ivy
Show all versions of media-domain Show documentation
/*
* Copyright (C) 2012 Licensed under the Apache License, Version 2.0
* VPRO The Netherlands
*/
package nl.vpro.domain.media.update;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import java.time.Instant;
import java.util.*;
import java.util.function.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.*;
import jakarta.xml.bind.Marshaller;
import jakarta.xml.bind.Unmarshaller;
import jakarta.xml.bind.annotation.*;
import jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import org.checkerframework.checker.nullness.qual.*;
import org.meeuw.i18n.regions.bind.jaxb.Code;
import com.fasterxml.jackson.annotation.*;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import nl.vpro.domain.*;
import nl.vpro.domain.media.Location;
import nl.vpro.domain.media.TwitterRef;
import nl.vpro.domain.media.*;
import nl.vpro.domain.media.exceptions.CircularReferenceException;
import nl.vpro.domain.media.exceptions.ModificationException;
import nl.vpro.domain.media.support.*;
import nl.vpro.domain.media.update.bind.LanguageList;
import nl.vpro.domain.media.update.bind.UsedLanguageUpdateAdapter;
import nl.vpro.domain.user.Broadcaster;
import nl.vpro.domain.user.Portal;
import nl.vpro.domain.user.validation.BroadcasterValidation;
import nl.vpro.domain.validation.ValidEmbargo;
import nl.vpro.i18n.validation.MustDisplay;
import nl.vpro.jackson2.StringInstantToJsonTimestamp;
import nl.vpro.util.*;
import nl.vpro.validation.*;
import nl.vpro.xml.bind.DurationXmlAdapter;
import nl.vpro.xml.bind.InstantXmlAdapter;
/**
* A MediaUpdate is meant for communicating updates. It is not meant as a complete representation of the object.
*
* A MediaUpdate is like a {@link MediaObject} but
*
* - It does not have {@link MutableOwnable} objects. When converting between a MediaUpdate and a MediaObject one need to indicate for which owner type this must happen.
* If you are updating you are always associated with a certain owner (normally {@link OwnerType#BROADCASTER}), so there is no case for updating fields of other owners.
*
* - It contains fewer implicit fields. E.g. a Broadcaster is just an id, and it does not contain a better string representation.
* These kind of fields are non modifiable, or are implicitely calculated. So there is no case in updating them.
*
* - It may contain a 'version'
* Some code may check this version to know whether certain fields ought to be ignored or not. This is to arrange forward and backwards compatibility.
* It may e.g. happen that a newer version of POMS has a new field. If you are not aware of this, sending an update XML without the field may result in the value to be emptied.
* To indicate that you are aware, you should sometimes supply a sufficiently high version.
*
*
*
* As {@link MediaObject} it has three extensions {@link ProgramUpdate}, {@link GroupUpdate} and {@link SegmentUpdate}
*
* @param The {@link MediaObject} extension this is for.
*
* @see nl.vpro.domain.media.update
* @see MediaObject
*/
@SuppressWarnings("LombokSetterMayBeUsed")
@XmlAccessorType(XmlAccessType.PROPERTY)
@XmlSeeAlso({SegmentUpdate.class, ProgramUpdate.class, GroupUpdate.class})
@Slf4j
@XmlTransient
@ValidEmbargo(groups = WarningValidatorGroup.class)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "objectType")
@JsonSubTypes({
@JsonSubTypes.Type(value = ProgramUpdate.class, name = "programUpdate"),
@JsonSubTypes.Type(value = GroupUpdate.class, name = "groupUpdate"),
@JsonSubTypes.Type(value = SegmentUpdate.class, name = "segmentUpdate") }
)
@JsonPropertyOrder({ "objectType",
/* xml attributes */
"mid",
"type",
"avType",
"workflow",
"mergedTo",
"sortDate",
"creationDate",
"lastModified",
"publishStart",
"publishStop",
"urn",
"embeddable",
/* xml elements */
"episodeOf",
"crids",
"broadcasters",
"portals",
"portalRestrictions",
"geoRestrictions",
"titles",
"expandedTitles",
"descriptions",
"genres",
"tags",
"intentions",
"expandedIntentions",
"targetGroups",
"expandedTargetGroups",
"geoLocations",
"expandedGeoLocations",
"topics",
"expandedTopics",
"source",
"hasSubtitles",
"countries",
"languages",
"isDubbed",
"availableSubtitles",
"avAttributes",
"releaseYear",
"duration",
"persons",
"awards",
"descendantOf",
"memberOf",
"ageRating",
"contentRatings",
"email",
"websites",
"twitter",
"teletext",
"predictionsForXml",
"locations",
"relations",
"images"}
)
@HasTitle(
groups = PomsValidatorGroup.class,
message = "{nl.vpro.constraints.hassubormaintitle}",
type = {TextualType.SUB, TextualType.MAIN}
)
@HasTitle(
groups = WarningValidatorGroup.class,
type = {TextualType.MAIN}
)
@HasGenre(
groups = WarningValidatorGroup.class
)
@AVTypeValidation
public abstract sealed class MediaUpdate
implements
MutableEmbargo>,
TextualObjectUpdate>,
IntegerVersionSpecific,
MediaIdentifiable permits ProgramUpdate, GroupUpdate, SegmentUpdate {
public static @PolyNull MediaUpdate create(@PolyNull M object, OwnerType owner) {
if (object == null) {
return null;
}
MediaUpdate created;
if (object instanceof Program program) {
created = (MediaUpdate) ProgramUpdate.create(program, owner);
} else if (object instanceof Group group) {
created = (MediaUpdate) GroupUpdate.create(group, owner);
} else if (object instanceof Segment segment) {
created = (MediaUpdate) SegmentUpdate.create(segment, owner);
} else {
throw new IllegalArgumentException("%s should be Program Group or Segment".formatted(object));
}
return created;
}
public static @PolyNull MediaUpdate create(@PolyNull M object) {
return create(object, OwnerType.BROADCASTER);
}
public static @PolyNull MediaUpdate create(@PolyNull M object, OwnerType owner, IntegerVersion version) {
MediaUpdate update = create(object, owner);
if (update != null) {
update.setVersion(version);
}
return update;
}
public static > MediaUpdate createUpdate(MB object, OwnerType ownerType) {
return create(object.build(), ownerType);
}
protected IntegerVersion version;
protected boolean xmlVersion = true;
@Valid
protected MediaObject mediaObjectToValidate;
protected String mid;
@Deprecated(since = "7.7")
@Pattern(regexp = "^urn:vpro:media:(?:group|program|segment):[0-9]+$")
protected String urn;
private List<@NotNull @CRID String> crids;
protected AVType avType;
protected Boolean embeddable = Boolean.TRUE;
Boolean isDeleted;
List countries;
List<@NotNull @Valid UsedLanguage> languages;
AVAttributesUpdate avAttributes;
Instant publishStart;
Instant publishStop;
java.time.Duration duration;
Short releaseYear;
@NotNull(groups = {WarningValidatorGroup.class })
@MustDisplay(groups = {PomsValidatorGroup.class})
AgeRating ageRating;
List<@NotNull ContentRating> contentRatings;
List<@NotNull @Email(message = "{nl.vpro.constraints.Email.message}") String> email;
protected List<@NotNull @Valid ImageUpdate> images;
/**
* This represents the editable intentions
* Only display the intentions for the given owner
* (more intentions might be present in the metadata).
*/
protected List<@NotNull IntentionType> intentions;
protected List<@NotNull TargetGroupType> targetGroups;
protected List<@NotNull @Valid GeoLocationUpdate> geoLocations;
private List<@Valid TopicUpdate> topics;
@Valid
protected Asset asset;
private List<
@NotNull
@Size(min = 2, max = 4, message = "2 < id < 5")
@BroadcasterValidation
@jakarta.validation.constraints.Pattern(regexp = "[A-Z0-9_-]{2,4}", message = "Broadcaster id ${validatedValue} should match {regexp}")
String> broadcasters;
private List<@NotNull String> portals;
private SortedSet<
@NotNull
@Size(min = 1, max = 255)
String> tags;
private List<@NotNull @Valid CreditsUpdate> credits;
private List<@NotNull @Valid PortalRestrictionUpdate> portalRestrictions;
private SortedSet<@NotNull @Valid GeoRestrictionUpdate> geoRestrictions;
private SortedSet<@NotNull @Valid TitleUpdate> titles;
private SortedSet<@NotNull @Valid DescriptionUpdate> descriptions;
private SortedSet<
@NotNull
@Pattern(regexp = "3\\.([0-9]+\\.)*[0-9]+")
String> genres;
private SortedSet<@NotNull MemberRefUpdate> memberOf;
private List<
@NotNull
@URI(message = "{nl.vpro.constraints.URI}",
mustHaveScheme = true,
minHostParts = 2,
groups = WarningValidatorGroup.class
)
@Size(min = 1, message = "{nl.vpro.constraints.text.Size.min}")
@Size(max = 255, message = "{nl.vpro.constraints.text.Size.max}")
String> websites;
private List<@NotNull @Pattern(message = "{nl.vpro.constraints.twitterRefs.Pattern}", regexp="^[@#][A-Za-z0-9_]{1,139}$") String> twitterrefs;
private SortedSet<@NotNull @Valid LocationUpdate> locations;
private SortedSet<@NotNull @Valid RelationUpdate> relations;
protected SortedSet<@NotNull @Valid PredictionUpdate> predictions;
@Getter
@JsonIgnore
boolean imported = false;
@XmlTransient
protected boolean fromXml = false;
protected MediaUpdate() {
//
}
protected MediaUpdate(IntegerVersion version, M mediaobject, OwnerType ownerType) {
this.version = version;
fillFromMedia(mediaobject, ownerType);
fillFrom(mediaobject, ownerType);
}
//Part of the process of creating a MediaUpdate from a MediaObject
protected final void fillFromMedia(final M mediaobject, final OwnerType owner) {
this.mid = mediaobject.getMid();
this.urn = mediaobject.getUrn();
this.crids = mediaobject.getCrids();
this.avType = mediaobject.getAVType();
this.embeddable = mediaobject.isEmbeddable();
this.isDeleted = mediaobject.isDeleted();
this.countries = mediaobject.getCountries();
this.languages = mediaobject.getLanguages();
this.avAttributes = AVAttributesUpdate.of(mediaobject.getAvAttributes());
Embargos.copy(mediaobject, this);
this.duration = AuthorizedDuration.duration(mediaobject.getDuration());
this.releaseYear = mediaobject.getReleaseYear();
this.ageRating = mediaobject.getAgeRating();
this.contentRatings = mediaobject.getContentRatings();
this.images = toList(
mediaobject.getImages(),
(i) -> i.getOwner() == owner && ! i.isDeleted(),
ImageUpdate::new,
false)
;
// asset?
this.broadcasters = toList(mediaobject.getBroadcasters(), Broadcaster::getId);
this.portals = toList(mediaobject.getPortals(), Portal::getId);
this.tags = toSet(mediaobject.getTags(), Tag::getText);
this.credits = toCreditsUpdate(mediaobject.getCredits());
if (this.credits.isEmpty()) {
this.credits = null;
}
if (isNotBefore(5, 11)) {
this.intentions = toUpdateIntentions(mediaobject.getIntentions(), owner);
this.targetGroups = toUpdateTargetGroups(mediaobject.getTargetGroups(), owner);
}
if (isNotBefore(5, 12)) {
this.portalRestrictions = toList(mediaobject.getPortalRestrictions(), PortalRestrictionUpdate::new);
this.geoRestrictions = toSet(mediaobject.getGeoRestrictions(), GeoRestrictionUpdate::new);
}
if (owner == null || owner == OwnerType.BROADCASTER) {
TextualObjects.copyToUpdate(mediaobject, this);
} else {
TextualObjects.copyToUpdate(mediaobject, this, owner);
}
this.genres = toSet(mediaobject.getGenres(), Genre::getTermId);
this.memberOf = toSet(mediaobject.getMemberOf(), MemberRefUpdate::create);
this.websites = toList(mediaobject.getWebsites(), Website::get);
this.twitterrefs= toList(mediaobject.getTwitterRefs(), TwitterRef::get);
this.email = toList(mediaobject.getEmail(), nl.vpro.domain.media.Email::get);
this.locations = toSet(mediaobject.getLocations(), (l) -> l.getOwner() == owner && ! l.isDeleted(), LocationUpdate::new);
this.relations = toSet(mediaobject.getRelations(), RelationUpdate::new);
this.predictions = toSet(mediaobject.getPredictions(), Prediction::isPlannedAvailability, PredictionUpdate::of);
if (isNotBefore(5, 12)) {
this.geoLocations = toGeoLocationUpdates(mediaobject.getGeoLocations(), owner);
this.topics = toTopicUpdates(mediaobject.getTopics(), owner);
}
}
protected abstract void fillFrom(M mediaObject, OwnerType ownerType);
/**
* The POMS version this XML applies too. This is optional, though some features will only be supported if you explicitly specify a version which is big enough (To ensure backward compatibility). If you don't specify it, there will be no backwards compatibility.
*
*
* The main point is that the XML may contain elements which' absent means something. E.g. having no {@code no country associated with the object. This was introduced in poms 5.0. If you specify a version before 5.0, all country information will be ignored, and left was it was.
*
*/
@Override
@XmlTransient
public IntegerVersion getVersion() {
return version;
}
@Override
public void setVersion(IntegerVersion version) {
this.version = version;
}
@XmlAttribute(name = "version")
protected String getVersionAttribute() {
if (xmlVersion) {
IntegerVersion version = getVersion();
return version == null ? null : version.toString();
} else {
return null;
}
}
protected void setVersionAttribute(String version) {
setVersion(Version.parseIntegers(version));
}
@JsonIgnore
public boolean isValid() {
return violations().isEmpty();
}
public Set extends ConstraintViolation>> warningViolations() {
return violations(WarningValidatorGroup.class);
}
public Set extends ConstraintViolation>> violations(Class>... groups) {
if (groups.length == 0) {
groups = Validation.DEFAULT_GROUPS;
}
mediaObjectToValidate = null;
Set extends ConstraintViolation>> result = Validation.validate(this, groups);
if (result.isEmpty()) {
mediaObjectToValidate = fetch(OwnerType.BROADCASTER);
try {
result = Validation.validate(this, groups);
if (result.isEmpty()) {
log.debug("validates");
}
return result;
} catch (Throwable t) {
log.error(t.getMessage(), t);
return Collections.emptySet();
}
}
return result;
}
public String violationMessage() {
Set extends ConstraintViolation extends MediaUpdate extends M>>> violations = violations();
return violationMessage(violations);
}
public static String violationMessage(Set extends ConstraintViolation extends MediaUpdate extends N>>> violations) {
if(violations.isEmpty()) {
return null;
}
StringBuilder sb = new StringBuilder("List of constraint violations: [\n");
for(ConstraintViolation> violation : violations) {
sb.append('\t')
.append(violation.toString())
.append('\n');
}
sb.append(']');
return sb.toString();
}
protected abstract M newMedia();
private M fetchOwnerless() {
M media = newMedia();
media.setUrn(urn);
media.setMid(mid);
media.setCreationInstant(null); // not supported by update format. will be set by persistence layer
media.setCrids(crids);
media.setAVType(avType);
media.setEmbeddable(embeddable == null || embeddable);
media.setCountries(countries);
media.setLanguages(languages);
media.setAvAttributes(AVAttributesUpdate.toAvAttributes(avAttributes));
Embargos.copy(this, media);
try {
media.setDuration(duration);
} catch(ModificationException mfe) {
log.error(mfe.getMessage());
}
media.setReleaseYear(releaseYear);
media.setAgeRating(ageRating);
media.setContentRatings(contentRatings);
// images have owner
media.setBroadcasters(toList(broadcasters, Broadcaster::new));
media.setPortals(toList(portals, Portal::new));
media.setTags(toSet(tags, Tag::new));
media.setCredits(toList(credits, CreditsUpdate::toCredits, true));
media.setPortalRestrictions(toList(portalRestrictions, PortalRestrictionUpdate::toPortalRestriction));
media.setGeoRestrictions(toSet(geoRestrictions,
g -> g.getRegion() != Region.UNIVERSE,
GeoRestrictionUpdate::toGeoRestriction
));
// titles have owner,
media.setGenres(toSet(genres, Genre::new));
media.setMemberOf(toSet(memberOf, this::toMemberRef));
// locations are owned
media.setRelations(toSet(relations, RelationUpdate::toRelation));
// scheduleevents are owned
media.setPredictions(toSet(predictions, PredictionUpdate::toPrediction));
if (isDeleted == Boolean.TRUE) {
MediaObjects.markForDeletionIfNeeded(media, "");
} else {
MediaObjects.markForRepublication(media, "");
}
return media;
}
/**
* Convert this MediaUpdate object to a MediaObject
* Clone all the fields of MediaUpdate into a new MediaObject
*/
public M fetch(OwnerType owner) {
M returnObject = fetchOwnerless();
TextualObjects.copy(this, returnObject, owner);
for (Location l : toSet(locations, l -> l.toLocation(owner))) {
returnObject.addLocation(l);
}
Predicate imageFilter = isImported() ? (i) -> i.getImageUri() != null : (i) -> true;
returnObject.setImages(toList(images, i -> i.toImage(owner)).stream()
.filter(imageFilter)
.collect(Collectors.toList()));
returnObject.setWebsites(toList(websites, (w) -> new Website(w, owner)));
returnObject.setTwitterRefs(toList(twitterrefs, (t) -> new TwitterRef(t, owner)));
returnObject.setEmail(toList(email, (e) -> new nl.vpro.domain.media.Email(e, owner)));
if(intentions != null) {
MediaObjectOwnableLists.addOrUpdateOwnableList(returnObject, returnObject.getIntentions(), toIntentions(intentions, owner));
} else {
MediaObjectOwnableLists.remove(returnObject.getIntentions(), owner);
}
if(targetGroups != null) {
MediaObjectOwnableLists.addOrUpdateOwnableList(returnObject, returnObject.getTargetGroups(), toTargetGroups(targetGroups, owner));
} else {
MediaObjectOwnableLists.remove(returnObject.getTargetGroups(), owner);
}
if (isNotBefore(5, 12)) {
if (geoLocations != null) {
MediaObjectOwnableLists.addOrUpdateOwnableList(returnObject, returnObject.getGeoLocations(), toGeoLocations(geoLocations, owner));
} else {
MediaObjectOwnableLists.remove(returnObject.getGeoLocations(), owner);
}
if (topics != null ) {
MediaObjectOwnableLists.addOrUpdateOwnableList(returnObject, returnObject.getTopics(), toTopics(topics, owner));
} else {
MediaObjectOwnableLists.remove(returnObject.getTopics(), owner);
}
}
return returnObject;
}
/**
* From a SortedSet to a List
* Returning only the values for the given owner.
* We decided to return an empty list if owner differ rather than raise an
* exception (this code will usually be executed behind a queue)
*
* Given a null it will return null to keep the distinction between systems
* that are aware of this field (we use empty list to delete)
*/
private List toUpdateIntentions(SortedSet intentions, OwnerType owner){
if (intentions == null) {
return null;
}
return OwnableLists.filterByOwnerOrFirst(intentions, owner)
.map(Intentions::getValues)
.map(l -> l.stream().map(Intention::getValue).collect(Collectors.toList()))
.orElse(new ArrayList<>());
}
private Intentions toIntentions(List intentionValues, OwnerType owner){
if (intentionValues == null) {
return null;
}
return Intentions.builder()
.owner(owner)
.values(intentionValues)
.build();
}
/**
* From a SortedSet to a List
* Returning only the values for the given owner.
* We decided to return an empty list if owner differ rather than raise an
* exception (this code will usually be executed behind a queue)
*
* Given a null it will return null to keep the distinction between systems
* that are aware of this field (we use empty list to delete)
*/
private List toUpdateTargetGroups(SortedSet targetGroups, OwnerType owner){
if (targetGroups == null){
return null;
}
return OwnableLists
.filterByOwnerOrFirst(targetGroups, owner)
.map(TargetGroups::getValues)
.map(l -> l.stream().map(TargetGroup::getValue).collect(Collectors.toList()))
.orElse(new ArrayList<>());
}
private TargetGroups toTargetGroups(@Nullable List targetGroupValues, @NonNull OwnerType owner){
if (targetGroupValues == null){
return null;
}
return TargetGroups.builder()
.owner(owner)
.values(targetGroupValues)
.build();
}
private List toGeoLocationUpdates(SortedSet geoLocationsSet, OwnerType owner) {
if (geoLocationsSet == null) {
return null;
}
return OwnableLists.filterByOwner(geoLocationsSet, owner)
.map(GeoLocations::getValues)
.map(l -> l.stream()
.map(GeoLocationUpdate::new)
.collect(Collectors.toList()))
.orElse(new ArrayList<>());
}
private GeoLocations toGeoLocations(List geoLocationUpdates, OwnerType owner) {
if (geoLocationUpdates == null){
return null;
}
List geoLocations = geoLocationUpdates
.stream()
.map(GeoLocationUpdate::toGeoLocation)
.collect(Collectors.toList());
return GeoLocations.builder()
.owner(owner)
.values(geoLocations)
.build();
}
private List toTopicUpdates(SortedSet topicsSet, OwnerType owner) {
if (topicsSet == null) {
return null;
}
return OwnableLists.filterByOwner(topicsSet, owner)
.map(Topics::getValues)
.map(l -> l.stream()
.map(TopicUpdate::new)
.collect(Collectors.toList()))
.orElse(new ArrayList<>());
}
private Topics toTopics(List topicUpdates, OwnerType owner) {
if (topicUpdates == null) {
return null;
}
List topics = topicUpdates
.stream()
.map(TopicUpdate::toTopic)
.collect(Collectors.toList());
return Topics.builder()
.owner(owner)
.values(topics)
.build();
}
private List toCreditsUpdate(List credits) {
if (credits == null) {
return null;
}
List creditsUpdates = new ArrayList<>();
for (Credits credit: credits) {
if (credit instanceof Person p) {
if (p.getGtaaUri() != null && isBefore(5, 12)) {
continue;
}
creditsUpdates.add(new PersonUpdate(p));
} else {
if (isNotBefore(5, 12)) {
creditsUpdates.add(new NameUpdate((Name) credit));
}
}
}
return creditsUpdates;
}
public M fetch() {
return fetch(OwnerType.BROADCASTER);
}
protected List toList(List list, Predicate filter, Function mapper, boolean nullToNull) {
if (list == null) {
if (nullToNull) {
return null;
}
list = new ArrayList<>();
}
return list
.stream()
.filter(filter)
.map(mapper)
.collect(Collectors.toList());
}
protected List toList(List list, Function mapper, boolean nullToNull) {
return toList(list, (u) -> true, mapper, nullToNull);
}
protected List toList(List list, Function mapper) {
return toList(list, (u) -> true, mapper, false);
}
protected > TreeSet toSet(Set list, Predicate filter, Function mapper) {
if (list == null) {
list = new TreeSet<>();
}
return list
.stream()
.filter(filter)
.map(mapper)
.collect(Collectors.toCollection(TreeSet::new));
}
protected > TreeSet toSet(Set list, Function mapper) {
return toSet(list, (u) -> true, mapper);
}
/**
*
* @since 1.5
*/
@XmlAttribute
@Size.List({@Size(max = 255), @Size(min = 4)})
@Pattern(regexp = "^[ .a-zA-Z0-9_-]+$", flags = {Pattern.Flag.CASE_INSENSITIVE}, message = "{nl.vpro.constraints.mid}")
@Override
public final String getMid() {
return mid;
}
/**
* @since 1.8
*/
public void setMid(String mid) {
this.mid = mid;
}
public abstract SubMediaType getType();
/**
* @since 5.6
*/
@Override
@JsonIgnore
public final MediaType getMediaType() {
SubMediaType subMediaType = getType();
return subMediaType == null ? null : subMediaType.getMediaType();
}
@XmlAttribute(name = "deleted")
protected Boolean getDeletedAttribute() {
return isDeleted() ? true : null;
}
protected void setDeletedAttribute(Boolean deleted) {
isDeleted = deleted != null ? (deleted ? true : null) : null;
}
@XmlTransient
public boolean isDeleted() {
return isDeleted != null && isDeleted;
}
public void setDeleted(boolean isDeleted) {
this.isDeleted = !isDeleted ? null : true;
}
@XmlAttribute
@Deprecated
public String getUrn() {
return urn;
}
/**
* @deprecated Refer to existing media by {@link #setMid(String) mid} instead.
*/
@Deprecated(since = "7.7")
public void setUrn(String s) {
this.urn = s;
}
@XmlTransient
@Override
public Long getId() {
String urn = getUrn();
if (urn == null) {
return null;
}
return Long.valueOf(urn.substring(getUrnPrefix().length()));
}
void setId(Long id) {
setUrn(getUrnPrefix() + id);
}
protected abstract String getUrnPrefix();
@XmlAttribute(name = "avType")
@NotNull
public AVType getAVType() {
return avType;
}
public void setAVType(AVType avType) {
this.avType = avType;
}
@XmlAttribute
public Boolean getEmbeddable() {
return embeddable;
}
public void setEmbeddable(Boolean isEmbeddable) {
this.embeddable = isEmbeddable;
}
@Override
@XmlAttribute(name = "publishStart")
@XmlJavaTypeAdapter(InstantXmlAdapter.class)
@XmlSchemaType(name = "dateTime")
@JsonDeserialize(using = StringInstantToJsonTimestamp.Deserializer.class)
@JsonSerialize(using = StringInstantToJsonTimestamp.Serializer.class)
public Instant getPublishStartInstant() {
return publishStart;
}
@NonNull
@Override
public MediaUpdate setPublishStartInstant(Instant publishStart) {
this.publishStart = publishStart;
return this;
}
@Override
@XmlAttribute(name = "publishStop")
@XmlJavaTypeAdapter(InstantXmlAdapter.class)
@XmlSchemaType(name = "dateTime")
@JsonDeserialize(using = StringInstantToJsonTimestamp.Deserializer.class)
@JsonSerialize(using = StringInstantToJsonTimestamp.Serializer.class)
public Instant getPublishStopInstant() {
return publishStop;
}
@NonNull
@Override
public MediaUpdate setPublishStopInstant(Instant publishStop) {
this.publishStop = publishStop;
return this;
}
@XmlElement(name = "crid")
@StringList(pattern = "(?i)crid://.*/.*", maxLength = 255)
@Override
@NonNull
public List getCrids() {
if (crids == null) {
crids = new ArrayList<>();
}
return crids;
}
public void setCrids(List crids) {
this.crids = crids;
}
@XmlElement(name = "broadcaster", required = true)
@Size(min = 1, groups = WarningValidatorGroup.class)
@NonNull
public List getBroadcasters() {
if (broadcasters == null) {
broadcasters = new ArrayList<>();
}
return broadcasters;
}
public void setBroadcasters(List broadcasters) {
this.broadcasters = broadcasters;
}
@XmlTransient
public void setBroadcasters(String... broadcasters) {
this.broadcasters = Arrays.asList(broadcasters);
}
@XmlElement(name = "portal", required = false)
@NonNull
public List getPortals() {
if (portals == null) {
portals = new ArrayList<>();
}
return portals;
}
public void setPortals(List portals) {
this.portals = portals;
}
@XmlTransient
public void setPortals(String... portals) {
this.portals = Arrays.asList(portals);
}
@XmlElement(name = "exclusive")
@Valid
public List getPortalRestrictions() {
if (portalRestrictions == null) {
portalRestrictions = new ArrayList<>();
}
return portalRestrictions;
}
public void setPortalRestrictions(List restrictions) {
this.portalRestrictions = restrictions;
}
@XmlTransient
public void setPortalRestrictions(String... restrictions) {
List updates = getPortalRestrictions();
Stream.of(restrictions).forEach(r -> updates.add(PortalRestrictionUpdate.of(r)));
}
@XmlElement(name = "region")
@Valid
@NonNull
public SortedSet getGeoRestrictions() {
if (geoRestrictions == null) {
geoRestrictions = new TreeSet<>();
}
return geoRestrictions;
}
public void setGeoRestrictions(SortedSet restrictions) {
this.geoRestrictions = restrictions;
}
@Override
@XmlElement(name = "title", required = true)
@Valid
@NotNull
@NonNull
@Size(min = 1, groups = RedundantValidatorGroup.class)
public SortedSet getTitles() {
if (titles == null) {
titles = new TreeSet<>();
}
return titles;
}
@Override
public void setTitles(SortedSet titles) {
this.titles = titles;
}
@XmlTransient
public void setTitles(TitleUpdate... titles) {
setTitles(new TreeSet<>(Arrays.asList(titles)));
}
@Override
@XmlElement(name = "description")
@Valid
@NonNull
public SortedSet getDescriptions() {
if (descriptions == null) {
descriptions = new TreeSet<>();
}
return descriptions;
}
@Override
public void setDescriptions(SortedSet descriptions) {
this.descriptions = descriptions;
}
@XmlTransient
public void setDescriptions(DescriptionUpdate... descriptions) {
this.descriptions = new TreeSet<>(Arrays.asList(descriptions));
}
@Override
@JsonIgnore
public BiFunction getTitleCreator() {
return TitleUpdate::new;
}
@Override
@JsonIgnore
public BiFunction getDescriptionCreator() {
return DescriptionUpdate::new;
}
@XmlElement(name = "tag")
public SortedSet getTags() {
if (tags == null) {
tags = new TreeSet<>();
}
return tags;
}
public void setTags(SortedSet tags) {
this.tags = tags;
}
@XmlTransient
public void setTags(String... tags) {
setTags(new TreeSet<>(Arrays.asList(tags)));
}
@XmlElement(name = "country")
@XmlJavaTypeAdapter(Code.class)
public List getCountries() {
if (countries == null) {
countries = new ArrayList<>();
}
return countries;
}
public void setCountries(List countries) {
this.countries = countries;
}
@XmlElement(name = "language")
@XmlJavaTypeAdapter(value = UsedLanguageUpdateAdapter.class)
@JsonSerialize(using = LanguageList.Serializer.class)
@JsonDeserialize(using = LanguageList.Deserializer.class)
public List getLanguages() {
if (languages == null) {
languages = new ArrayList<>();
}
return languages;
}
public void setLanguages(List languages) {
this.languages = languages;
}
@XmlElement(name = "genre")
@StringList(pattern = "3\\.([0-9]+\\.)*[0-9]+", maxLength = 255)
@NonNull
public SortedSet getGenres() {
if (genres == null) {
genres = new TreeSet<>();
}
return genres;
}
public void setGenres(SortedSet genres) {
this.genres = genres;
}
@XmlTransient
public void setGenres(String... genres) {
setGenres(new TreeSet<>(Arrays.asList(genres)));
}
@XmlElementWrapper(name = "intentions")
@XmlElement(name = "intention")
@NonNull
public List getIntentions() {
return intentions;
}
public void setIntentions(List intentions) {
this.intentions = intentions;
}
@XmlElementWrapper(name = "targetGroups")
@XmlElement(name = "targetGroup")
@NonNull
public List getTargetGroups() {
return targetGroups;
}
public void setTargetGroups(List targetGroups) {
this.targetGroups = targetGroups;
}
@XmlElement(name = "avAttributes")
public AVAttributesUpdate getAvAttributes() {
return avAttributes;
}
public void setAvAttributes(AVAttributesUpdate avAttributes) {
this.avAttributes = avAttributes;
}
@XmlElement
@XmlJavaTypeAdapter(DurationXmlAdapter.class)
public java.time.Duration getDuration() {
return duration;
}
public void setDuration(java.time.Duration duration) {
this.duration = duration;
}
@XmlElement
public Short getReleaseYear() {
return releaseYear;
}
public void setReleaseYear(Short releaseYear) {
this.releaseYear = releaseYear;
}
@XmlElementWrapper(name = "credits")
@XmlElements({
@XmlElement(name = "person", type = PersonUpdate.class),
@XmlElement(name = "name", type = NameUpdate.class)
})
@Valid
@NonNull
public List getCredits() {
if (credits == null) {
credits = new ArrayList<>();
}
return credits;
}
public void setCredits(List credits) {
this.credits = credits;
}
@XmlTransient
public void setCredits(CreditsUpdate... credits){
setCredits(new ArrayList<>(Arrays.asList(credits)));
}
@XmlElement
@NonNull
public SortedSet getMemberOf() {
if (memberOf == null) {
memberOf = new TreeSet<>();
}
return memberOf;
}
protected MemberRef toMemberRef(MemberRefUpdate m) {
MemberRef ref = new MemberRef();
//ref.setMember(fetch(OwnerType.BROADCASTER));
ref.setMediaRef(m.getMediaRef());
ref.setNumber(m.getPosition());
ref.setHighlighted(m.isHighlighted());
ref.setAdded(null);
return ref;
}
public void setMemberOf(SortedSet memberOf) throws CircularReferenceException {
this.memberOf = memberOf;
}
@XmlElement
public AgeRating getAgeRating() {
return ageRating;
}
public void setAgeRating(AgeRating ageRating) {
this.ageRating = ageRating;
}
@XmlElement(name = "contentRating")
@NonNull
public List getContentRatings() {
if (contentRatings == null) {
contentRatings = new ArrayList<>();
}
return contentRatings;
}
public void setContentRatings(List contentRatings) {
this.contentRatings = contentRatings;
}
@XmlElement
@NonNull
public List getEmail() {
if (email == null) {
email = new ArrayList<>();
}
return email;
}
public void setEmail(List emails) {
this.email = emails;
}
@XmlTransient
public void setEmail(String... emails) {
setEmail(new ArrayList<>(Arrays.asList(emails)));
}
@XmlElement(name = "website")
@NonNull
public List getWebsites() {
if (websites == null) {
websites = new ArrayList<>();
}
return websites;
}
public void setWebsites(List websites) {
this.websites = websites;
}
@XmlTransient
public void setWebsites(String... websites) {
setWebsites(new ArrayList<>(Arrays.asList(websites)));
}
public void setWebsiteObjects(List websites) {
this.websites = websites.stream()
.map(Website::getUrl)
.collect(Collectors.toList());
}
@XmlElement(name = "twitterref")
@NonNull
public List getTwitterrefs() {
if (twitterrefs == null) {
twitterrefs = new ArrayList<>();
}
return twitterrefs;
}
@XmlTransient
public void setTwitterRefs(List twitterRefs) {
this.twitterrefs = twitterRefs;
}
/**
* @since 5.6
*/
@XmlElement(name = "prediction")
@Valid
@NonNull
public SortedSet getPredictions() {
if (predictions == null) {
predictions = new TreeSet<>();
}
return predictions;
}
/**
* @since 5.6
*/
public void setPredictions(SortedSet predictions) {
this.predictions = predictions;
}
@XmlElementWrapper(name = "locations")
@XmlElement(name = "location")
@Valid
@NonNull
public SortedSet getLocations() {
if (locations == null) {
locations = new TreeSet<>();
}
return locations;
}
public void setLocations(SortedSet locations) {
this.locations = locations;
}
@XmlTransient
public void setLocations(LocationUpdate... locations) {
setLocations(new TreeSet<>(Arrays.asList(locations)));
}
@XmlElement(name = "relation")
@NonNull
public SortedSet getRelations() {
if (relations == null) {
relations = new TreeSet<>();
}
return relations;
}
public void setRelations(SortedSet relations) {
this.relations = relations;
}
@XmlElementWrapper(name = "images")
@XmlElement(name = "image")
@Valid
@NonNull
public List getImages() {
if (images == null) {
images = new ArrayList<>();
}
return images;
}
public void setImages(List images) {
// Leave builder.images to the fetch(ImageImporter) method
this.images = images;
}
@XmlTransient
public void setImages(ImageUpdate... images) {
setImages(new ArrayList<>());
this.images.addAll(Arrays.asList(images));
}
/**
* Get asset containing the location source to be encoded.
*
* @return asset or null when unavailable
* @since 2.1
*/
@XmlElement(name = "asset")
@Nullable
public Asset getAsset() {
return asset;
}
public void setAsset(Asset asset) {
this.asset = asset;
}
@XmlElementWrapper(name = "geoLocations")
@XmlElement(name = "geoLocation")
@Valid
@NonNull
public List getGeoLocations() {
return geoLocations;
}
public void setGeoLocations(List geoLocationUpdates) {
this.geoLocations = geoLocationUpdates;
}
@XmlTransient
public void setGeoLocations(GeoLocationUpdate... geoLocationUpdates) {
setGeoLocations(Arrays.asList(geoLocationUpdates));
}
@XmlElementWrapper(name = "topics")
@XmlElement(name = "topic")
@Valid
@NonNull
public List getTopics() {
return topics;
}
public void setTopics(List topicUpdates) {
this.topics = topicUpdates;
}
@XmlTransient
public void setTopics(TopicUpdate... topicUpdates) {
setTopics(Arrays.asList(topicUpdates));
}
@Override
public String toString() {
return (isDeleted == Boolean.TRUE ? "DELETED:" : "") + getClass().getSimpleName() + "[" + getType() + ":" + (mid == null ? "" : mid) + ":" + Optional.ofNullable(getMainTitle()).orElse("") + "]";
}
void afterUnmarshal(Unmarshaller u, Object parent) {
this.fromXml = true;
if (parent != null) {
if (parent instanceof IntegerVersionSpecific) {
version = ((IntegerVersionSpecific) parent).getVersion();
xmlVersion = false;
}
}
}
void beforeMarshal(Marshaller marshaller) {
log.trace("Before");
}
protected boolean isNotBefore(Integer... intVersion) {
return version == null || version.isNotBefore(intVersion);
}
protected boolean isBefore(Integer... intVersion) {
return version != null && version.isBefore(intVersion);
}
}