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