]> git.basschouten.com Git - openhab-addons.git/blob
895d2a48b9f4dc86c8a6278e4543528595de097c
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.icalendar.internal.logic;
14
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;
24
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27
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;
41
42 /**
43  * Implementation of {@link AbstractPresentableCalendar} with ical4j. Please
44  * use {@link AbstractPresentableCalendar#create(InputStream)} for productive
45  * instantiation.
46  *
47  * @author Michael Wodniok - Initial contribution
48  * @author Andrew Fiddian-Green - Methods getJustBegunEvents() & getJustEndedEvents()
49  */
50 @NonNullByDefault
51 class BiweeklyPresentableCalendar extends AbstractPresentableCalendar {
52     private final ICalendar usedCalendar;
53
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.");
59             }
60             this.usedCalendar = currentCalendar;
61         }
62     }
63
64     @Override
65     public @Nullable Event getCurrentEvent(Instant instant) {
66         final VEventWPeriod currentComponentWPeriod = this.getCurrentComponentWPeriod(instant);
67         if (currentComponentWPeriod == null) {
68             return null;
69         }
70
71         return currentComponentWPeriod.toEvent();
72     }
73
74     @Override
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)) {
84                     continue;
85                 } else if (begInst.isAfter(frameEnd)) {
86                     break;
87                 }
88                 // fall through => means we are within the time frame
89                 Duration duration = getEventLength(event);
90                 if (duration == null) {
91                     duration = Duration.ofMinutes(1);
92                 }
93                 eventList.add(new VEventWPeriod(event, begInst, begInst.plus(duration)).toEvent());
94                 break;
95             }
96         }
97         return eventList;
98     }
99
100     @Override
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) {
107                 continue;
108             }
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)) {
115                     continue;
116                 } else if (endInst.isAfter(frameEnd)) {
117                     break;
118                 }
119                 // fall through => means we are within the time frame
120                 eventList.add(new VEventWPeriod(event, begInst, endInst).toEvent());
121                 break;
122             }
123         }
124         return eventList;
125     }
126
127     @Override
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) {
137                 continue;
138             }
139             startDates.advanceTo(Date.from(instant));
140             while (startDates.hasNext()) {
141                 final Instant startInstant = startDates.next().toInstant();
142                 if (startInstant.isAfter(instant)) {
143                     @Nullable
144                     final Uid currentEventUid = currentEvent.getUid();
145                     if (currentEventUid == null || !isCounteredBy(startInstant, currentEventUid, negativeEvents)) {
146                         candidates.add(new VEventWPeriod(currentEvent, startInstant, startInstant.plus(duration)));
147                         break;
148                     }
149                 }
150             }
151         }
152         VEventWPeriod earliestNextEvent = null;
153         for (final VEventWPeriod positiveCandidate : candidates) {
154             if (earliestNextEvent == null || earliestNextEvent.start.isAfter(positiveCandidate.start)) {
155                 earliestNextEvent = positiveCandidate;
156             }
157         }
158
159         if (earliestNextEvent == null) {
160             return null;
161         }
162         return earliestNextEvent.toEvent();
163     }
164
165     @Override
166     public boolean isEventPresent(Instant instant) {
167         return (this.getCurrentComponentWPeriod(instant) != null);
168     }
169
170     /**
171      * Classifies events into positive and negative ones.
172      *
173      * @param positiveEvents A List where to add positive ones.
174      * @param negativeEvents A List where to add negative ones.
175      */
176     private void classifyEvents(Collection<VEvent> positiveEvents, Collection<VEvent> negativeEvents) {
177         for (final VEvent currentEvent : usedCalendar.getEvents()) {
178             @Nullable
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);
183         }
184     }
185
186     /**
187      * Searches for a current event at given Instant.
188      *
189      * @param instant The Instant to use for finding events.
190      * @return A VEventWPeriod describing the event or null if there is none.
191      */
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);
196
197         for (final VEvent currentEvent : positiveEvents) {
198             final DateIterator startDates = this.getRecurredEventDateIterator(currentEvent);
199             final Duration duration = getEventLength(currentEvent);
200             if (duration == null) {
201                 continue;
202             }
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)) {
208                     @Nullable
209                     final Uid eventUid = currentEvent.getUid();
210                     if (eventUid == null || !isCounteredBy(startInstant, eventUid, negativeEvents)) {
211                         return new VEventWPeriod(currentEvent, startInstant, endInstant);
212                     }
213                 }
214                 if (startInstant.isAfter(instant.plus(duration))) {
215                     break;
216                 }
217             }
218         }
219
220         return null;
221     }
222
223     /**
224      * Finds a duration of the event.
225      *
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.
228      */
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());
234         }
235         final DateStart start = vEvent.getDateStart();
236         final DateEnd end = vEvent.getDateEnd();
237         if (start == null || end == null) {
238             return null;
239         }
240         return Duration.between(start.getValue().toInstant(), end.getValue().toInstant());
241     }
242
243     /**
244      * Retrieves a DateIterator to iterate through the events occurrences.
245      *
246      * @param vEvent The VEvent to create the iterator for.
247      * @return The DateIterator for {@link VEvent}
248      */
249     private DateIterator getRecurredEventDateIterator(VEvent vEvent) {
250         final TimezoneInfo tzinfo = this.usedCalendar.getTimezoneInfo();
251
252         final DateStart firstStart = vEvent.getDateStart();
253         TimeZone tz;
254         if (tzinfo.isFloating(firstStart)) {
255             tz = TimeZone.getDefault();
256         } else {
257             final TimezoneAssignment startAssignment = tzinfo.getTimezone(firstStart);
258             tz = (startAssignment == null ? TimeZone.getTimeZone("UTC") : startAssignment.getTimeZone());
259         }
260         return vEvent.getDateIterator(tz);
261     }
262
263     /**
264      * Checks whether an counter event blocks an event with given uid and start.
265      *
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.
270      */
271     private boolean isCounteredBy(Instant startInstant, Uid eventUid, Collection<VEvent> counterEvents) {
272         for (final VEvent counterEvent : counterEvents) {
273             @Nullable
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)) {
281                         return true;
282                     }
283                 }
284             }
285         }
286         return false;
287     }
288
289     /**
290      * A Class describing an event together with a start and end instant.
291      *
292      * @author Michael Wodniok - Initial contribution.
293      */
294     private static class VEventWPeriod {
295         final VEvent vEvent;
296         final Instant start;
297         final Instant end;
298
299         public VEventWPeriod(VEvent vEvent, Instant start, Instant end) {
300             this.vEvent = vEvent;
301             this.start = start;
302             this.end = end;
303         }
304
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);
311         }
312     }
313 }