]> git.basschouten.com Git - openhab-addons.git/blob
4e107b50843ef078b51ab1c606798fbcd9797a14
[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);
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      * @return All events which begin in the time frame.
241      */
242     private List<VEventWPeriod> getVEventWPeriodsBetween(Instant frameBegin, Instant frameEnd) {
243         final List<VEvent> positiveEvents = new ArrayList<>();
244         final List<VEvent> negativeEvents = new ArrayList<>();
245         classifyEvents(positiveEvents, negativeEvents);
246
247         final List<VEventWPeriod> eventList = new ArrayList<>();
248         for (final VEvent positiveEvent : positiveEvents) {
249             final DateIterator positiveBeginDates = getRecurredEventDateIterator(positiveEvent);
250             positiveBeginDates.advanceTo(Date.from(frameBegin));
251             while (positiveBeginDates.hasNext()) {
252                 final Instant begInst = positiveBeginDates.next().toInstant();
253                 if (begInst.isAfter(frameEnd)) {
254                     break;
255                 }
256                 Duration duration = getEventLength(positiveEvent);
257                 if (duration == null) {
258                     duration = Duration.ZERO;
259                 }
260
261                 final VEventWPeriod resultingVEWP = new VEventWPeriod(positiveEvent, begInst, begInst.plus(duration));
262                 final Uid eventUid = positiveEvent.getUid();
263                 if (eventUid != null) {
264                     if (!isCounteredBy(begInst, eventUid, negativeEvents)) {
265                         eventList.add(resultingVEWP);
266                     }
267                 } else {
268                     eventList.add(resultingVEWP);
269                 }
270             }
271         }
272
273         return eventList;
274     }
275
276     /**
277      * Classifies events into positive and negative ones.
278      *
279      * @param positiveEvents A List where to add positive ones.
280      * @param negativeEvents A List where to add negative ones.
281      */
282     private void classifyEvents(Collection<VEvent> positiveEvents, Collection<VEvent> negativeEvents) {
283         for (final VEvent currentEvent : usedCalendar.getEvents()) {
284             final Status eventStatus = currentEvent.getStatus();
285             boolean positive = (eventStatus == null || (eventStatus.isTentative() || eventStatus.isConfirmed()));
286             final Collection<VEvent> positiveOrNegativeEvents = (positive ? positiveEvents : negativeEvents);
287             positiveOrNegativeEvents.add(currentEvent);
288         }
289     }
290
291     /**
292      * Searches for a current event at given Instant.
293      *
294      * @param instant The Instant to use for finding events.
295      * @return A VEventWPeriod describing the event or null if there is none.
296      */
297     private @Nullable VEventWPeriod getCurrentComponentWPeriod(Instant instant) {
298         final List<VEvent> negativeEvents = new ArrayList<VEvent>();
299         final List<VEvent> positiveEvents = new ArrayList<VEvent>();
300         classifyEvents(positiveEvents, negativeEvents);
301
302         for (final VEvent currentEvent : positiveEvents) {
303             final DateIterator startDates = this.getRecurredEventDateIterator(currentEvent);
304             final Duration duration = getEventLength(currentEvent);
305             if (duration == null) {
306                 continue;
307             }
308             startDates.advanceTo(Date.from(instant.minus(duration)));
309             while (startDates.hasNext()) {
310                 final Instant startInstant = startDates.next().toInstant();
311                 final Instant endInstant = startInstant.plus(duration);
312                 if (startInstant.isBefore(instant) && endInstant.isAfter(instant)) {
313                     final Uid eventUid = currentEvent.getUid();
314                     if (eventUid == null || !isCounteredBy(startInstant, eventUid, negativeEvents)) {
315                         return new VEventWPeriod(currentEvent, startInstant, endInstant);
316                     }
317                 }
318                 if (startInstant.isAfter(instant.plus(duration))) {
319                     break;
320                 }
321             }
322         }
323
324         return null;
325     }
326
327     /**
328      * Finds a duration of the event.
329      *
330      * @param vEvent The event to find out the duration.
331      * @return Either a Duration describing the events length or null, if no information is available.
332      */
333     private static @Nullable Duration getEventLength(VEvent vEvent) {
334         final DurationProperty duration = vEvent.getDuration();
335         if (duration != null) {
336             final biweekly.util.Duration eventDuration = duration.getValue();
337             return Duration.ofMillis(eventDuration.toMillis());
338         }
339         final DateStart start = vEvent.getDateStart();
340         final DateEnd end = vEvent.getDateEnd();
341         if (start == null || end == null) {
342             return null;
343         }
344         return Duration.between(start.getValue().toInstant(), end.getValue().toInstant());
345     }
346
347     /**
348      * Retrieves a DateIterator to iterate through the events occurrences.
349      *
350      * @param vEvent The VEvent to create the iterator for.
351      * @return The DateIterator for {@link VEvent}
352      */
353     private DateIterator getRecurredEventDateIterator(VEvent vEvent) {
354         final TimezoneInfo tzinfo = this.usedCalendar.getTimezoneInfo();
355
356         final DateStart firstStart = vEvent.getDateStart();
357         TimeZone tz;
358         if (tzinfo.isFloating(firstStart)) {
359             tz = TimeZone.getDefault();
360         } else {
361             final TimezoneAssignment startAssignment = tzinfo.getTimezone(firstStart);
362             tz = (startAssignment == null ? TimeZone.getTimeZone("UTC") : startAssignment.getTimeZone());
363         }
364         return vEvent.getDateIterator(tz);
365     }
366
367     /**
368      * Checks whether an counter event blocks an event with given uid and start.
369      *
370      * @param startInstant The start of the event.
371      * @param eventUid The uid of the event.
372      * @param counterEvents Events that may counter.
373      * @return True if a counter event exists that matches uid and start, else false.
374      */
375     private boolean isCounteredBy(Instant startInstant, Uid eventUid, Collection<VEvent> counterEvents) {
376         for (final VEvent counterEvent : counterEvents) {
377             final Uid counterEventUid = counterEvent.getUid();
378             if (counterEventUid != null && eventUid.getValue().contentEquals(counterEventUid.getValue())) {
379                 final DateIterator counterStartDates = getRecurredEventDateIterator(counterEvent);
380                 counterStartDates.advanceTo(Date.from(startInstant));
381                 if (counterStartDates.hasNext()) {
382                     final Instant counterStartInstant = counterStartDates.next().toInstant();
383                     if (counterStartInstant.equals(startInstant)) {
384                         return true;
385                     }
386                 }
387             }
388         }
389         return false;
390     }
391
392     /**
393      * A Class describing an event together with a start and end instant.
394      *
395      * @author Michael Wodniok - Initial contribution.
396      */
397     private static class VEventWPeriod {
398         final VEvent vEvent;
399         final Instant start;
400         final Instant end;
401
402         public VEventWPeriod(VEvent vEvent, Instant start, Instant end) {
403             this.vEvent = vEvent;
404             this.start = start;
405             this.end = end;
406         }
407
408         public Event toEvent() {
409             final Summary eventSummary = vEvent.getSummary();
410             final String title = eventSummary != null ? eventSummary.getValue() : "-";
411             final Description eventDescription = vEvent.getDescription();
412             final String description = eventDescription != null ? eventDescription.getValue() : "";
413             return new Event(title, start, end, description);
414         }
415     }
416 }