]> git.basschouten.com Git - openhab-addons.git/blob
61d2f9a25184da5334a3a1bdef1286dc83e3dba1
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.Collections;
22 import java.util.Date;
23 import java.util.List;
24 import java.util.TimeZone;
25 import java.util.regex.Pattern;
26 import java.util.stream.Collectors;
27
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.openhab.binding.icalendar.internal.logic.EventTextFilter.Type;
31
32 import biweekly.ICalendar;
33 import biweekly.component.VEvent;
34 import biweekly.io.TimezoneAssignment;
35 import biweekly.io.TimezoneInfo;
36 import biweekly.io.text.ICalReader;
37 import biweekly.parameter.Range;
38 import biweekly.property.Comment;
39 import biweekly.property.Contact;
40 import biweekly.property.DateEnd;
41 import biweekly.property.DateStart;
42 import biweekly.property.Description;
43 import biweekly.property.DurationProperty;
44 import biweekly.property.Location;
45 import biweekly.property.RecurrenceId;
46 import biweekly.property.Status;
47 import biweekly.property.Summary;
48 import biweekly.property.TextProperty;
49 import biweekly.property.Uid;
50 import biweekly.util.ICalDate;
51 import biweekly.util.com.google.ical.compat.javautil.DateIterator;
52
53 /**
54  * Implementation of {@link AbstractPresentableCalendar} with ical4j. Please
55  * use {@link AbstractPresentableCalendar#create(InputStream)} for productive
56  * instantiation.
57  *
58  * @author Michael Wodniok - Initial contribution
59  * @author Andrew Fiddian-Green - Methods getJustBegunEvents() & getJustEndedEvents()
60  * @author Michael Wodniok - Extension for filtered events
61  * @author Michael Wodniok - Added logic for events moved with "RECURRENCE-ID" (issue 9647)
62  * @author Michael Wodniok - Extended logic for defined behavior with parallel current events
63  *         (issue 10808)
64  */
65 @NonNullByDefault
66 class BiweeklyPresentableCalendar extends AbstractPresentableCalendar {
67     private final ICalendar usedCalendar;
68
69     BiweeklyPresentableCalendar(InputStream streamed) throws IOException, CalendarException {
70         try (final ICalReader reader = new ICalReader(streamed)) {
71             final ICalendar currentCalendar = reader.readNext();
72             if (currentCalendar == null) {
73                 throw new CalendarException("No calendar was parsed.");
74             }
75             this.usedCalendar = currentCalendar;
76         }
77     }
78
79     @Override
80     public @Nullable Event getCurrentEvent(Instant instant) {
81         final VEventWPeriod currentComponentWPeriod = this.getCurrentComponentWPeriod(instant);
82         if (currentComponentWPeriod == null) {
83             return null;
84         }
85
86         return currentComponentWPeriod.toEvent();
87     }
88
89     @Override
90     public List<Event> getJustBegunEvents(Instant frameBegin, Instant frameEnd) {
91         final List<Event> eventList = new ArrayList<>();
92         // process all the events in the iCalendar
93         for (final VEvent event : usedCalendar.getEvents()) {
94             // iterate over all begin dates
95             final DateIterator begDates = getRecurredEventDateIterator(event);
96             while (begDates.hasNext()) {
97                 final Instant begInst = begDates.next().toInstant();
98                 if (begInst.isBefore(frameBegin)) {
99                     continue;
100                 } else if (begInst.isAfter(frameEnd)) {
101                     break;
102                 }
103                 // fall through => means we are within the time frame
104                 Duration duration = getEventLength(event);
105                 if (duration == null) {
106                     duration = Duration.ofMinutes(1);
107                 }
108                 eventList.add(new VEventWPeriod(event, begInst, begInst.plus(duration)).toEvent());
109                 break;
110             }
111         }
112         return eventList;
113     }
114
115     @Override
116     public List<Event> getJustEndedEvents(Instant frameBegin, Instant frameEnd) {
117         final List<Event> eventList = new ArrayList<>();
118         // process all the events in the iCalendar
119         for (final VEvent event : usedCalendar.getEvents()) {
120             final Duration duration = getEventLength(event);
121             if (duration == null) {
122                 continue;
123             }
124             // iterate over all begin dates
125             final DateIterator begDates = getRecurredEventDateIterator(event);
126             while (begDates.hasNext()) {
127                 final Instant begInst = begDates.next().toInstant();
128                 final Instant endInst = begInst.plus(duration);
129                 if (endInst.isBefore(frameBegin)) {
130                     continue;
131                 } else if (endInst.isAfter(frameEnd)) {
132                     break;
133                 }
134                 // fall through => means we are within the time frame
135                 eventList.add(new VEventWPeriod(event, begInst, endInst).toEvent());
136                 break;
137             }
138         }
139         return eventList;
140     }
141
142     @Override
143     public @Nullable Event getNextEvent(Instant instant) {
144         final Collection<VEventWPeriod> candidates = new ArrayList<VEventWPeriod>();
145         final Collection<VEvent> negativeEvents = new ArrayList<VEvent>();
146         final Collection<VEvent> positiveEvents = new ArrayList<VEvent>();
147         classifyEvents(positiveEvents, negativeEvents);
148         for (final VEvent currentEvent : positiveEvents) {
149             final DateIterator startDates = this.getRecurredEventDateIterator(currentEvent);
150             final Duration duration = getEventLength(currentEvent);
151             if (duration == null) {
152                 continue;
153             }
154             startDates.advanceTo(Date.from(instant));
155             while (startDates.hasNext()) {
156                 final Instant startInstant = startDates.next().toInstant();
157                 if (startInstant.isAfter(instant)) {
158                     final Uid currentEventUid = currentEvent.getUid();
159                     if (currentEventUid == null || !isCounteredBy(startInstant, currentEventUid, negativeEvents)) {
160                         candidates.add(new VEventWPeriod(currentEvent, startInstant, startInstant.plus(duration)));
161                         break;
162                     }
163                 }
164             }
165         }
166         VEventWPeriod earliestNextEvent = null;
167         for (final VEventWPeriod positiveCandidate : candidates) {
168             if (earliestNextEvent == null || earliestNextEvent.start.isAfter(positiveCandidate.start)) {
169                 earliestNextEvent = positiveCandidate;
170             }
171         }
172
173         if (earliestNextEvent == null) {
174             return null;
175         }
176         return earliestNextEvent.toEvent();
177     }
178
179     @Override
180     public boolean isEventPresent(Instant instant) {
181         return (this.getCurrentComponentWPeriod(instant) != null);
182     }
183
184     @Override
185     public List<Event> getFilteredEventsBetween(Instant begin, Instant end, @Nullable EventTextFilter filter,
186             int maximumCount) {
187         List<VEventWPeriod> candidates = this.getVEventWPeriodsBetween(begin, end, maximumCount);
188         final List<Event> results = new ArrayList<>(candidates.size());
189
190         if (filter != null) {
191             Pattern filterPattern;
192             if (filter.type == Type.TEXT) {
193                 filterPattern = Pattern.compile(".*" + Pattern.quote(filter.value) + ".*",
194                         Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
195             } else {
196                 filterPattern = Pattern.compile(filter.value);
197             }
198
199             Class<? extends TextProperty> propertyClass;
200             switch (filter.field) {
201                 case SUMMARY:
202                     propertyClass = Summary.class;
203                     break;
204                 case COMMENT:
205                     propertyClass = Comment.class;
206                     break;
207                 case CONTACT:
208                     propertyClass = Contact.class;
209                     break;
210                 case DESCRIPTION:
211                     propertyClass = Description.class;
212                     break;
213                 case LOCATION:
214                     propertyClass = Location.class;
215                     break;
216                 default:
217                     throw new IllegalArgumentException("Unknown Property to filter for.");
218             }
219
220             List<VEventWPeriod> filteredCandidates = candidates.stream().filter(current -> {
221                 List<? extends TextProperty> properties = current.vEvent.getProperties(propertyClass);
222                 for (TextProperty prop : properties) {
223                     if (filterPattern.matcher(prop.getValue()).matches()) {
224                         return true;
225                     }
226                 }
227                 return false;
228             }).collect(Collectors.toList());
229             candidates = filteredCandidates;
230         }
231
232         for (VEventWPeriod eventWPeriod : candidates) {
233             results.add(eventWPeriod.toEvent());
234         }
235
236         Collections.sort(results);
237
238         return results.subList(0, (maximumCount > results.size() ? results.size() : maximumCount));
239     }
240
241     /**
242      * Finds events which begin in the given frame.
243      *
244      * @param frameBegin Begin of the frame where to search events.
245      * @param frameEnd End of the time frame where to search events.
246      * @param maximumPerSeries Limit the results per series. Set to 0 for no limit.
247      * @return All events which begin in the time frame.
248      */
249     private List<VEventWPeriod> getVEventWPeriodsBetween(Instant frameBegin, Instant frameEnd, int maximumPerSeries) {
250         final List<VEvent> positiveEvents = new ArrayList<>();
251         final List<VEvent> negativeEvents = new ArrayList<>();
252         classifyEvents(positiveEvents, negativeEvents);
253
254         final List<VEventWPeriod> eventList = new ArrayList<>();
255         for (final VEvent positiveEvent : positiveEvents) {
256             final DateIterator positiveBeginDates = getRecurredEventDateIterator(positiveEvent);
257             positiveBeginDates.advanceTo(Date.from(frameBegin));
258             int foundInSeries = 0;
259             while (positiveBeginDates.hasNext()) {
260                 final Instant begInst = positiveBeginDates.next().toInstant();
261                 if (begInst.isAfter(frameEnd) || begInst.equals(frameEnd)) {
262                     break;
263                 }
264                 Duration duration = getEventLength(positiveEvent);
265                 if (duration == null) {
266                     duration = Duration.ZERO;
267                 }
268
269                 final VEventWPeriod resultingVEWP = new VEventWPeriod(positiveEvent, begInst, begInst.plus(duration));
270                 final Uid eventUid = positiveEvent.getUid();
271                 if (eventUid != null) {
272                     if (!isCounteredBy(begInst, eventUid, negativeEvents)) {
273                         eventList.add(resultingVEWP);
274                         foundInSeries++;
275                         if (maximumPerSeries != 0 && foundInSeries >= maximumPerSeries) {
276                             break;
277                         }
278                     }
279                 } else {
280                     eventList.add(resultingVEWP);
281                     foundInSeries++;
282                     if (maximumPerSeries != 0 && foundInSeries >= maximumPerSeries) {
283                         break;
284                     }
285                 }
286             }
287         }
288
289         return eventList;
290     }
291
292     /**
293      * Classifies events into positive and negative ones.
294      *
295      * @param positiveEvents A List where to add positive ones.
296      * @param negativeEvents A List where to add negative ones.
297      */
298     private void classifyEvents(Collection<VEvent> positiveEvents, Collection<VEvent> negativeEvents) {
299         for (final VEvent currentEvent : usedCalendar.getEvents()) {
300             final Status eventStatus = currentEvent.getStatus();
301             boolean positive = (eventStatus == null || (eventStatus.isTentative() || eventStatus.isConfirmed()));
302             final RecurrenceId eventRecurrenceId = currentEvent.getRecurrenceId();
303             if (positive && eventRecurrenceId != null) {
304                 // RecurrenceId moves an event. This blocks other events of series and creates a new single instance
305                 positiveEvents.add(currentEvent);
306                 negativeEvents.add(currentEvent);
307             } else {
308                 final Collection<VEvent> positiveOrNegativeEvents = (positive ? positiveEvents : negativeEvents);
309                 positiveOrNegativeEvents.add(currentEvent);
310             }
311         }
312     }
313
314     /**
315      * Searches for a current event at given Instant.
316      *
317      * @param instant The Instant to use for finding events.
318      * @return A VEventWPeriod describing the event or null if there is none.
319      */
320     private @Nullable VEventWPeriod getCurrentComponentWPeriod(Instant instant) {
321         final List<VEvent> negativeEvents = new ArrayList<VEvent>();
322         final List<VEvent> positiveEvents = new ArrayList<VEvent>();
323         classifyEvents(positiveEvents, negativeEvents);
324
325         VEventWPeriod earliestEndingEvent = null;
326
327         for (final VEvent currentEvent : positiveEvents) {
328             final DateIterator startDates = this.getRecurredEventDateIterator(currentEvent);
329             final Duration duration = getEventLength(currentEvent);
330             if (duration == null) {
331                 continue;
332             }
333             startDates.advanceTo(Date.from(instant.minus(duration)));
334             while (startDates.hasNext()) {
335                 final Instant startInstant = startDates.next().toInstant();
336                 final Instant endInstant = startInstant.plus(duration);
337                 if (startInstant.isBefore(instant) && endInstant.isAfter(instant)) {
338                     final Uid eventUid = currentEvent.getUid();
339                     if (eventUid == null || !isCounteredBy(startInstant, eventUid, negativeEvents)) {
340                         if (earliestEndingEvent == null || endInstant.isBefore(earliestEndingEvent.end)) {
341                             earliestEndingEvent = new VEventWPeriod(currentEvent, startInstant, endInstant);
342                         }
343                     }
344                 }
345                 if (startInstant.isAfter(instant.plus(duration))) {
346                     break;
347                 }
348             }
349         }
350
351         return earliestEndingEvent;
352     }
353
354     /**
355      * Finds a duration of the event.
356      *
357      * @param vEvent The event to find out the duration.
358      * @return Either a Duration describing the events length or null, if no information is available.
359      */
360     private static @Nullable Duration getEventLength(VEvent vEvent) {
361         final DurationProperty duration = vEvent.getDuration();
362         if (duration != null) {
363             final biweekly.util.Duration eventDuration = duration.getValue();
364             return Duration.ofMillis(eventDuration.toMillis());
365         }
366         final DateStart start = vEvent.getDateStart();
367         final DateEnd end = vEvent.getDateEnd();
368         if (start == null || end == null) {
369             return null;
370         }
371         return Duration.between(start.getValue().toInstant(), end.getValue().toInstant());
372     }
373
374     /**
375      * Retrieves a DateIterator to iterate through the events occurrences.
376      *
377      * @param vEvent The VEvent to create the iterator for.
378      * @return The DateIterator for {@link VEvent}
379      */
380     private DateIterator getRecurredEventDateIterator(VEvent vEvent) {
381         final TimezoneInfo tzinfo = this.usedCalendar.getTimezoneInfo();
382
383         final DateStart firstStart = vEvent.getDateStart();
384         TimeZone tz;
385         if (tzinfo.isFloating(firstStart)) {
386             tz = TimeZone.getDefault();
387         } else {
388             final TimezoneAssignment startAssignment = tzinfo.getTimezone(firstStart);
389             tz = (startAssignment == null ? TimeZone.getTimeZone("UTC") : startAssignment.getTimeZone());
390         }
391         return vEvent.getDateIterator(tz);
392     }
393
394     /**
395      * Checks whether an counter event blocks an event with given uid and start.
396      *
397      * @param startInstant The start of the event.
398      * @param eventUid The uid of the event.
399      * @param counterEvents Events that may counter.
400      * @return True if a counter event exists that matches uid and start, else false.
401      */
402     private boolean isCounteredBy(Instant startInstant, Uid eventUid, Collection<VEvent> counterEvents) {
403         for (final VEvent counterEvent : counterEvents) {
404             final Uid counterEventUid = counterEvent.getUid();
405             if (counterEventUid != null && eventUid.getValue().contentEquals(counterEventUid.getValue())) {
406                 final RecurrenceId counterRecurrenceId = counterEvent.getRecurrenceId();
407                 if (counterRecurrenceId != null) {
408                     ICalDate recurrenceDate = counterRecurrenceId.getValue();
409                     if (recurrenceDate != null) {
410                         Instant recurrenceInstant = Instant.ofEpochMilli(recurrenceDate.getTime());
411                         if (recurrenceInstant.equals(startInstant)) {
412                             return true;
413                         }
414                         Range futureOrPast = counterRecurrenceId.getRange();
415                         if (futureOrPast != null && futureOrPast.equals(Range.THIS_AND_FUTURE)
416                                 && startInstant.isAfter(recurrenceInstant)) {
417                             return true;
418                         }
419                         if (futureOrPast != null && futureOrPast.equals(Range.THIS_AND_PRIOR)
420                                 && startInstant.isBefore(recurrenceInstant)) {
421                             return true;
422                         }
423                     }
424                 } else {
425                     final DateIterator counterStartDates = getRecurredEventDateIterator(counterEvent);
426                     counterStartDates.advanceTo(Date.from(startInstant));
427                     if (counterStartDates.hasNext()) {
428                         final Instant counterStartInstant = counterStartDates.next().toInstant();
429                         if (counterStartInstant.equals(startInstant)) {
430                             return true;
431                         }
432                     }
433                 }
434             }
435         }
436         return false;
437     }
438
439     /**
440      * A Class describing an event together with a start and end instant.
441      *
442      * @author Michael Wodniok - Initial contribution.
443      */
444     private static class VEventWPeriod {
445         final VEvent vEvent;
446         final Instant start;
447         final Instant end;
448
449         public VEventWPeriod(VEvent vEvent, Instant start, Instant end) {
450             this.vEvent = vEvent;
451             this.start = start;
452             this.end = end;
453         }
454
455         public Event toEvent() {
456             final Summary eventSummary = vEvent.getSummary();
457             final String title = eventSummary != null ? eventSummary.getValue() : "-";
458             final Description eventDescription = vEvent.getDescription();
459             final String description = eventDescription != null ? eventDescription.getValue() : "";
460             return new Event(title, start, end, description);
461         }
462     }
463 }