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