2 * Copyright (c) 2010-2020 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.icalendar.internal.logic;
15 import java.io.IOException;
16 import java.io.InputStream;
17 import java.time.Duration;
18 import java.time.Instant;
19 import java.util.ArrayList;
20 import java.util.Collection;
21 import java.util.Date;
22 import java.util.List;
23 import java.util.TimeZone;
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
28 import biweekly.ICalendar;
29 import biweekly.component.VEvent;
30 import biweekly.io.TimezoneAssignment;
31 import biweekly.io.TimezoneInfo;
32 import biweekly.io.text.ICalReader;
33 import biweekly.property.DateEnd;
34 import biweekly.property.DateStart;
35 import biweekly.property.Description;
36 import biweekly.property.DurationProperty;
37 import biweekly.property.Status;
38 import biweekly.property.Summary;
39 import biweekly.property.Uid;
40 import biweekly.util.com.google.ical.compat.javautil.DateIterator;
43 * Implementation of {@link AbstractPresentableCalendar} with ical4j. Please
44 * use {@link AbstractPresentableCalendar#create(InputStream)} for productive
47 * @author Michael Wodniok - Initial contribution
48 * @author Andrew Fiddian-Green - Methods getJustBegunEvents() & getJustEndedEvents()
51 class BiweeklyPresentableCalendar extends AbstractPresentableCalendar {
52 private final ICalendar usedCalendar;
54 BiweeklyPresentableCalendar(InputStream streamed) throws IOException, CalendarException {
55 try (final ICalReader reader = new ICalReader(streamed)) {
56 final ICalendar currentCalendar = reader.readNext();
57 if (currentCalendar == null) {
58 throw new CalendarException("No calendar was parsed.");
60 this.usedCalendar = currentCalendar;
65 public @Nullable Event getCurrentEvent(Instant instant) {
66 final VEventWPeriod currentComponentWPeriod = this.getCurrentComponentWPeriod(instant);
67 if (currentComponentWPeriod == null) {
71 return currentComponentWPeriod.toEvent();
75 public List<Event> getJustBegunEvents(Instant frameBegin, Instant frameEnd) {
76 final List<Event> eventList = new ArrayList<>();
77 // process all the events in the iCalendar
78 for (final VEvent event : usedCalendar.getEvents()) {
79 // iterate over all begin dates
80 final DateIterator begDates = getRecurredEventDateIterator(event);
81 while (begDates.hasNext()) {
82 final Instant begInst = begDates.next().toInstant();
83 if (begInst.isBefore(frameBegin)) {
85 } else if (begInst.isAfter(frameEnd)) {
88 // fall through => means we are within the time frame
89 Duration duration = getEventLength(event);
90 if (duration == null) {
91 duration = Duration.ofMinutes(1);
93 eventList.add(new VEventWPeriod(event, begInst, begInst.plus(duration)).toEvent());
101 public List<Event> getJustEndedEvents(Instant frameBegin, Instant frameEnd) {
102 final List<Event> eventList = new ArrayList<>();
103 // process all the events in the iCalendar
104 for (final VEvent event : usedCalendar.getEvents()) {
105 final Duration duration = getEventLength(event);
106 if (duration == null) {
109 // iterate over all begin dates
110 final DateIterator begDates = getRecurredEventDateIterator(event);
111 while (begDates.hasNext()) {
112 final Instant begInst = begDates.next().toInstant();
113 final Instant endInst = begInst.plus(duration);
114 if (endInst.isBefore(frameBegin)) {
116 } else if (endInst.isAfter(frameEnd)) {
119 // fall through => means we are within the time frame
120 eventList.add(new VEventWPeriod(event, begInst, endInst).toEvent());
128 public @Nullable Event getNextEvent(Instant instant) {
129 final Collection<VEventWPeriod> candidates = new ArrayList<VEventWPeriod>();
130 final Collection<VEvent> negativeEvents = new ArrayList<VEvent>();
131 final Collection<VEvent> positiveEvents = new ArrayList<VEvent>();
132 classifyEvents(positiveEvents, negativeEvents);
133 for (final VEvent currentEvent : positiveEvents) {
134 final DateIterator startDates = this.getRecurredEventDateIterator(currentEvent);
135 final Duration duration = getEventLength(currentEvent);
136 if (duration == null) {
139 startDates.advanceTo(Date.from(instant));
140 while (startDates.hasNext()) {
141 final Instant startInstant = startDates.next().toInstant();
142 if (startInstant.isAfter(instant)) {
144 final Uid currentEventUid = currentEvent.getUid();
145 if (currentEventUid == null || !isCounteredBy(startInstant, currentEventUid, negativeEvents)) {
146 candidates.add(new VEventWPeriod(currentEvent, startInstant, startInstant.plus(duration)));
152 VEventWPeriod earliestNextEvent = null;
153 for (final VEventWPeriod positiveCandidate : candidates) {
154 if (earliestNextEvent == null || earliestNextEvent.start.isAfter(positiveCandidate.start)) {
155 earliestNextEvent = positiveCandidate;
159 if (earliestNextEvent == null) {
162 return earliestNextEvent.toEvent();
166 public boolean isEventPresent(Instant instant) {
167 return (this.getCurrentComponentWPeriod(instant) != null);
171 * Classifies events into positive and negative ones.
173 * @param positiveEvents A List where to add positive ones.
174 * @param negativeEvents A List where to add negative ones.
176 private void classifyEvents(Collection<VEvent> positiveEvents, Collection<VEvent> negativeEvents) {
177 for (final VEvent currentEvent : usedCalendar.getEvents()) {
179 final Status eventStatus = currentEvent.getStatus();
180 boolean positive = (eventStatus == null || (eventStatus.isTentative() || eventStatus.isConfirmed()));
181 final Collection<VEvent> positiveOrNegativeEvents = (positive ? positiveEvents : negativeEvents);
182 positiveOrNegativeEvents.add(currentEvent);
187 * Searches for a current event at given Instant.
189 * @param instant The Instant to use for finding events.
190 * @return A VEventWPeriod describing the event or null if there is none.
192 private @Nullable VEventWPeriod getCurrentComponentWPeriod(Instant instant) {
193 final List<VEvent> negativeEvents = new ArrayList<VEvent>();
194 final List<VEvent> positiveEvents = new ArrayList<VEvent>();
195 classifyEvents(positiveEvents, negativeEvents);
197 for (final VEvent currentEvent : positiveEvents) {
198 final DateIterator startDates = this.getRecurredEventDateIterator(currentEvent);
199 final Duration duration = getEventLength(currentEvent);
200 if (duration == null) {
203 startDates.advanceTo(Date.from(instant.minus(duration)));
204 while (startDates.hasNext()) {
205 final Instant startInstant = startDates.next().toInstant();
206 final Instant endInstant = startInstant.plus(duration);
207 if (startInstant.isBefore(instant) && endInstant.isAfter(instant)) {
209 final Uid eventUid = currentEvent.getUid();
210 if (eventUid == null || !isCounteredBy(startInstant, eventUid, negativeEvents)) {
211 return new VEventWPeriod(currentEvent, startInstant, endInstant);
214 if (startInstant.isAfter(instant.plus(duration))) {
224 * Finds a duration of the event.
226 * @param vEvent The event to find out the duration.
227 * @return Either a Duration describing the events length or null, if no information is available.
229 private static @Nullable Duration getEventLength(VEvent vEvent) {
230 final DurationProperty duration = vEvent.getDuration();
231 if (duration != null) {
232 final biweekly.util.Duration eventDuration = duration.getValue();
233 return Duration.ofMillis(eventDuration.toMillis());
235 final DateStart start = vEvent.getDateStart();
236 final DateEnd end = vEvent.getDateEnd();
237 if (start == null || end == null) {
240 return Duration.between(start.getValue().toInstant(), end.getValue().toInstant());
244 * Retrieves a DateIterator to iterate through the events occurrences.
246 * @param vEvent The VEvent to create the iterator for.
247 * @return The DateIterator for {@link VEvent}
249 private DateIterator getRecurredEventDateIterator(VEvent vEvent) {
250 final TimezoneInfo tzinfo = this.usedCalendar.getTimezoneInfo();
252 final DateStart firstStart = vEvent.getDateStart();
254 if (tzinfo.isFloating(firstStart)) {
255 tz = TimeZone.getDefault();
257 final TimezoneAssignment startAssignment = tzinfo.getTimezone(firstStart);
258 tz = (startAssignment == null ? TimeZone.getTimeZone("UTC") : startAssignment.getTimeZone());
260 return vEvent.getDateIterator(tz);
264 * Checks whether an counter event blocks an event with given uid and start.
266 * @param startInstant The start of the event.
267 * @param eventUid The uid of the event.
268 * @param counterEvents Events that may counter.
269 * @return True if a counter event exists that matches uid and start, else false.
271 private boolean isCounteredBy(Instant startInstant, Uid eventUid, Collection<VEvent> counterEvents) {
272 for (final VEvent counterEvent : counterEvents) {
274 final Uid counterEventUid = counterEvent.getUid();
275 if (counterEventUid != null && eventUid.getValue().contentEquals(counterEventUid.getValue())) {
276 final DateIterator counterStartDates = getRecurredEventDateIterator(counterEvent);
277 counterStartDates.advanceTo(Date.from(startInstant));
278 if (counterStartDates.hasNext()) {
279 final Instant counterStartInstant = counterStartDates.next().toInstant();
280 if (counterStartInstant.equals(startInstant)) {
290 * A Class describing an event together with a start and end instant.
292 * @author Michael Wodniok - Initial contribution.
294 private static class VEventWPeriod {
299 public VEventWPeriod(VEvent vEvent, Instant start, Instant end) {
300 this.vEvent = vEvent;
305 public Event toEvent() {
306 final Summary eventSummary = vEvent.getSummary();
307 final String title = eventSummary != null ? eventSummary.getValue() : "-";
308 final Description eventDescription = vEvent.getDescription();
309 final String description = eventDescription != null ? eventDescription.getValue() : "";
310 return new Event(title, start, end, description);