
org.bedework.calfacade.SchedulingInfo Maven / Gradle / Ivy
Show all versions of bw-calendar-facade Show documentation
/* ********************************************************************
Appropriate copyright notice
*/
package org.bedework.calfacade;
import org.bedework.base.exc.BedeworkException;
import org.bedework.calfacade.exc.CalFacadeErrorCode;
import org.bedework.calfacade.util.ChangeTableEntry;
import org.bedework.util.calendar.PropertyIndex;
import org.bedework.util.misc.Util;
import org.bedework.base.response.GetEntityResponse;
import org.bedework.base.response.Response;
import net.fortuna.ical4j.model.Calendar;
import net.fortuna.ical4j.model.Component;
import net.fortuna.ical4j.model.component.VEvent;
import net.fortuna.ical4j.model.property.ParticipantType;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.bedework.util.calendar.IcalDefs.entityTypeVpoll;
import static org.bedework.util.calendar.IcalendarUtil.fromBuilder;
/** Handle component participants which represent attendees or
* organizer/owner.
*
* Currently, we have the complication of having attendee only,
* participant only, or both. Also, participants are stored as
* x-properties mixed in with others.
*
* The approach taken is to load all the participants from
* the x-properties into the bwparticipants set on creation of
* this class. That set is updated as the result of various
* operations and the onSave() method updates th ex-properties to
* reflect the changes.
*
*
* User: mike Date: 9/10/24 Time: 13:40
*/
public class SchedulingInfo {
private final BwEvent parent;
/* Manages the owner (scheduling) for the component */
private SchedulingOwner schedulingOwner;
private Set bwParticipants;
private Map bwParticipantMap;
private Set participants;
private Map participantMap;
private boolean changed;
public SchedulingInfo(final BwEvent parent) {
this.parent = parent;
}
public BwEvent getParent() {
return parent;
}
/**
*
* @return the SchedulingOwner - organizer in 5545 terms.
*/
public SchedulingOwner getSchedulingOwner() {
if (schedulingOwner == null) {
final var owners = getParticipantsWithRoles(
ParticipantType.VALUE_OWNER);
final BwParticipant powner;
if (owners.size() == 1) {
powner = owners.values().iterator().next().getBwParticipant();
} else {
powner = null;
}
schedulingOwner = new SchedulingOwner(this,
parent.getOrganizer(),
powner);
}
return schedulingOwner;
}
/**
*
* @param calendarAddress of owner
* @return new owner with calendar address and role set.
*/
public SchedulingOwner newSchedulingOwner(
final String calendarAddress) {
final BwParticipant powner;
if (parent.getEntityType() == entityTypeVpoll) {
final var part = findParticipant(calendarAddress);
if (part == null) {
powner = new BwParticipant(this);
getBwParticipantsSet().add(powner);
} else {
powner = part.getBwParticipant();
}
} else {
powner = null;
}
final BwOrganizer organizer;
if (powner == null) {
organizer = new BwOrganizer();
} else {
organizer = null;
}
schedulingOwner = new SchedulingOwner(this,
organizer,
powner);
schedulingOwner.setCalendarAddress(calendarAddress);
markChanged();
return schedulingOwner;
}
public SchedulingOwner newSchedulingOwner(final BwOrganizer organizer,
final BwParticipant powner) {
schedulingOwner = new SchedulingOwner(this,
organizer,
powner);
return schedulingOwner;
}
public SchedulingOwner copySchedulingOwner(final SchedulingOwner from) {
final BwOrganizer org;
final BwParticipant powner;
if (from.getOrganizer() != null) {
// Assume no participants.
org = (BwOrganizer)from.getOrganizer().clone();
parent.setOrganizer(org);
schedulingOwner = newSchedulingOwner(org, null);
markChanged();
return schedulingOwner;
}
if (from.getParticipant() == null) {
// Should not happen
return null;
}
// Look for an existing participant
final Participant p = findParticipant(from.getCalendarAddress());
if (p == null) {
// Just add a new copy with role set.
powner = (BwParticipant)from.getParticipant().clone();
powner.addParticipantType(ParticipantType.VALUE_OWNER);
getBwParticipantsSet().add(powner);
schedulingOwner = newSchedulingOwner(null, powner);
return schedulingOwner;
}
// Update
powner = p.getBwParticipant();
from.getParticipant().copyTo(powner);
powner.addParticipantType(ParticipantType.VALUE_OWNER);
schedulingOwner = newSchedulingOwner(null, powner);
return schedulingOwner;
}
/**
*
* @return unmodifiable set of all Participant objects
*/
public Set getParticipants() {
return Collections.unmodifiableSet(getParticipantsSet());
}
/**
*
* @return number of participant objects
*/
public int getNumParticipants() {
return getParticipantsSet().size();
}
public Set getParticipantAddrs() {
getParticipantsSet(); // Ensure populated
return participantMap.keySet();
}
public void clearParticipants() {
if (parent.getAttendees() != null) {
parent.getAttendees().clear();
}
// Remove all but owner participant if it exists.
final var owner = getSchedulingOwner();
final var ownerParticipant = owner.getParticipant();
bwParticipants.clear();
if (ownerParticipant != null) {
ownerParticipant.setParticipantType(
ParticipantType.VALUE_OWNER);
bwParticipants.add(ownerParticipant);
} // Else it's an ORGANIZER
markChanged();
}
/**
*
* @param participant we want left
*/
public void setOnlyParticipant(final Participant participant) {
clearParticipants(); // Leaves owner
makeParticipant(participant.getAttendee(), participant.getBwParticipant());
}
public GetEntityResponse getOnlyParticipant() {
final var resp = new GetEntityResponse();
final var recipients = getRecipientParticipants();
if (recipients.size() != 1) {
return Response.error(resp, new BedeworkException(
CalFacadeErrorCode.schedulingExpectOneAttendee));
}
resp.setEntity(recipients.values().iterator().next());
return resp;
}
public Participant findParticipant(final String calAddr) {
getParticipantsSet(); // Ensure populated
return participantMap.get(calAddr);
}
public Participant makeParticipant() {
final var p = newBwParticipant();
final var att = new BwAttendee();
parent.addAttendee(att);
final var a = new Participant(this, att, p);
getParticipantsSet().add(a);
markChanged();
return a;
}
public void removeRecipientParticipant(final Participant val) {
final var ctab = parent.getChangeset();
final var att = val.getAttendee();
if (att != null) {
if (ctab == null) {
parent.removeAttendee(att);
} else {
ctab.changed(PropertyIndex.PropertyInfoIndex.ATTENDEE,
att, null);
}
}
final var part = val.getBwParticipant();
if (part != null) {
part.removeParticipantType(ParticipantType.VALUE_ATTENDEE);
part.removeParticipantType(ParticipantType.VALUE_CHAIR);
part.removeParticipantType(ParticipantType.VALUE_VOTER);
if (part.getParticipantType() == null) {
// No remaining roles - remove it.
removeBwParticipant(part);
}
}
markChanged();
}
public Participant addParticipant(final Participant val) {
final var evAtt = parent.findAttendee(val.getCalendarAddress());
final var att = val.getAttendee();
final var ctab = parent.getChangeset();
if (att != null) {
if (ctab == null) {
parent.addAttendee(val.getAttendee());
} else {
final ChangeTableEntry cte = ctab.getEntry(
PropertyIndex.PropertyInfoIndex.ATTENDEE);
if (evAtt != null) {
cte.addChangedValue(att);
} else {
cte.addAddedValue(att);
}
}
}
var evPart = getBwParticipantMap()
.get(val.getCalendarAddress());
final var part = val.getBwParticipant();
if (part != null) {
if (evPart != null) {
// Merge the two.
part.copyToMerge(evPart);
} else {
evPart = new BwParticipant(this);
part.copyTo(evPart);
getBwParticipantsSet().add(evPart);
}
}
markChanged();
return val;
}
/** if there is no participant with the same uri then a copy will be
* added and returned.
*
* Otherwise the values in the parameter will be copied to the
* existing participant.
*
* @param val participant to copy
* @return copied attende
*/
public Participant copyParticipant(final Participant val) {
final var ourParticipant = findParticipant(val.getCalendarAddress());
if (ourParticipant != null) {
val.copyTo(ourParticipant);
final var ctab = parent.getChangeset();
if (ctab != null) {
final ChangeTableEntry cte = ctab.getEntry(
PropertyIndex.PropertyInfoIndex.ATTENDEE);
cte.addChangedValue(ourParticipant.getAttendee());
}
return ourParticipant;
}
final BwAttendee att;
final BwParticipant part;
if (val.getAttendee() != null) {
att = (BwAttendee)val.getAttendee().clone();
} else {
att = null;
}
if (val.getBwParticipant() != null) {
part = (BwParticipant)val.getBwParticipant().clone();
getBwParticipantsSet().add(part);
} else {
part = null;
}
return addParticipant(new Participant(this, att, part));
}
public Participant makeParticipant(final BwAttendee att,
final BwParticipant part) {
final var a = new Participant(this, att, part);
return addParticipant(a);
}
/** Create a Participant 'like' the parameter in that, if the
* param has a BwAttendee then so does the result. Ditto for
* participant. No values are copied.
*
* @param val template Participant
* @return new Participant
*/
public Participant makeParticipantLike(final Participant val) {
final BwAttendee mAtt;
final BwParticipant mPart;
if (val.getAttendee() != null) {
mAtt = new BwAttendee();
mPart = null;
} else {
mPart = newBwParticipant();
mAtt = null;
}
return makeParticipant(mAtt, mPart);
}
/** The new object is added to the set.
* getParticipants must be called to get new updated set.
*
* @return a new Participant enclosing the ical object
*/
public Participant newParticipant(final net.fortuna.ical4j.model.component.Participant part) {
final var bwpart = new BwParticipant(this, part);
final var calAddr = bwpart.getCalendarAddress();
if (calAddr != null) {
if (getBwParticipantMap().get(calAddr) != null) {
throw new BedeworkException("Duplicate participant " + calAddr);
}
}
getBwParticipantsSet().add(bwpart);
final var participant = new Participant(this, null, bwpart);
markChanged();
return participant;
}
/** if the participant is not in the set then a new object is
* added to the set. Otherwise, the exisiting object is updated
* from the parameter.
* getParticipants must be called to get new updated set.
*
* @return a new Participant enclosing the ical object
*/
public Participant addUpdateParticipant(final net.fortuna.ical4j.model.component.Participant part) {
final var bwpart = new BwParticipant(this, part);
final var calAddr = bwpart.getCalendarAddress();
boolean addIt = true;
if (calAddr != null) {
final var setBwpart = getBwParticipantMap().get(calAddr);
if (setBwpart != null) {
bwpart.copyToMerge(setBwpart);
addIt = false;
}
}
if (addIt) {
getBwParticipantsSet().add(bwpart);
}
final var participant = new Participant(this, null, bwpart);
markChanged();
return participant;
}
public Map getParticipantsWithRoles(
final String... roles) {
final Map vals = new HashMap<>();
for (final var p: getParticipants()) {
for (final var r: roles) {
if (p.includesParticipantType(r)) {
vals.put(p.getCalendarAddress(), p);
break;
}
}
}
return Collections.unmodifiableMap(vals);
}
/**
*
* @return unmodifiable set of Participant objects that should
* receive scheduling messages
*/
public Map getRecipientParticipants() {
return getParticipantsWithRoles(ParticipantType.VALUE_ATTENDEE,
ParticipantType.VALUE_CHAIR,
ParticipantType.VALUE_VOTER);
}
public void markChanged() {
changed = true;
participants = null;
participantMap = null;
bwParticipantMap = null;
schedulingOwner = null;
}
public void onSave() {
if (!changed) {
return;
}
final List props =
parent.getXproperties(BwXproperty.bedeworkParticipant);
if (!Util.isEmpty(props)) {
for (final BwXproperty p: props) {
parent.removeXproperty(p);
}
}
for (final var p: getBwParticipantsSet()) {
final BwXproperty xp =
new BwXproperty(BwXproperty.bedeworkParticipant,
null, p.asString());
parent.addXproperty(xp);
}
changed = false;
}
public SchedulingInfo copyFor(final BwEvent ev) {
final var res = new SchedulingInfo(ev);
BwOrganizer org = parent.getOrganizer();
if (org != null) {
org = (BwOrganizer)org.clone();
}
ev.setOrganizer(org);
ev.setAttendees(parent.cloneAttendees());
for (final BwParticipant p: getBwParticipantsSet()) {
res.getBwParticipantsSet().add((BwParticipant)p.clone());
}
res.markChanged();
return res;
}
private Set getParticipantsSet() {
if (participants != null) {
return participants;
}
participants = new HashSet<>();
participantMap = new HashMap<>();
final var evatts = parent.getAttendees();
final Map attMap = new HashMap<>();
if (evatts != null) {
for (final BwAttendee att: evatts) {
final var addr = att.getAttendeeUri();
attMap.put(addr, att);
final var part = findBwParticipant(addr);
final var participant = new Participant(this, att, part);
participants.add(participant);
participantMap.put(addr, participant);
}
}
for (final var part: getBwParticipantsSet()) {
final var addr = part.getCalendarAddress();
if (addr == null) {
continue;
}
final var att = attMap.get(addr);
if (att == null) {
// No associated participant
final var participant = new Participant(this, null, part);
participants.add(participant);
participantMap.put(addr, participant);
} else {
attMap.remove(addr);
}
}
for (final BwAttendee att: attMap.values()) {
// Attendee with no bwparticipant
final var participant = new Participant(this, att, null);
participants.add(participant);
participantMap.put(att.getAttendeeUri(), participant);
}
return participants;
}
private Map getBwParticipantMap() {
if (bwParticipantMap == null) {
bwParticipantMap = new HashMap<>();
for (final var part: getBwParticipantsSet()) {
bwParticipantMap.put(part.getCalendarAddress(), part);
}
}
return bwParticipantMap;
}
private Set getBwParticipantsSet() {
if (bwParticipants == null) {
bwParticipants = new HashSet<>();
final var xprops =
parent.getXproperties(BwXproperty.bedeworkParticipant);
if (Util.isEmpty(xprops)) {
return bwParticipants;
}
// Better if ical4j supported sub-component parsing
final StringBuilder sb = new StringBuilder(
"""
BEGIN:VCALENDAR
PRODID://Bedework.org//BedeWork V3.9//EN
VERSION:2.0
BEGIN:VEVENT
UID:0123
""");
boolean found = false;
for (final var xp: xprops) {
found = true;
sb.append(xp.getValue());
}
if (!found) {
return bwParticipants;
}
sb.append(
"""
END:VEVENT
END:VCALENDAR
""");
final Calendar ical = fromBuilder(sb.toString());
/* Should be one event object */
final VEvent ev = ical.getComponent(Component.VEVENT);
for (final Component comp:
ev.getComponents().getComponents("PARTICIPANT")) {
bwParticipants.add(new BwParticipant(this, (net.fortuna.ical4j.model.component.Participant)comp));
}
}
return bwParticipants;
}
private BwParticipant findBwParticipant(final String calAddr) {
for (final BwParticipant p: getBwParticipantsSet()) {
if (calAddr.equals(p.getCalendarAddress())) {
return p;
}
}
return null;
}
private void removeBwParticipant(final BwParticipant part) {
getBwParticipantsSet().remove(part);
markChanged();
}
private BwParticipant newBwParticipant() {
final var p = new BwParticipant(this);
getBwParticipantsSet().add(p);
markChanged();
return p;
}
}