diff --git a/src/main/java/biweekly/component/VAlarm.java b/src/main/java/biweekly/component/VAlarm.java index 207cd557..9615651d 100644 --- a/src/main/java/biweekly/component/VAlarm.java +++ b/src/main/java/biweekly/component/VAlarm.java @@ -520,65 +520,79 @@ public void setTrigger(Trigger trigger) { protected void validate(List components, ICalVersion version, List warnings) { checkRequiredCardinality(warnings, Action.class, Trigger.class); + validateAction(warnings); + validateTrigger(components, warnings); + } + + @SuppressWarnings("unchecked") + private void validateAction(List warnings) { Action action = getAction(); - if (action != null) { - //AUDIO alarms should not have more than 1 attachment - if (action.isAudio()) { - if (getAttachments().size() > 1) { - warnings.add(new ValidationWarning(7)); - } - } + if (action == null) { + return; + } - //DESCRIPTION is required for DISPLAY alarms - if (action.isDisplay()) { - checkRequiredCardinality(warnings, Description.class); + //AUDIO alarms should not have more than 1 attachment + if (action.isAudio()) { + if (getAttachments().size() > 1) { + warnings.add(new ValidationWarning(7)); } + } - if (action.isEmail()) { - //SUMMARY and DESCRIPTION is required for EMAIL alarms - checkRequiredCardinality(warnings, Summary.class, Description.class); - - //EMAIL alarms must have at least 1 ATTENDEE - if (getAttendees().isEmpty()) { - warnings.add(new ValidationWarning(8)); - } - } else { - //only EMAIL alarms can have ATTENDEEs - if (!getAttendees().isEmpty()) { - warnings.add(new ValidationWarning(9)); - } - } + //DESCRIPTION is required for DISPLAY alarms + if (action.isDisplay()) { + checkRequiredCardinality(warnings, Description.class); + } + + if (action.isEmail()) { + //SUMMARY and DESCRIPTION is required for EMAIL alarms + checkRequiredCardinality(warnings, Summary.class, Description.class); - if (action.isProcedure()) { - checkRequiredCardinality(warnings, Description.class); + //EMAIL alarms must have at least 1 ATTENDEE + if (getAttendees().isEmpty()) { + warnings.add(new ValidationWarning(8)); + } + } else { + //only EMAIL alarms can have ATTENDEEs + if (!getAttendees().isEmpty()) { + warnings.add(new ValidationWarning(9)); } } + if (action.isProcedure()) { + checkRequiredCardinality(warnings, Description.class); + } + } + + private void validateTrigger(List components, List warnings) { Trigger trigger = getTrigger(); - if (trigger != null) { - Related related = trigger.getRelated(); - if (related != null) { - ICalComponent parent = components.get(components.size() - 1); - - //if the TRIGGER is relative to DTSTART, confirm that DTSTART exists - if (related == Related.START && parent.getProperty(DateStart.class) == null) { - warnings.add(new ValidationWarning(11)); - } - - //if the TRIGGER is relative to DTEND, confirm that DTEND (or DUE) exists - if (related == Related.END) { - boolean noEndDate = false; - - if (parent instanceof VEvent) { - noEndDate = (parent.getProperty(DateEnd.class) == null && (parent.getProperty(DateStart.class) == null || parent.getProperty(DurationProperty.class) == null)); - } else if (parent instanceof VTodo) { - noEndDate = (parent.getProperty(DateDue.class) == null && (parent.getProperty(DateStart.class) == null || parent.getProperty(DurationProperty.class) == null)); - } - - if (noEndDate) { - warnings.add(new ValidationWarning(12)); - } - } + if (trigger == null) { + return; + } + + Related related = trigger.getRelated(); + if (related == null) { + return; + } + + ICalComponent parent = components.get(components.size() - 1); + + //if the TRIGGER is relative to DTSTART, confirm that DTSTART exists + if (related == Related.START && parent.getProperty(DateStart.class) == null) { + warnings.add(new ValidationWarning(11)); + } + + //if the TRIGGER is relative to DTEND, confirm that DTEND (or DUE) exists + if (related == Related.END) { + boolean noEndDate = false; + + if (parent instanceof VEvent) { + noEndDate = (parent.getProperty(DateEnd.class) == null && (parent.getProperty(DateStart.class) == null || parent.getProperty(DurationProperty.class) == null)); + } else if (parent instanceof VTodo) { + noEndDate = (parent.getProperty(DateDue.class) == null && (parent.getProperty(DateStart.class) == null || parent.getProperty(DurationProperty.class) == null)); + } + + if (noEndDate) { + warnings.add(new ValidationWarning(12)); } } } diff --git a/src/main/java/biweekly/io/StreamReader.java b/src/main/java/biweekly/io/StreamReader.java index 505b5ab6..4df60689 100644 --- a/src/main/java/biweekly/io/StreamReader.java +++ b/src/main/java/biweekly/io/StreamReader.java @@ -203,15 +203,13 @@ private void handleTimezones(ICalendar ical) { while (it.hasNext()) { VTimezone component = it.next(); - //make sure the component has an ID - String id = ValuedProperty.getValue(component.getTimezoneId()); - if (id == null || id.trim().isEmpty()) { - //note: do not remove invalid VTIMEZONE components from the ICalendar object + TimeZone timezone = buildTimeZone(component); + if (timezone == null) { + //do not remove invalid VTIMEZONE components from the ICalendar object warnings.add(new ParseWarning.Builder().message(39).build()); continue; } - TimeZone timezone = new ICalTimeZone(component); tzinfo.getTimezones().add(new TimezoneAssignment(timezone, component)); //remove the component from the ICalendar object @@ -282,6 +280,13 @@ private void handleTimezones(ICalendar ical) { } } + private TimeZone buildTimeZone(VTimezone component) { + String id = ValuedProperty.getValue(component.getTimezoneId()); + boolean idMissing = (id == null || id.trim().isEmpty()); + + return idMissing ? null : new ICalTimeZone(component); + } + private void reparseDateUnderDifferentTimezone(TimezonedDate timezonedDate, Calendar cal) { ICalDate date = timezonedDate.getDate(); diff --git a/src/main/java/biweekly/io/json/JCalValue.java b/src/main/java/biweekly/io/json/JCalValue.java index 087912a3..9ef39006 100644 --- a/src/main/java/biweekly/io/json/JCalValue.java +++ b/src/main/java/biweekly/io/json/JCalValue.java @@ -228,37 +228,20 @@ public List> asStructured() { List> components = new ArrayList>(array.size()); for (JsonValue value : array) { if (value.isNull()) { - components.add(Collections.emptyList()); + components.add(asStructuredNull()); continue; } Object obj = value.getValue(); if (obj != null) { - String s = obj.toString(); - List component = s.isEmpty() ? Collections.emptyList() : Collections.singletonList(s); - components.add(component); + components.add(asStructuredValue(obj)); continue; } List subArray = value.getArray(); if (subArray != null) { - List component = new ArrayList(subArray.size()); - for (JsonValue subArrayValue : subArray) { - if (subArrayValue.isNull()) { - component.add(""); - continue; - } - - obj = subArrayValue.getValue(); - if (obj != null) { - component.add(obj.toString()); - continue; - } - } - if (component.size() == 1 && component.get(0).isEmpty()) { - component.clear(); - } - components.add(component); + components.add(asStructuredSubArray(subArray)); + continue; } } return components; @@ -269,19 +252,57 @@ public List> asStructured() { Object obj = first.getValue(); if (obj != null) { List> components = new ArrayList>(1); - String s = obj.toString(); - List component = s.isEmpty() ? Collections.emptyList() : Collections.singletonList(s); - components.add(component); + components.add(asStructuredValue(obj)); return components; } //["request-status", {}, "text", null] if (first.isNull()) { List> components = new ArrayList>(1); - components.add(Collections.emptyList()); + components.add(asStructuredNull()); return components; } + /* + * JSON objects are ignored. + */ + + return Collections.emptyList(); + } + + private List asStructuredSubArray(List subArray) { + List component = new ArrayList(subArray.size()); + + for (JsonValue subArrayValue : subArray) { + if (subArrayValue.isNull()) { + component.add(""); + continue; + } + + Object obj = subArrayValue.getValue(); + if (obj != null) { + component.add(obj.toString()); + continue; + } + + /* + * JSON objects and arrays inside of sub-arrays are ignored. + */ + } + + if (component.size() == 1 && component.get(0).isEmpty()) { + component.clear(); + } + + return component; + } + + private List asStructuredValue(Object obj) { + String s = obj.toString(); + return s.isEmpty() ? Collections. emptyList() : Collections.singletonList(s); + } + + private List asStructuredNull() { return Collections.emptyList(); } diff --git a/src/main/java/biweekly/io/json/JsonValue.java b/src/main/java/biweekly/io/json/JsonValue.java index 729f5179..943f791a 100644 --- a/src/main/java/biweekly/io/json/JsonValue.java +++ b/src/main/java/biweekly/io/json/JsonValue.java @@ -71,6 +71,14 @@ public JsonValue(Map object) { isNull = (object == null); } + /** + * Creates a null JSON value. + * @return the JSON value + */ + public static JsonValue nullValue() { + return new JsonValue((Object) null); + } + /** * Gets the JSON value. * @return the value or null if it's not a JSON value diff --git a/src/main/java/biweekly/io/text/ICalWriter.java b/src/main/java/biweekly/io/text/ICalWriter.java index ea075a69..e23915dd 100644 --- a/src/main/java/biweekly/io/text/ICalWriter.java +++ b/src/main/java/biweekly/io/text/ICalWriter.java @@ -240,8 +240,8 @@ private void writeComponent(ICalComponent component, ICalComponent parent) throw writer.writeBeginComponent(componentScribe.getComponentName()); List propertyObjs = componentScribe.getProperties(component); - if (inICalendar && component.getProperty(Version.class) == null) { - propertyObjs.add(0, new Version(getTargetVersion())); + if (inICalendar) { + addVersionIfMissing(component, propertyObjs); } for (Object propertyObj : propertyObjs) { @@ -252,13 +252,7 @@ private void writeComponent(ICalComponent component, ICalComponent parent) throw List subComponents = componentScribe.getComponents(component); if (inICalRoot) { - //add the VTIMEZONE components - Collection timezones = getTimezoneComponents(); - for (VTimezone timezone : timezones) { - if (!subComponents.contains(timezone)) { - subComponents.add(0, timezone); - } - } + addTimezonesIfMissing(subComponents); } for (Object subComponentObj : subComponents) { @@ -267,24 +261,49 @@ private void writeComponent(ICalComponent component, ICalComponent parent) throw } if (inVCalRoot) { - Collection timezones = getTimezoneComponents(); - if (!timezones.isEmpty()) { - VTimezone timezone = timezones.iterator().next(); - VCalTimezoneProperties props = convert(timezone, context.getDates()); - - Timezone tz = props.getTz(); - if (tz != null) { - writeProperty(tz); - } - for (Daylight daylight : props.getDaylights()) { - writeProperty(daylight); - } - } + writeVCalTimezones(); } writer.writeEndComponent(componentScribe.getComponentName()); } + @SuppressWarnings({ "rawtypes", "unchecked" }) + private void addVersionIfMissing(ICalComponent component, List propertyObjs) { + if (component.getProperty(Version.class) != null) { + return; + } + + propertyObjs.add(0, new Version(getTargetVersion())); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private void addTimezonesIfMissing(List subComponents) { + Collection timezones = getTimezoneComponents(); + for (VTimezone timezone : timezones) { + if (!subComponents.contains(timezone)) { + subComponents.add(0, timezone); + } + } + } + + private void writeVCalTimezones() throws IOException { + Collection timezones = getTimezoneComponents(); + if (timezones.isEmpty()) { + return; + } + + VTimezone timezone = timezones.iterator().next(); + VCalTimezoneProperties props = convert(timezone, context.getDates()); + + Timezone tz = props.getTz(); + if (tz != null) { + writeProperty(tz); + } + for (Daylight daylight : props.getDaylights()) { + writeProperty(daylight); + } + } + @SuppressWarnings({ "rawtypes", "unchecked" }) private void writeProperty(ICalProperty property) throws IOException { ICalPropertyScribe scribe = index.getPropertyScribe(property); diff --git a/src/main/java/biweekly/util/DataUri.java b/src/main/java/biweekly/util/DataUri.java index f2237135..d2fef74d 100644 --- a/src/main/java/biweekly/util/DataUri.java +++ b/src/main/java/biweekly/util/DataUri.java @@ -147,24 +147,23 @@ public static DataUri parse(String uri) { throw Messages.INSTANCE.getIllegalArgumentException(23); } - String text = null; - byte[] data = null; if (base64) { dataStr = dataStr.replaceAll("\\s", ""); - data = Base64.decodeBase64(dataStr); - if (charset != null) { - try { - text = new String(data, charset); - } catch (UnsupportedEncodingException e) { - throw new IllegalArgumentException(Messages.INSTANCE.getExceptionMessage(24, charset), e); - } - data = null; + byte[] data = Base64.decodeBase64(dataStr); + + if (charset == null) { + return new DataUri(contentType, data); + } + + try { + String text = new String(data, charset); + return new DataUri(contentType, text); + } catch (UnsupportedEncodingException e) { + throw new IllegalArgumentException(Messages.INSTANCE.getExceptionMessage(24, charset), e); } - } else { - text = dataStr; } - return new DataUri(contentType, data, text); + return new DataUri(contentType, dataStr); } /** diff --git a/src/test/java/biweekly/io/json/JCalValueTest.java b/src/test/java/biweekly/io/json/JCalValueTest.java index 61c5d0d8..ad3b1ed5 100644 --- a/src/test/java/biweekly/io/json/JCalValueTest.java +++ b/src/test/java/biweekly/io/json/JCalValueTest.java @@ -161,8 +161,48 @@ public void structured() { @SuppressWarnings("unchecked") @Test public void asStructured() { - JCalValue value = new JCalValue(new JsonValue(Arrays.asList(new JsonValue("value1"), new JsonValue(false), new JsonValue((Object) null)))); - assertEquals(Arrays.asList(Arrays.asList("value1"), Arrays.asList("false"), Arrays.asList()), value.asStructured()); + //@formatter:off + JCalValue value = new JCalValue( + new JsonValue(Arrays.asList( + new JsonValue("value1"), new JsonValue(false), JsonValue.nullValue() + )) + ); + + assertEquals(Arrays.asList( + Arrays.asList("value1"), + Arrays.asList("false"), + Arrays.asList()), + value.asStructured()); + //@formatter:on + } + + @SuppressWarnings("unchecked") + @Test + public void asStructured_sub_array() { + //@formatter:off + JCalValue value = new JCalValue( + new JsonValue(Arrays.asList( + new JsonValue(Arrays.asList( + new JsonValue("value1"), + new JsonValue("value2"), + JsonValue.nullValue(), + new JsonValue(Arrays.asList(new JsonValue("value3"))) //ignore arrays that are nested this deep + )), + new JsonValue(Arrays.asList( //sub arrays that only contain a single empty element are converted to empty sub-arrays + new JsonValue("") + )), + new JsonValue(Arrays.asList( //sub arrays that only contain a single null element are converted to empty sub-arrays + JsonValue.nullValue() + )) + )) + ); + + assertEquals(Arrays.asList( + Arrays.asList("value1", "value2", ""), + Arrays.asList(), + Arrays.asList()), + value.asStructured()); + //@formatter:on } @SuppressWarnings("unchecked") @@ -174,9 +214,26 @@ public void asStructured_single_value() { @Test public void asStructured_object() { - Map object = new HashMap(); - object.put("a", new JsonValue("one")); - JCalValue value = new JCalValue(new JsonValue(object)); + Map jsonObject = new HashMap(); + jsonObject.put("a", new JsonValue("one")); + + JCalValue value = new JCalValue(new JsonValue(jsonObject)); + assertEquals(Arrays.asList(), value.asStructured()); //JSON objects are ignored + + value = new JCalValue(new JsonValue(Arrays.asList(new JsonValue(jsonObject)))); + assertEquals(Arrays.asList(), value.asStructured()); //JSON objects are ignored + } + + @SuppressWarnings("unchecked") + @Test + public void asStructured_null() { + JCalValue value = new JCalValue(JsonValue.nullValue()); + assertEquals(Arrays.asList(Arrays.asList()), value.asStructured()); + } + + @Test + public void asStructured_empty() { + JCalValue value = new JCalValue(); assertEquals(Arrays.asList(), value.asStructured()); } @@ -192,7 +249,7 @@ public void object() { //@formatter:off Map expectedMap = new HashMap(); expectedMap.put("a", new JsonValue("one")); - expectedMap.put("b", new JsonValue(Arrays.asList(new JsonValue(2), new JsonValue(3.0), new JsonValue((Object)null)))); + expectedMap.put("b", new JsonValue(Arrays.asList(new JsonValue(2), new JsonValue(3.0), JsonValue.nullValue()))); List expected = Arrays.asList( new JsonValue(expectedMap) ); @@ -205,7 +262,7 @@ public void object() { public void asObject() { Map object = new LinkedHashMap(); object.put("a", new JsonValue("one")); - object.put("b", new JsonValue(Arrays.asList(new JsonValue(2), new JsonValue(3.0), new JsonValue((Object) null)))); + object.put("b", new JsonValue(Arrays.asList(new JsonValue(2), new JsonValue(3.0), JsonValue.nullValue()))); object.put("c", new JsonValue((Object) null)); JCalValue value = new JCalValue(new JsonValue(object));