"UnsupportedOperationException: TimeZone is not applicable to current value" when parsing iCalendar from iCloud
Closed this issue · 4 comments
We got this parsing error over a support request:
EXCEPTION
at.bitfire.ical4android.InvalidCalendarException: Couldn't parse iCalendar
at at.bitfire.ical4android.ICalendar$Companion.fromReader(ICalendar.kt:161)
at at.bitfire.ical4android.Event$Companion.eventsFromReader(Event.kt:8)
…
Caused by: net.fortuna.ical4j.data.ParserException: Error at line 22:TimeZone is not applicable to current value
at net.fortuna.ical4j.data.CalendarParserImpl.parse(CalendarParserImpl.java:17)
at net.fortuna.ical4j.data.CalendarBuilder.build(CalendarBuilder.java:3)
at net.fortuna.ical4j.data.CalendarBuilder.build(CalendarBuilder.java:2)
at at.bitfire.ical4android.ICalendar$Companion.fromReader(ICalendar.kt:59)
... 37 more
Caused by: java.lang.UnsupportedOperationException: TimeZone is not applicable to current value
at net.fortuna.ical4j.model.property.DateListProperty.setTimeZone(DateListProperty.java:64)
at net.fortuna.ical4j.data.DefaultContentHandler.resolveTimezones(DefaultContentHandler.java:63)
at net.fortuna.ical4j.data.DefaultContentHandler.endCalendar(DefaultContentHandler.java:1)
at net.fortuna.ical4j.data.CalendarParserImpl.parseCalendar(CalendarParserImpl.java:50)
at net.fortuna.ical4j.data.CalendarParserImpl.parseCalendarList(CalendarParserImpl.java:15)
at net.fortuna.ical4j.data.CalendarParserImpl.parse(CalendarParserImpl.java:13)
... 40 more
I think it's because a TZID
was applied to a DateListProperty
without dates, something like RDATE;TZID=Some/TZ:
. Unfortunately we don't have the iCalendar (I have requested it, but little hope to actually get it) and I can't reproduce with this test:
class Ical4jTest {
@Test
fun testDateList_WithoutDates_WithTZ() {
val cal = ICalendar.fromReader(StringReader("BEGIN:VCALENDAR\r\n" +
"VERSION:2.0\r\n" +
"BEGIN:VEVENT\r\n" +
"SUMMARY:Test\r\n" +
"DTSTART;TZID=Europe/Vienna:20230824T161334\r\n" +
"RDATE;TZID=Europe/Vienna:\r\n" +
"END:VEVENT\r\n" +
"END:VCALENDAR"))
val event = cal.getComponent<VEvent>(Component.VEVENT)
assertEquals("Test", event.summary.value)
val rDate = event.getProperty<RDate>(Property.RDATE)
assertNull(rDate.dates)
}
}
Here we see that dates
is not null, but an empty array, and then the code that causes the exception doesn't make any problems:
- Manage to reproduce the problem
- Provide a PR for ical4j so that this is ignored, at least in lenient mode – as far as I can see, there's no disadvantage in ignoring the timezone in this case.
Updating
TLDR; no way to reproduce yet
After some tests without success, I will keep here everything I find.
The only way that DateListProperty
doesn't get any value for dates
is for constructor
/**
* @param name the property name
* @param parameters property parameters
*/
public DateListProperty(final String name, final ParameterList parameters, PropertyFactory factory) {
super(name, parameters, factory);
}
at RDate
, this constructor is called here:
/**
* @param aList a list of parameters for this component
* @param aValue a value string for this component
* @throws ParseException where the specified value string is not a valid date-time/date representation
*/
public RDate(final ParameterList aList, final String aValue)
throws ParseException {
super(RDATE, aList, new Factory());
periods = new PeriodList(false, true);
setValue(aValue);
}
However, this constructor calls setValue
, so it's quite improbable that dates
doesn't get initialized.
After looking at this, there's an empty constructor in RDate
that doesn't initialize anything for dates
:
/**
* Default constructor.
*/
public RDate() {
super(RDATE, new Factory());
periods = new PeriodList(false, true);
}
Then, if we do
@Test
fun testRDate_withoutDates() {
val rDate = RDate()
rDate.timeZone = tzReg.getTimeZone("Europe/Madrid")
}
nothing happens. This is because internally this constructor calls an initialization of PeriodList
with
public PeriodList(boolean utc, final boolean unmodifiable) {
this.utc = utc;
this.unmodifiable = unmodifiable;
if (unmodifiable) {
periods = Collections.emptySet();
}
else {
periods = new TreeSet<Period>();
}
}
unmodifiable
set to true, which initializes PeriodList.periods
, that then it's used in RDate
:
@Override
public final void setTimeZone(TimeZone timezone) {
if (periods != null && !(periods.isEmpty() && periods.isUnmodifiable())) {
periods.setTimeZone(timezone);
} else {
super.setTimeZone(timezone);
}
}
So the problematic setTimeZone
is not called here 😢. To sum up, I can't find a way to build an invalid RDate
, so back to the drawing board.
After seeing this overridden method, I've realized that ExDate
doesn't override anything, so maybe the issue is here. However, this test is also not-problematic:
@Test
fun testRDate_withoutDates() {
val rDate = ExDate()
rDate.timeZone = tzReg.getTimeZone("Europe/Madrid")
}
This is because the empty constructor calls the DateListProperty(final String name, PropertyFactory factory)
constructor, which then initializes dates
with
this(name, new DateList(Value.DATE_TIME), factory);
Again, so way dates
is null.
Now I feel stupid for not having tried passing a null
DateList
. If this is called, the exception is thrown, so we have to find somewhere that dates
might have been null.
@Test
fun testRDate_withoutDates() {
val list: DateList? = null
val exDate = ExDate(list)
exDate.timeZone = tzReg.getTimeZone("Europe/Madrid")
}
which btw also fails with RDate, obviously
. To search for this, it's important to note that there no way calling setValue
or constructing with Strings doesn't end with a null dates
, so we have to stick with other constructors.
I can't find anyhere that could be initializing with a null DateList
@rfc2822 I've tried everything I can think of... Isn't there any way to get the original ical?
@rfc2822 I've tried everything I can think of... Isn't there any way to get the original ical?
I have requested it, but I doubt the person is able to fetch & send it… we will see.
I'll leave it open for now, maybe we really get the ICS or manage to somehow reproduce it.
Missing information, reopen when occurring again.