From 7669fa905ea57e2bc47ac8233d8fadd6ff9ac153 Mon Sep 17 00:00:00 2001
From: srosse <stephane.rosse@frentix.com>
Date: Fri, 8 Mar 2019 09:58:02 +0100
Subject: [PATCH] OO-3952: rewrite EXDATE to match standard in iCal feed

---
 .../olat/commons/calendar/CalendarUtils.java  | 23 ++++-
 .../olat/commons/calendar/ICalServlet.java    | 95 ++++++++++++++++---
 2 files changed, 98 insertions(+), 20 deletions(-)

diff --git a/src/main/java/org/olat/commons/calendar/CalendarUtils.java b/src/main/java/org/olat/commons/calendar/CalendarUtils.java
index d5da7238248..c98006f9699 100644
--- a/src/main/java/org/olat/commons/calendar/CalendarUtils.java
+++ b/src/main/java/org/olat/commons/calendar/CalendarUtils.java
@@ -45,7 +45,8 @@ import net.fortuna.ical4j.model.property.ExDate;
 
 public class CalendarUtils {
 	private static final OLog log = Tracing.createLoggerFor(CalendarUtils.class);
-	private static final SimpleDateFormat ical4jFormatter = new SimpleDateFormat("yyyyMMdd");
+	private static final SimpleDateFormat ical4jDateFormatter = new SimpleDateFormat("yyyyMMdd");
+	private static final SimpleDateFormat ical4jDateTimeFormatter = new SimpleDateFormat("yyyyMMdd'T'HHmmss");
 	private static final SimpleDateFormat occurenceDateTimeFormat = new SimpleDateFormat("yyyyMMdd'T'HHmmss");
 
 	public static String getTimeAsString(Date date, Locale locale) {
@@ -187,8 +188,8 @@ public class CalendarUtils {
 	public static net.fortuna.ical4j.model.Date createDate(Date date) {
 		try {
 			String toString;
-			synchronized(ical4jFormatter) {//cluster_OK only to optimize memory/speed
-				toString = ical4jFormatter.format(date);
+			synchronized(ical4jDateFormatter) {//cluster_OK only to optimize memory/speed
+				toString = ical4jDateFormatter.format(date);
 			}
 			return new net.fortuna.ical4j.model.Date(toString);
 		} catch (ParseException e) {
@@ -196,12 +197,24 @@ public class CalendarUtils {
 		}
 	}
 	
+	public static net.fortuna.ical4j.model.DateTime createDateTime(Date date) {
+		try {
+			String toString;
+			synchronized(ical4jDateTimeFormatter) {//cluster_OK only to optimize memory/speed
+				toString = ical4jDateTimeFormatter.format(date);
+			}
+			return new net.fortuna.ical4j.model.DateTime(toString);
+		} catch (ParseException e) {
+			return null;
+		}
+	}
+	
 	public static String formatRecurrenceDate(Date date, boolean allDay) {
 		try {
 			String toString;
 			if(allDay) {
-				synchronized(ical4jFormatter) {//cluster_OK only to optimize memory/speed
-					toString = ical4jFormatter.format(date);
+				synchronized(ical4jDateFormatter) {//cluster_OK only to optimize memory/speed
+					toString = ical4jDateFormatter.format(date);
 				}
 			} else {
 				synchronized(occurenceDateTimeFormat) {//cluster_OK only to optimize memory/speed
diff --git a/src/main/java/org/olat/commons/calendar/ICalServlet.java b/src/main/java/org/olat/commons/calendar/ICalServlet.java
index c042501f34e..7302ef9c62e 100644
--- a/src/main/java/org/olat/commons/calendar/ICalServlet.java
+++ b/src/main/java/org/olat/commons/calendar/ICalServlet.java
@@ -30,6 +30,7 @@ import java.io.IOException;
 import java.io.Writer;
 import java.net.URISyntaxException;
 import java.net.URL;
+import java.util.Date;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
@@ -59,14 +60,19 @@ import net.fortuna.ical4j.data.ParserException;
 import net.fortuna.ical4j.model.Calendar;
 import net.fortuna.ical4j.model.Component;
 import net.fortuna.ical4j.model.ComponentList;
+import net.fortuna.ical4j.model.DateList;
 import net.fortuna.ical4j.model.Parameter;
 import net.fortuna.ical4j.model.Property;
 import net.fortuna.ical4j.model.PropertyList;
+import net.fortuna.ical4j.model.TimeZone;
 import net.fortuna.ical4j.model.ValidationException;
 import net.fortuna.ical4j.model.component.VEvent;
 import net.fortuna.ical4j.model.component.VTimeZone;
 import net.fortuna.ical4j.model.parameter.TzId;
+import net.fortuna.ical4j.model.parameter.Value;
 import net.fortuna.ical4j.model.property.CalScale;
+import net.fortuna.ical4j.model.property.DtStart;
+import net.fortuna.ical4j.model.property.ExDate;
 import net.fortuna.ical4j.model.property.Uid;
 import net.fortuna.ical4j.model.property.Url;
 import net.fortuna.ical4j.model.property.Version;
@@ -346,6 +352,8 @@ public class ICalServlet extends HttpServlet {
 			return Agent.googleCalendar;
 		} else if(userAgent.startsWith("Java/1.")) {
 			return Agent.java;
+		} else if(userAgent.indexOf("CalendarAgent/") >= 0) {
+			return Agent.calendar;
 		}
 		return Agent.unkown;
 	}
@@ -404,25 +412,82 @@ public class ICalServlet extends HttpServlet {
 		try {
 			ComponentList events = calendar.getComponents();
 			for (final Iterator<?> i = events.iterator(); i.hasNext();) {
-				Object comp = i.next();
-				String event = comp.toString();
-				if (agent == Agent.outlook && comp instanceof VEvent) {
-					event = quoteTimeZone(event, (VEvent)comp, timezoneIds);
-				}
-				if(agent == Agent.googleCalendar) {
-					event = event.replace("CLASS:PRIVATE" + Strings.LINE_SEPARATOR, "");
-					event = event.replace("X-OLAT-MANAGED:all" + Strings.LINE_SEPARATOR, "");
-					event = event.replace("DESCRIPTION:" + Strings.LINE_SEPARATOR, "");
-					event = event.replace("LOCATION:" + Strings.LINE_SEPARATOR, "");
-				}
-				
-				out.write(event);
+				outputCalendarComponent(i.next(), out, agent, timezoneIds);
 			}
 		} catch (IOException | OLATRuntimeException e) {
 			log.error("", e);
 		}
 	}
 	
+	private void outputCalendarComponent(Object component, Writer out, Agent agent, Set<String> timezoneIds) throws IOException {
+		if (component instanceof VEvent) {
+			rewriteExDate((VEvent)component);
+		}
+
+		String event = component.toString();
+		if (agent == Agent.outlook && component instanceof VEvent) {
+			event = quoteTimeZone(event, (VEvent)component, timezoneIds);
+		}
+		if(agent == Agent.googleCalendar) {
+			event = event.replace("CLASS:PRIVATE" + Strings.LINE_SEPARATOR, "");
+			event = event.replace("X-OLAT-MANAGED:all" + Strings.LINE_SEPARATOR, "");
+			event = event.replace("DESCRIPTION:" + Strings.LINE_SEPARATOR, "");
+			event = event.replace("LOCATION:" + Strings.LINE_SEPARATOR, "");
+		}
+		
+		out.write(event);
+	}
+	
+	/**
+	 * 
+	 * @param event The event to rewrite
+	 */
+	private void rewriteExDate(VEvent event) {
+		DtStart start = event.getStartDate();
+		ExDate exDate = (ExDate)event.getProperties().getProperty(Property.EXDATE);
+
+		if(exDate != null && start != null) {
+			Date startDate = start.getDate();
+			java.util.Calendar startCal = java.util.Calendar.getInstance();
+			startCal.setTime(startDate);
+				
+			TimeZone startZone = start.getTimeZone();
+			TimeZone exZone = exDate.getTimeZone();
+			DateList dateList = exDate.getDates();
+			
+			java.util.Calendar excCal = java.util.Calendar.getInstance();
+
+			Parameter dateParameter = event.getProperties().getProperty(Property.DTSTART)
+					.getParameters().getParameter(Value.DATE.getName());
+			boolean dateOnly = dateParameter != null;
+
+			DateList newDateList = dateOnly ? new DateList(Value.DATE) : new DateList();
+			for(Object obj:dateList) {
+				Date d = (Date)obj;
+				if(dateOnly) {
+					newDateList.add(CalendarUtils.createDate(d));
+				} else {
+					excCal.setTime(d);
+					if(excCal.get(java.util.Calendar.HOUR_OF_DAY) != startCal.get(java.util.Calendar.HOUR_OF_DAY)) {
+						excCal.set(java.util.Calendar.HOUR_OF_DAY, startCal.get(java.util.Calendar.HOUR_OF_DAY));
+						excCal.set(java.util.Calendar.MINUTE, startCal.get(java.util.Calendar.MINUTE));
+						excCal.set(java.util.Calendar.SECOND, startCal.get(java.util.Calendar.SECOND));
+						d = excCal.getTime();
+					}
+					newDateList.add(CalendarUtils.createDateTime(d));
+				}
+			}
+			
+			ExDate newExDate = new ExDate(newDateList);
+			if(exZone == null && startZone != null) {
+				newExDate.setTimeZone(startZone);
+			}
+			
+			event.getProperties().remove(exDate);
+			event.getProperties().add(newExDate);
+		}
+	}
+	
 	private String quoteTimeZone(String event, VEvent vEvent, Set<String> timezoneIds) {
 		if(vEvent == null || vEvent.getStartDate().getTimeZone() == null
 				|| vEvent.getStartDate().getTimeZone().getVTimeZone() == null) {
@@ -502,8 +567,7 @@ public class ICalServlet extends HttpServlet {
 				URL resource = ResourceLoader.getResource("zoneinfo-outlook/" + id + ".ics");
 				CalendarBuilder builder = new CalendarBuilder();
 				Calendar calendar = builder.build(resource.openStream());
-				VTimeZone vTimeZone = (VTimeZone)calendar.getComponent(Component.VTIMEZONE);
-				return vTimeZone;
+				return (VTimeZone)calendar.getComponent(Component.VTIMEZONE);
 			} catch (Exception e) {
 				log.error("", e);
 				return null;
@@ -515,6 +579,7 @@ public class ICalServlet extends HttpServlet {
     		unkown,
     		outlook,
     		googleCalendar,
+    		calendar,// macos, iOS
     		java
     }
 }
\ No newline at end of file
-- 
GitLab