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