2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.icalendar.internal.logic;
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;
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.openhab.binding.icalendar.internal.logic.EventTextFilter.Type;
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;
51 * Implementation of {@link AbstractPresentableCalendar} with ical4j. Please
52 * use {@link AbstractPresentableCalendar#create(InputStream)} for productive
55 * @author Michael Wodniok - Initial contribution
56 * @author Andrew Fiddian-Green - Methods getJustBegunEvents() & getJustEndedEvents()
57 * @author Michael Wodniok - Extension for filtered events
60 class BiweeklyPresentableCalendar extends AbstractPresentableCalendar {
61 private final ICalendar usedCalendar;
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.");
69 this.usedCalendar = currentCalendar;
74 public @Nullable Event getCurrentEvent(Instant instant) {
75 final VEventWPeriod currentComponentWPeriod = this.getCurrentComponentWPeriod(instant);
76 if (currentComponentWPeriod == null) {
80 return currentComponentWPeriod.toEvent();
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)) {
94 } else if (begInst.isAfter(frameEnd)) {
97 // fall through => means we are within the time frame
98 Duration duration = getEventLength(event);
99 if (duration == null) {
100 duration = Duration.ofMinutes(1);
102 eventList.add(new VEventWPeriod(event, begInst, begInst.plus(duration)).toEvent());
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) {
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)) {
125 } else if (endInst.isAfter(frameEnd)) {
128 // fall through => means we are within the time frame
129 eventList.add(new VEventWPeriod(event, begInst, endInst).toEvent());
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) {
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)));
160 VEventWPeriod earliestNextEvent = null;
161 for (final VEventWPeriod positiveCandidate : candidates) {
162 if (earliestNextEvent == null || earliestNextEvent.start.isAfter(positiveCandidate.start)) {
163 earliestNextEvent = positiveCandidate;
167 if (earliestNextEvent == null) {
170 return earliestNextEvent.toEvent();
174 public boolean isEventPresent(Instant instant) {
175 return (this.getCurrentComponentWPeriod(instant) != null);
179 public List<Event> getFilteredEventsBetween(Instant begin, Instant end, @Nullable EventTextFilter filter,
181 List<VEventWPeriod> candidates = this.getVEventWPeriodsBetween(begin, end, maximumCount);
182 final List<Event> results = new ArrayList<>(candidates.size());
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);
190 filterPattern = Pattern.compile(filter.value);
193 Class<? extends TextProperty> propertyClass;
194 switch (filter.field) {
196 propertyClass = Summary.class;
199 propertyClass = Comment.class;
202 propertyClass = Contact.class;
205 propertyClass = Description.class;
208 propertyClass = Location.class;
211 throw new IllegalArgumentException("Unknown Property to filter for.");
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()) {
222 }).collect(Collectors.toList());
223 candidates = filteredCandidates;
226 for (VEventWPeriod eventWPeriod : candidates) {
227 results.add(eventWPeriod.toEvent());
230 Collections.sort(results);
232 return results.subList(0, (maximumCount > results.size() ? results.size() : maximumCount));
236 * Finds events which begin in the given frame.
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.
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);
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)) {
258 Duration duration = getEventLength(positiveEvent);
259 if (duration == null) {
260 duration = Duration.ZERO;
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);
269 if (maximumPerSeries != 0 && foundInSeries >= maximumPerSeries) {
274 eventList.add(resultingVEWP);
276 if (maximumPerSeries != 0 && foundInSeries >= maximumPerSeries) {
287 * Classifies events into positive and negative ones.
289 * @param positiveEvents A List where to add positive ones.
290 * @param negativeEvents A List where to add negative ones.
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);
302 * Searches for a current event at given Instant.
304 * @param instant The Instant to use for finding events.
305 * @return A VEventWPeriod describing the event or null if there is none.
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);
312 for (final VEvent currentEvent : positiveEvents) {
313 final DateIterator startDates = this.getRecurredEventDateIterator(currentEvent);
314 final Duration duration = getEventLength(currentEvent);
315 if (duration == null) {
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);
328 if (startInstant.isAfter(instant.plus(duration))) {
338 * Finds a duration of the event.
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.
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());
349 final DateStart start = vEvent.getDateStart();
350 final DateEnd end = vEvent.getDateEnd();
351 if (start == null || end == null) {
354 return Duration.between(start.getValue().toInstant(), end.getValue().toInstant());
358 * Retrieves a DateIterator to iterate through the events occurrences.
360 * @param vEvent The VEvent to create the iterator for.
361 * @return The DateIterator for {@link VEvent}
363 private DateIterator getRecurredEventDateIterator(VEvent vEvent) {
364 final TimezoneInfo tzinfo = this.usedCalendar.getTimezoneInfo();
366 final DateStart firstStart = vEvent.getDateStart();
368 if (tzinfo.isFloating(firstStart)) {
369 tz = TimeZone.getDefault();
371 final TimezoneAssignment startAssignment = tzinfo.getTimezone(firstStart);
372 tz = (startAssignment == null ? TimeZone.getTimeZone("UTC") : startAssignment.getTimeZone());
374 return vEvent.getDateIterator(tz);
378 * Checks whether an counter event blocks an event with given uid and start.
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.
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)) {
403 * A Class describing an event together with a start and end instant.
405 * @author Michael Wodniok - Initial contribution.
407 private static class VEventWPeriod {
412 public VEventWPeriod(VEvent vEvent, Instant start, Instant end) {
413 this.vEvent = vEvent;
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);