Java tutorial
/* * Version: 1.0 * * The contents of this file are subject to the OpenVPMS License Version * 1.0 (the 'License'); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.openvpms.org/license/ * * Software distributed under the License is distributed on an 'AS IS' basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * Copyright 2019 (C) OpenVPMS Ltd. All Rights Reserved. */ package org.openvpms.web.workspace.workflow.appointment.repeat; import net.sf.jasperreports.engine.util.ObjectUtils; import org.apache.commons.collections4.Predicate; import org.apache.commons.collections4.PredicateUtils; import org.apache.commons.lang.builder.EqualsBuilder; import org.joda.time.DateTime; import org.joda.time.Duration; import org.openvpms.archetype.rules.util.DateRules; import org.openvpms.archetype.rules.workflow.ScheduleArchetypes; import org.openvpms.archetype.rules.workflow.Times; import org.openvpms.component.business.domain.im.act.Act; import org.openvpms.component.business.domain.im.act.ActRelationship; import org.openvpms.component.business.domain.im.common.IMObjectReference; import org.openvpms.component.business.service.archetype.IArchetypeService; import org.openvpms.component.business.service.archetype.helper.ActBean; import org.openvpms.component.model.bean.IMObjectBean; import org.openvpms.component.model.object.Reference; import org.openvpms.web.component.im.act.ActHelper; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.ListIterator; /** * Schedule event series. * * @author Tim Anderson */ public class ScheduleEventSeries { /** * Used to indicate overlapping events. */ public static class Overlap { private final Times event1; private final Times event2; public Overlap(Times event1, Times event2) { this.event1 = event1; this.event2 = event2; } public Times getEvent1() { return event1; } public Times getEvent2() { return event2; } } /** * The default maximum number of events. */ public static final int DEFAULT_MAX_EVENTS = 365; /** * The event. */ private final Act event; /** * The archetype service. */ private final IArchetypeService service; /** * The maximum no. of events that can be created. */ private final int maxEvents; /** * The series or {@code null}, if the event isn't associated with a series. */ private Act series; /** * The acts in the series. */ private List<Act> acts; /** * The prior state. */ private State previous; /** * The current state. */ private State current; /** * If {@code true} only update the times of existing events in the series. */ private boolean updateTimesOnly; /** * Constructs an {@link ScheduleEventSeries}. * * @param event the event * @param service the archetype service */ public ScheduleEventSeries(Act event, IArchetypeService service) { this(event, service, DEFAULT_MAX_EVENTS); } /** * Constructs an {@link ScheduleEventSeries}. * * @param event the event. An appointment or calendar block * @param service the archetype service * @param maxEvents the maximum no. of events in a series */ public ScheduleEventSeries(Act event, IArchetypeService service, int maxEvents) { this.event = event; this.service = service; this.maxEvents = maxEvents; IMObjectBean bean = service.getBean(event); series = bean.getSource("repeat", Act.class); if (series != null) { previous = createState(bean); ActBean seriesBean = new ActBean(series, service); acts = getEvents(event, seriesBean); previous.setExpression(RepeatHelper.getExpression(seriesBean)); int index = acts.indexOf(event); previous.setCondition(RepeatHelper.getCondition(seriesBean, index)); current = copy(previous); } else { current = createState(bean); acts = new ArrayList<>(); } } /** * Returns the event. * * @return the event */ public Act getEvent() { return event; } /** * Invoked to notify this of any event changes. */ public void refresh() { current.update(new ActBean(event, service)); } /** * Returns the repeat expression for this series. * * @return the repeat expression, or {@code null} if none has been configured */ public RepeatExpression getExpression() { return current.getExpression(); } /** * Sets the repeat expression. * * @param expression the repeat expression. May be [@code null} */ public void setExpression(RepeatExpression expression) { current.setExpression(expression); } /** * Returns the repeat-until condition for this series. * * @return the condition, or {@code null} if none has been configured */ public RepeatCondition getCondition() { return current.getCondition(); } /** * Sets the repeat condition. * * @param condition the condition. May be {@code null} */ public void setCondition(RepeatCondition condition) { current.setCondition(condition); } /** * Returns the first overlapping events. * * @return the first overlapping events, or {@code null} if none overlap */ public Overlap getFirstOverlap() { return calculateSeries(new ArrayList<>()); } /** * Determines if only the times of existing events should be updated. * <p> * This should be set {@code true} when moving a series. * * @param updateTimesOnly if {@code true}, only update the times, otherwise update all event fields */ public void setUpdateTimesOnly(boolean updateTimesOnly) { this.updateTimesOnly = updateTimesOnly; } /** * Determines if the expression or condition has been modified. * * @return {@code true} if the expression or condition has been modified */ public boolean isModified() { refresh(); return !ObjectUtils.equals(previous, current); } /** * Saves the series. */ public void save() { refresh(); if (isModified()) { if (previous != null && !current.repeats()) { deleteSeries(); } else if (previous != null) { if (!previous.repeats() && current.repeats()) { createEvents(); } else { updateSeries(); } } else if (current.repeats()) { createEvents(); } previous = copy(current); } } /** * Returns the series act. * * @return the series act, or {@code null} if the event isn't associated with a series */ public Act getSeries() { return series; } /** * Returns the events that make up the series. * * @return the events */ public List<Act> getEvents() { if (series != null) { ActBean bean = new ActBean(series, service); return ActHelper.sort(bean.getNodeActs("items")); } return Collections.emptyList(); } /** * Calculates the series. * * @return the series, or {@code null} if the events overlap */ public List<Times> getEventTimes() { ArrayList<Times> result = new ArrayList<>(); result.add(Times.create(event)); Overlap overlap = calculateSeries(result); return overlap == null ? result : null; } /** * Returns the time that the series starts. * * @return the time */ public Date getStartTime() { return current.getStartTime(); } /** * Returns the maximum number of events in the series. * * @return the maximum number of events */ public int getMaxEvents() { return maxEvents; } /** * Copies state. * * @param state the state to copy * @return a copy of {@code state} */ protected State copy(State state) { return new State(state); } /** * Creates state from an act. * * @param bean the act bean * @return a new state */ protected State createState(IMObjectBean bean) { return new State(bean); } /** * Creates a new event linked to the series. * * @param times the event times * @param seriesBean the series * @return the appointment */ protected Act create(Times times, IMObjectBean seriesBean) { Act act = (Act) service.create(event.getArchetypeId()); IMObjectBean bean = populate(act, times, current); bean.setTarget("author", current.getAuthor()); seriesBean.addTarget("items", act, "repeat"); return act; } /** * Updates an event. * <p> * If {@link #updateTimesOnly} is {@code false}, then {@link #populate(IMObjectBean, State)} will be invoked to * populate the event with the {@code state}. * * @param act the event * @param times the event times * @param state the state to populate the event from * @return the event */ protected IMObjectBean populate(Act act, Times times, State state) { act.setActivityStartTime(times.getStartTime()); act.setActivityEndTime(times.getEndTime()); IMObjectBean bean = service.getBean(act); bean.setTarget("schedule", state.getSchedule()); if (!updateTimesOnly) { populate(bean, state); } return bean; } /** * Populates an event from state. This is invoked after the event times and schedule have been set. * * @param bean the event bean * @param state the state */ protected void populate(IMObjectBean bean, State state) { } /** * Determines if the series can be calculated. * * @param state the current event state * @return {@code true} if the series can be calculated */ protected boolean canCalculateSeries(State state) { return state.getSchedule() != null; } /** * Returns the archetype service. * * @return the service */ protected IArchetypeService getService() { return service; } /** * Calculates the times for the event series. * * @param series used to collect the times * @return the first overlapping event, or {@code null} if there are no overlaps */ private Overlap calculateSeries(List<Times> series) { Overlap overlap = null; int index = acts.indexOf(event); if (current.repeats() && (acts.isEmpty() || index >= 0)) { Date startTime = event.getActivityStartTime(); Date endTime = event.getActivityEndTime(); Duration duration = new Duration(new DateTime(startTime), new DateTime(endTime)); if (canCalculateSeries(current)) { List<Times> times = new ArrayList<>(); times.add(Times.create(event)); ListIterator<Act> iterator = (index + 1 < acts.size()) ? acts.listIterator(index + 1) : null; RepeatExpression expression = current.getExpression(); RepeatCondition condition = current.getCondition(); Predicate<Date> max = new TimesPredicate<>(maxEvents - 1); Predicate<Date> predicate = PredicateUtils.andPredicate(max, condition.create()); while ((startTime = expression.getRepeatAfter(startTime, predicate)) != null) { endTime = new DateTime(startTime).plus(duration).toDate(); IMObjectReference reference = null; if (iterator != null && iterator.hasNext()) { Act act = iterator.next(); reference = act.getObjectReference(); } Times newEvent = new Times(reference, startTime, endTime); overlap = getOverlap(times, newEvent); if (overlap != null) { break; } times.add(newEvent); series.add(newEvent); } } } return overlap; } /** * Detects any overlap to the supplied event. * * @param series the series times, ordered on increasing start time * @param event the event the event * @return the overlap, or {@code null} if none is found */ private Overlap getOverlap(List<Times> series, Times event) { Overlap overlap = null; // comparator that treats overlapping time ranges as equal Comparator<Times> comparator = (o1, o2) -> { Date startTime1 = o1.getStartTime(); Date endTime1 = o1.getEndTime(); Date startTime2 = o2.getStartTime(); Date endTime2 = o2.getEndTime(); if (DateRules.compareTo(startTime1, startTime2) < 0 && DateRules.compareTo(endTime1, startTime2) <= 0) { return -1; } if (DateRules.compareTo(startTime2, endTime2) >= 0 && DateRules.compareTo(endTime1, endTime2) > 0) { return 1; } return Long.compare(o1.getId(), o2.getId()); }; int index = Collections.binarySearch(series, event, comparator); if (index >= 0) { overlap = new Overlap(series.get(index), event); } return overlap; } /** * Creates events corresponding to the expression. */ private void createEvents() { List<Times> times = new ArrayList<>(); calculateSeries(times); acts.clear(); acts.add(event); series = createSeries(); ActBean seriesBean = populateSeries(series, 0); List<Act> toSave = new ArrayList<>(); seriesBean.addNodeRelationship("items", event); toSave.add(event); toSave.add(series); for (Times t : times) { Act act = create(t, seriesBean); acts.add(act); toSave.add(act); } service.save(toSave); } /** * Creates a new calendar event series. * * @return a new <em>act.calendarEventSeries</em> act */ private Act createSeries() { series = (Act) service.create(ScheduleArchetypes.CALENDAR_EVENT_SERIES); series.setActivityStartTime(event.getActivityStartTime()); return series; } /** * Updates a series. * * @return {@code true} if changes were made */ private boolean updateSeries() { boolean result; List<Times> times = new ArrayList<>(); Overlap overlap = calculateSeries(times); if (overlap != null) { result = false; } else { int index = acts.indexOf(event); if (index >= 0) { List<Act> future = Collections.emptyList(); if (index + 1 < acts.size()) { future = acts.subList(index + 1, acts.size()); } updateSeries(future, times, index); acts = new ArrayList<>(acts.subList(index, acts.size())); result = true; } else { // shouldn't occur result = false; } } return result; } /** * Updates a series. * * @param acts the acts to update * @param times the new times * @param actsPriorToEvent the no. of acts in the series prior to the event */ private void updateSeries(List<Act> acts, List<Times> times, int actsPriorToEvent) { Act oldSeries = series; boolean createSeries = !current.repeatEquals(previous); // create a new series if the repeat expression or condition has changed Act currentSeries = (createSeries) ? createSeries() : series; ActBean bean = populateSeries(currentSeries, actsPriorToEvent); ActBean oldBean = (createSeries) ? new ActBean(oldSeries, service) : bean; acts = new ArrayList<>(acts); // copy to avoid modifying source Iterator<Times> timesIterator = times.iterator(); Iterator<Act> iterator = acts.listIterator(); List<Act> toSave = new ArrayList<>(); toSave.add(event); while (timesIterator.hasNext()) { Act act; if (iterator.hasNext()) { act = iterator.next(); iterator.remove(); populate(act, timesIterator.next(), current); if (oldSeries != currentSeries) { oldBean.removeNodeRelationships("items", act); bean.addNodeRelationship("items", act); } } else { act = create(timesIterator.next(), bean); } toSave.add(act); } if (oldSeries != currentSeries) { oldBean.removeNodeRelationships("items", event); bean.addNodeRelationship("items", event); } // any remaining acts need to be removed. Detach them from their series for (Act act : acts) { oldBean.removeNodeRelationships("items", act); toSave.add(act); } if (!toSave.isEmpty()) { if (oldSeries != currentSeries) { toSave.add(oldSeries); } toSave.add(currentSeries); service.save(toSave); } if (!acts.isEmpty()) { for (Act act : acts) { service.remove(act); } } } /** * Deletes the events after the current event, and unlinks it from the series. * If no acts reference the series act, it is also removed. */ private void deleteSeries() { int index = acts.indexOf(event); if (index >= 0) { List<Act> future = Collections.emptyList(); if (index + 1 < acts.size()) { // delete the acts after the event. future = acts.subList(index + 1, acts.size()); } deleteSeries(future); acts.clear(); } } /** * Deletes the series. * * @param acts the acts to delete */ private void deleteSeries(List<Act> acts) { ActBean bean = new ActBean(series, service); for (Act act : acts) { bean.removeNodeRelationships("items", act); } bean.removeNodeRelationships("items", event); List<Act> toSave = new ArrayList<>(acts); toSave.add(series); toSave.add(event); service.save(toSave); for (Act act : acts) { service.remove(act); } if (bean.getValues("items", ActRelationship.class).isEmpty()) { service.remove(series); } series = null; } /** * Populates the series with the current expression and condition. * * @param series the series to populate * @param actsPriorToEvent the no. of acts in the series prior to the event * @return the series bean */ private ActBean populateSeries(Act series, int actsPriorToEvent) { ActBean seriesBean = new ActBean(series, service); String expr = null; Integer interval = null; String units = null; Date endTime = null; Integer times = null; RepeatExpression expression = current.getExpression(); RepeatCondition condition = current.getCondition(); if (expression instanceof CalendarRepeatExpression) { CalendarRepeatExpression calendar = (CalendarRepeatExpression) expression; interval = calendar.getInterval(); units = calendar.getUnits().toString(); } else { expr = ((CronRepeatExpression) expression).getExpression(); } if (condition instanceof RepeatUntilDateCondition) { endTime = ((RepeatUntilDateCondition) condition).getDate(); } else { times = ((RepeatNTimesCondition) condition).getTimes() + actsPriorToEvent; } seriesBean.setValue("interval", interval); seriesBean.setValue("units", units); seriesBean.setValue("expression", expr); seriesBean.setValue("endTime", endTime); seriesBean.setValue("times", times); return seriesBean; } /** * Returns all of the acts in the series. * * @param event the event * @param series the series * @return all of the acts in the series */ private List<Act> getEvents(Act event, IMObjectBean series) { List<Reference> items = series.getTargetRefs("items"); items.remove(event.getObjectReference()); List<Act> result; result = ActHelper.getActs(items); result.add(event); return ActHelper.sort(result); } /** * Event series state. */ protected static class State { /** * The event start time. */ private Date startTime; /** * The event end time. */ private Date endTime; /** * The schedule. */ private Reference schedule; /** * The author. */ private Reference author; /** * The expression. */ private RepeatExpression expression; /** * The condition. */ private RepeatCondition condition; /** * Initialises the state from an event. * * @param event the event */ public State(IMObjectBean event) { update(event); } /** * Copy constructor. * * @param state the state to copy */ public State(State state) { this.startTime = state.startTime; this.endTime = state.endTime; this.schedule = state.schedule; this.author = state.author; this.expression = state.expression; this.condition = state.condition; } /** * Updates the state from an event. * * @param event the event */ public void update(IMObjectBean event) { Act act = (Act) event.getObject(); startTime = act.getActivityStartTime(); endTime = act.getActivityEndTime(); schedule = event.getTargetRef("schedule"); author = event.getTargetRef("author"); } /** * Returns the event start time. * * @return the start time */ public Date getStartTime() { return startTime; } /** * Returns the expression. * * @return the expression. May be {@code null} */ public RepeatExpression getExpression() { return expression; } /** * Sets the expression. * * @param expression the expression. May be {@code null} */ public void setExpression(RepeatExpression expression) { this.expression = expression; } /** * Sets the condition. * * @param condition the condition. May be {@code null} */ public void setCondition(RepeatCondition condition) { this.condition = condition; } /** * Returns the condition. * * @return the condition. May be {@code null} */ public RepeatCondition getCondition() { return condition; } public boolean repeats() { return expression != null && condition != null; } public Reference getSchedule() { return schedule; } public Reference getAuthor() { return author; } public boolean repeatEquals(State other) { return new EqualsBuilder().append(expression, other.expression).append(condition, other.condition) .isEquals(); } /** * Indicates whether some other object is "equal to" this one. * * @param obj the reference object with which to compare. * @return {@code true} if this object is the same as the obj */ @Override public boolean equals(Object obj) { boolean result; if (obj == this) { result = true; } else if (!(obj instanceof State)) { result = false; } else { State other = (State) obj; if (DateRules.compareTo(startTime, other.startTime) != 0 || DateRules.compareTo(endTime, other.endTime) != 0) { result = false; } else { result = new EqualsBuilder().append(schedule, other.schedule).append(author, other.author) .append(expression, other.expression).append(condition, other.condition).isEquals(); } } return result; } } }