Skip to content

Commit

Permalink
Merge pull request #2567 from bardsoftware/tkt-2548-upgrade-ical4j
Browse files Browse the repository at this point in the history
Upgrade ical4j library
  • Loading branch information
dbarashev authored Oct 3, 2024
2 parents 75e088e + 3d21781 commit b4b5b0d
Show file tree
Hide file tree
Showing 11 changed files with 247 additions and 77 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ public static CalendarEvent newEvent(Date date, boolean isRecurring, Type type,
myColor = color;
}

public Date getDate() { return myDate; }
public String getTitle() {
return myTitle;
}
Expand Down
15 changes: 15 additions & 0 deletions biz.ganttproject.core/src/main/java/org/w3c/util/DateParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
// Please first read the full copyright statement in file COPYRIGHT.html
package org.w3c.util;

import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
Expand Down Expand Up @@ -277,4 +282,14 @@ public static void main(String args[]) {
test(new Date());
}

private static final DateFormat ourIsoDateFormatter = new SimpleDateFormat("yyyy-MM-dd");

public static Date toJavaDate(LocalDate localDate) {
try {
return ourIsoDateFormatter.parse(localDate.format(DateTimeFormatter.ISO_DATE));
} catch (ParseException e) {
throw new RuntimeException("Failure parsing date " + localDate + " from MS Project file", e);
}
}

}
10 changes: 9 additions & 1 deletion biz.ganttproject.impex.ical/build.gradle
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
plugins {
id "org.jetbrains.kotlin.jvm"
}

configurations {
implementation.extendsFrom(providedCompile)
}
Expand All @@ -6,7 +10,11 @@ dependencies {
providedCompile project(':biz.ganttproject.app.libs')
providedCompile project(':biz.ganttproject.core')
providedCompile project(':ganttproject')
implementation 'org.mnode.ical4j:ical4j:1.+'
implementation 'org.mnode.ical4j:ical4j:4.0.2'

testImplementation 'org.junit.jupiter:junit-jupiter-api:5.11.0'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.11.0'

}

task copyPlugin(dependsOn: jar) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public static void main(String[] args) throws Exception {
}
CalendarBuilder builder = new CalendarBuilder();
Calendar c = builder.build(new UnfoldingReader(new FileReader(mainArgs.file.get(0))));
for (Component comp : (List<Component>)c.getComponents()) {
for (Component comp : c.getComponents()) {
System.err.println(comp);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,32 +22,15 @@
import biz.ganttproject.app.DefaultLocalizer;
import biz.ganttproject.app.InternationalizationKt;
import biz.ganttproject.core.calendar.CalendarEvent;
import biz.ganttproject.core.calendar.GPCalendarCalc;
import biz.ganttproject.core.time.TimeDuration;
import biz.ganttproject.core.time.impl.GPTimeUnitStack;
import com.google.common.collect.Lists;
import net.fortuna.ical4j.data.CalendarBuilder;
import net.fortuna.ical4j.data.ParserException;
import net.fortuna.ical4j.data.UnfoldingReader;
import net.fortuna.ical4j.model.Calendar;
import net.fortuna.ical4j.model.Component;
import net.fortuna.ical4j.model.Date;
import net.fortuna.ical4j.model.Property;
import net.fortuna.ical4j.model.Recur;
import net.fortuna.ical4j.model.component.VEvent;
import net.fortuna.ical4j.model.property.RRule;
import net.fortuna.ical4j.model.property.Summary;
import net.fortuna.ical4j.util.CompatibilityHints;
import net.sourceforge.ganttproject.GPLogger;
import net.sourceforge.ganttproject.calendar.CalendarEditorPanel;
import net.sourceforge.ganttproject.importer.ImporterBase;
import net.sourceforge.ganttproject.wizard.AbstractWizard;
import net.sourceforge.ganttproject.wizard.WizardPage;

import javax.swing.*;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.*;
import java.util.Collections;
import java.util.List;

Expand All @@ -59,7 +42,7 @@
*/
public class IcsFileImporter extends ImporterBase {
private static final LoggerApi LOGGER = GPLogger.create("Import.Ics");
private static DefaultLocalizer ourLocalizer = InternationalizationKt.getRootLocalizer();
private static final DefaultLocalizer ourLocalizer = InternationalizationKt.getRootLocalizer();
private final CalendarEditorPage myEditorPage;

public IcsFileImporter() {
Expand Down Expand Up @@ -110,7 +93,7 @@ public void setFile(File file) {
*/
static class CalendarEditorPage implements WizardPage {
private File myFile;
private JPanel myPanel = new JPanel();
private final JPanel myPanel = new JPanel();
private List<CalendarEvent> myEvents;
private void setFile(File f) {
myFile = f;
Expand Down Expand Up @@ -151,48 +134,14 @@ public void setActive(AbstractWizard wizard) {
* Reads calendar events from file
* @return a list of events if file was parsed successfully or null otherwise
*/
private static List<CalendarEvent> readEvents(File f) {
static List<CalendarEvent> readEvents(File f) {
try {
CompatibilityHints.setHintEnabled(CompatibilityHints.KEY_RELAXED_PARSING, true);
CalendarBuilder builder = new CalendarBuilder();
List<CalendarEvent> gpEvents = Lists.newArrayList();
Calendar c = builder.build(new UnfoldingReader(new FileReader(f)));
for (Component comp : (List<Component>)c.getComponents()) {
if (comp instanceof VEvent) {
VEvent event = (VEvent) comp;
if (event.getStartDate() == null) {
LOGGER.debug("No start date found, ignoring. Event={}", new Object[] {event}, Collections.emptyMap());
continue;
}
Date eventStartDate = event.getStartDate().getDate();
if (event.getEndDate() == null) {
LOGGER.debug("No end date found, using start date instead. Event={}", new Object[] {event}, Collections.emptyMap());
}
Date eventEndDate = event.getEndDate() == null ? eventStartDate : event.getEndDate().getDate();
TimeDuration oneDay = GPTimeUnitStack.createLength(GPTimeUnitStack.DAY, 1);
if (eventEndDate != null) {
java.util.Date startDate = GPTimeUnitStack.DAY.adjustLeft(eventStartDate);
java.util.Date endDate = GPTimeUnitStack.DAY.adjustLeft(eventEndDate);
RRule recurrenceRule = (RRule) event.getProperty(Property.RRULE);
boolean recursYearly = false;
if (recurrenceRule != null) {
recursYearly = Recur.YEARLY.equals(recurrenceRule.getRecur().getFrequency()) && 1 == recurrenceRule.getRecur().getInterval();
}
while (startDate.compareTo(endDate) < 0) {
Summary summary = event.getSummary();
gpEvents.add(CalendarEvent.newEvent(
startDate, recursYearly, CalendarEvent.Type.HOLIDAY,
summary == null ? "" : summary.getValue(),
null));
startDate = GPCalendarCalc.PLAIN.shiftDate(startDate, oneDay);
}
}
}
}
return gpEvents;
return IcsImport.readEvents(new FileInputStream(f));
} catch (IOException | ParserException e) {
GPLogger.log(e);
return null;
}
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
* Copyright 2013-2024 BarD Software s.r.o., Dmitry Barashev.
*
* This file is part of GanttProject, an opensource project management tool.
*
* GanttProject is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* GanttProject is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with GanttProject. If not, see <http://www.gnu.org/licenses/>.
*/
package biz.ganttproject.impex.ical;

import biz.ganttproject.LoggerApi;
import biz.ganttproject.core.calendar.CalendarEvent;
import biz.ganttproject.core.calendar.GPCalendarCalc;
import biz.ganttproject.core.time.TimeDuration;
import biz.ganttproject.core.time.impl.GPTimeUnitStack;
import com.google.common.collect.Lists;
import net.fortuna.ical4j.data.CalendarBuilder;
import net.fortuna.ical4j.data.ParserException;
import net.fortuna.ical4j.data.UnfoldingReader;
import net.fortuna.ical4j.model.Calendar;
import net.fortuna.ical4j.model.Property;
import net.fortuna.ical4j.model.component.CalendarComponent;
import net.fortuna.ical4j.model.component.VEvent;
import net.fortuna.ical4j.model.property.DateProperty;
import net.fortuna.ical4j.model.property.RRule;
import net.fortuna.ical4j.model.property.Summary;
import net.fortuna.ical4j.transform.recurrence.Frequency;
import net.fortuna.ical4j.util.CompatibilityHints;
import net.sourceforge.ganttproject.GPLogger;
import org.w3c.util.DateParser;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.time.*;
import java.time.temporal.Temporal;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;

class IcsImport {
static List<CalendarEvent> readEvents(InputStream input) throws ParserException, IOException {
CompatibilityHints.setHintEnabled(CompatibilityHints.KEY_RELAXED_PARSING, true);
CalendarBuilder builder = new CalendarBuilder();
List<CalendarEvent> gpEvents = Lists.newArrayList();
Calendar c = builder.build(new UnfoldingReader(new InputStreamReader(input)));
for (CalendarComponent comp : c.getComponents()) {
if (comp instanceof VEvent) {
VEvent event = (VEvent) comp;

if (event.getDateTimeStart().isEmpty()) {
LOGGER.debug("No start date found, ignoring. Event={}", new Object[] {event}, Collections.emptyMap());
continue;
}
var eventStartDate = event.getDateTimeStart().get().getDate();
if (event.getDateTimeEnd().isEmpty()) {
LOGGER.debug("No end date found, using start date instead. Event={}", new Object[] {event}, Collections.emptyMap());
}
var eventEndDate = event.getDateTimeEnd().map(DateProperty::getDate).orElse(eventStartDate);
TimeDuration oneDay = GPTimeUnitStack.createLength(GPTimeUnitStack.DAY, 1);
if (eventEndDate != null) {
java.util.Date startDate = GPTimeUnitStack.DAY.adjustLeft(getDate(eventStartDate));
java.util.Date endDate = GPTimeUnitStack.DAY.adjustLeft(getDate(eventEndDate));
var recursYearly = new AtomicBoolean(false);
event.getProperty(Property.RRULE).ifPresent(it -> {
if (it instanceof RRule<?> recurrenceRule) {
var frequency = recurrenceRule.getRecur().getFrequency();
var interval = recurrenceRule.getRecur().getInterval();
recursYearly.set(frequency == Frequency.YEARLY && interval == 1);
}
});
while (startDate.compareTo(endDate) < 0) {
var summary = event.getSummary().map(Summary::getValue).orElse("");

gpEvents.add(CalendarEvent.newEvent(
startDate, recursYearly.get(), CalendarEvent.Type.HOLIDAY,
summary.trim(),
null));
startDate = GPCalendarCalc.PLAIN.shiftDate(startDate, oneDay);
}
}
}
}
return gpEvents;

}

private static java.util.Date getDate(Temporal t) {
if (t instanceof LocalDate localDate) {
return DateParser.toJavaDate(localDate);
}
if (t instanceof Instant instant) {
return java.util.Date.from(instant);
}
if (t instanceof LocalDateTime localDateTime) {
return DateParser.toJavaDate(localDateTime.toLocalDate());
}
if (t instanceof ZonedDateTime zonedDateTime) {
return DateParser.toJavaDate(zonedDateTime.toLocalDate());
}
if (t instanceof OffsetDateTime offsetDateTime) {
return DateParser.toJavaDate(offsetDateTime.toLocalDate());
}
throw new IllegalArgumentException("Unsupported temporal type: " + t.getClass());
}

private static final LoggerApi LOGGER = GPLogger.create("Import.Ics");

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Copyright 2024 BarD Software s.r.o., Dmitry Barashev.
*
* This file is part of GanttProject, an opensource project management tool.
*
* GanttProject is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* GanttProject is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with GanttProject. If not, see <http://www.gnu.org/licenses/>.
*/
package biz.ganttproject.impex.ical

import biz.ganttproject.core.calendar.CalendarEvent
import biz.ganttproject.core.time.CalendarFactory
import biz.ganttproject.core.time.CalendarFactory.LocaleApi
import biz.ganttproject.core.time.CalendarFactory.setLocaleApi
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.w3c.util.DateParser
import java.text.DateFormat
import java.util.Locale

class IcsFileImporterTest {
@BeforeEach
fun setUp() {
object : CalendarFactory() {
init {
setLocaleApi(object : LocaleApi {
override fun getLocale(): Locale {
return Locale.US
}

override fun getShortDateFormat(): DateFormat {
return DateFormat.getDateInstance(DateFormat.SHORT, Locale.US)
}
})
}
}
}
@Test
fun `basic smoke test`() {
val events = IcsImport.readEvents(IcsFileImporterTest::class.java.getResourceAsStream("/test.ics")!!)
assertFalse(events.isEmpty())
assertEquals(DateParser.parse("2023-12-07"), events[0].date)
assertEquals("Chanukah: 1 Candle", events[0].title)
assertEquals(CalendarEvent.Type.HOLIDAY ,events[0].type)
}
}
27 changes: 27 additions & 0 deletions biz.ganttproject.impex.ical/src/test/resources/test.ics
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//hebcal.com/NONSGML Hebcal Calendar v15.0.6//EN
CALSCALE:GREGORIAN
METHOD:PUBLISH
X-LOTUS-CHARSET:UTF-8
X-PUBLISHED-TTL:PT14D
X-WR-CALNAME:Jewish Holidays ✡️
X-WR-CALDESC:Major Jewish holidays for the Diaspora from Hebcal.com
X-APPLE-CALENDAR-COLOR:#800002
BEGIN:VEVENT
DTSTAMP:20240501T202041Z
CATEGORIES:Holiday
SUMMARY: Chanukah: 1 Candle
DTSTART;VALUE=DATE:20231207
DTEND;VALUE=DATE:20231208
UID:hebcal-20231207-1c6fa26d
TRANSP:TRANSPARENT
X-MICROSOFT-CDO-BUSYSTATUS:FREE
X-MICROSOFT-CDO-ALLDAYEVENT:TRUE
CLASS:PUBLIC
DESCRIPTION:Hanukkah\, the Jewish festival of rededication. Also known as
the Festival of Lights\, the eight-day festival is observed by lighting th
e candles of a hanukkiah (menorah)\n\nhttps://hebcal.com/h/chanukah-2023?u
c=ical-jewish-holidays-v2
END:VEVENT
END:VCALENDAR
Loading

0 comments on commit b4b5b0d

Please sign in to comment.