diff --git a/.github/workflows/publish-snapshots.yml b/.github/workflows/publish-snapshots.yml new file mode 100644 index 0000000..eea7b7e --- /dev/null +++ b/.github/workflows/publish-snapshots.yml @@ -0,0 +1,23 @@ +name: Publish snapshots + +on: + push: + branches: + - 'develop' + +jobs: + gradle: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: actions/setup-java@v1 + with: + java-version: 11 + - uses: eskatos/gradle-command-action@v1 + with: + arguments: build -x test publish + env: + GPR_USERNAME: benfortuna + GPR_TOKEN: ${{ secrets.GITHUB_TOKEN }} + MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }} + MAVEN_PASSWORD: ${{ secrets.MAVEN_TOKEN }} diff --git a/README.md b/README.md index ed0bf5c..59a7eda 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ The purpose of this library is to provide custom marshalling between iCal4j obje The following is a non-exhaustive list of known JSON calendar formats: * [jCal](https://tools.ietf.org/html/rfc7265) - The JSON Format for iCalendar -* [jscalendar](https://tools.ietf.org/html/draft-ietf-calext-jscalendar-32) - A JSON representation of calendar data (currently a draft specification) +* [JSCalendar](https://tools.ietf.org/html/draft-ietf-calext-jscalendar-32) - A JSON representation of calendar data (currently a draft specification) ## Implementation @@ -20,4 +20,50 @@ dependencies includes: ## Usage -TBD. +### Serialization + +#### jCal JSON format: + +```java +Calendar calendar = ...; + +SimpleModule module = new SimpleModule(); +module.addSerializer(Calendar.class, new JCalSerializer()); +ObjectMapper mapper = new ObjectMapper(); +mapper.registerModule(module); + +String serialized = mapper.writeValueAsString(calendar); +``` + +#### JSCalendar JSON format: + +```java +Calendar calendar = ...; + +SimpleModule module = new SimpleModule(); +module.addSerializer(Calendar.class, new JSCalendarSerializer()); +ObjectMapper mapper = new ObjectMapper(); +mapper.registerModule(module); + +String serialized = mapper.writeValueAsString(calendar); +``` + +### Deserialization + +```java +String json = ...; + +SimpleModule module = new SimpleModule(); +module.addDeserializer(Calendar.class, new JCalMapper()) +ObjectMapper mapper = new ObjectMapper(); +mapper.registerModule(module); + +Calendar calendar = mapper.readValue(json, Calendar.class); +``` + +## References + +* [RFC5545](https://tools.ietf.org/html/rfc5545) (iCalendar) +* [RFC7265](https://tools.ietf.org/html/rfc7265) (jCal) +* [JSCalendar Draft](https://tools.ietf.org/html/draft-ietf-calext-jscalendar-32) +* [JSCalendar to iCalendar Draft](https://datatracker.ietf.org/doc/html/draft-ietf-calext-jscalendar-icalendar-04) diff --git a/gradle.properties b/gradle.properties index d705083..c43f290 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -ical4jVersion = 4.0.0-alpha8 +ical4jVersion = 3.0.22 jacksonVersion = 2.12.1 groovyVersion = 3.0.7 slf4jVersion = 1.7.30 diff --git a/src/main/java/org/mnode/ical4j/json/JCalMapper.java b/src/main/java/org/mnode/ical4j/json/JCalMapper.java index 9b5f24b..dfcaba5 100644 --- a/src/main/java/org/mnode/ical4j/json/JCalMapper.java +++ b/src/main/java/org/mnode/ical4j/json/JCalMapper.java @@ -7,12 +7,14 @@ import net.fortuna.ical4j.model.*; import net.fortuna.ical4j.model.component.CalendarComponent; import net.fortuna.ical4j.model.component.VEvent; +import net.fortuna.ical4j.model.parameter.Value; import net.fortuna.ical4j.model.property.ProdId; import net.fortuna.ical4j.model.property.Uid; import net.fortuna.ical4j.model.property.Version; import java.io.IOException; import java.net.URISyntaxException; +import java.text.ParseException; import java.util.Arrays; import java.util.List; @@ -39,8 +41,8 @@ public Calendar deserialize(JsonParser p, DeserializationContext ctxt) throws IO assertNextToken(p, JsonToken.START_ARRAY); while (!JsonToken.END_ARRAY.equals(p.nextToken())) { try { - calendar.add(parseProperty(p)); - } catch (URISyntaxException e) { + calendar.getProperties().add(parseProperty(p)); + } catch (URISyntaxException | ParseException e) { throw new IllegalArgumentException(e); } } @@ -48,15 +50,15 @@ public Calendar deserialize(JsonParser p, DeserializationContext ctxt) throws IO assertNextToken(p, JsonToken.START_ARRAY); while (!JsonToken.END_ARRAY.equals(p.nextToken())) { try { - calendar.add((CalendarComponent) parseComponent(p)); - } catch (URISyntaxException e) { + calendar.getComponents().add((CalendarComponent) parseComponent(p)); + } catch (URISyntaxException | ParseException e) { throw new IllegalArgumentException(e); } } return calendar; } - private Component parseComponent(JsonParser p) throws IOException, URISyntaxException { + private Component parseComponent(JsonParser p) throws IOException, URISyntaxException, ParseException { assertCurrentToken(p, JsonToken.START_ARRAY); ComponentBuilder componentBuilder = new ComponentBuilder<>().factories(componentFactories); componentBuilder.name(p.nextTextValue()); @@ -73,7 +75,7 @@ private Component parseComponent(JsonParser p) throws IOException, URISyntaxExce return componentBuilder.build(); } - private Property parseProperty(JsonParser p) throws IOException, URISyntaxException { + private Property parseProperty(JsonParser p) throws IOException, URISyntaxException, ParseException { assertCurrentToken(p, JsonToken.START_ARRAY); PropertyBuilder propertyBuilder = new PropertyBuilder().factories(propertyFactories); propertyBuilder.name(p.nextTextValue()); @@ -88,8 +90,24 @@ private Property parseProperty(JsonParser p) throws IOException, URISyntaxExcept throw new IllegalArgumentException(e); } } + // propertyType - p.nextTextValue(); + String propertyType = p.nextTextValue(); + switch (propertyType) { + case "binary": + propertyBuilder.parameter(Value.BINARY); + case "duration": + propertyBuilder.parameter(Value.DURATION); + case "date": + propertyBuilder.parameter(Value.DATE); + case "date-time": + propertyBuilder.parameter(Value.DATE_TIME); + case "period": + propertyBuilder.parameter(Value.PERIOD); + case "uri": + propertyBuilder.parameter(Value.URI); + } + propertyBuilder.value(p.nextTextValue()); assertNextToken(p, JsonToken.END_ARRAY); diff --git a/src/main/java/org/mnode/ical4j/json/JCalSerializer.java b/src/main/java/org/mnode/ical4j/json/JCalSerializer.java index f9bb767..cbd36fe 100644 --- a/src/main/java/org/mnode/ical4j/json/JCalSerializer.java +++ b/src/main/java/org/mnode/ical4j/json/JCalSerializer.java @@ -7,16 +7,13 @@ import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.ser.std.StdSerializer; -import net.fortuna.ical4j.model.Calendar; -import net.fortuna.ical4j.model.Component; -import net.fortuna.ical4j.model.Parameter; -import net.fortuna.ical4j.model.Property; +import net.fortuna.ical4j.model.*; import net.fortuna.ical4j.model.component.VEvent; import net.fortuna.ical4j.model.component.VTimeZone; import net.fortuna.ical4j.model.component.VToDo; +import net.fortuna.ical4j.model.parameter.Value; import java.io.IOException; -import java.util.List; public class JCalSerializer extends StdSerializer { @@ -36,13 +33,13 @@ private JsonNode buildVCalendar(Calendar calendar) { vcalendar.add("vcalendar"); ArrayNode vcalprops = mapper.createArrayNode(); - for (Property p : calendar.getProperties().getAll()) { + for (Property p : calendar.getProperties()) { vcalprops.add(buildPropertyArray(p)); } vcalendar.add(vcalprops); ArrayNode vcalcomponents = mapper.createArrayNode(); - for (Component c : calendar.getComponents().getAll()) { + for (Component c : calendar.getComponents()) { vcalcomponents.add(buildComponentArray(c)); } vcalendar.add(vcalcomponents); @@ -57,22 +54,22 @@ private JsonNode buildComponentArray(Component component) { cArray.add(component.getName().toLowerCase()); ArrayNode componentprops = mapper.createArrayNode(); - for (Property p : component.getProperties().getAll()) { + for (Property p : component.getProperties()) { componentprops.add(buildPropertyArray(p)); } cArray.add(componentprops); ArrayNode subcomponents = mapper.createArrayNode(); if (component instanceof VEvent) { - for (Component c : ((VEvent) component).getAlarms().getAll()) { + for (Component c : ((VEvent) component).getAlarms()) { subcomponents.add(buildComponentArray(c)); } } else if (component instanceof VToDo) { - for (Component c : ((VToDo) component).getAlarms().getAll()) { + for (Component c : ((VToDo) component).getAlarms()) { subcomponents.add(buildComponentArray(c)); } } else if (component instanceof VTimeZone) { - for (Component c : ((VTimeZone) component).getObservances().getAll()) { + for (Component c : ((VTimeZone) component).getObservances()) { subcomponents.add(buildComponentArray(c)); } } @@ -86,7 +83,7 @@ private JsonNode buildPropertyArray(Property property) { ArrayNode pArray = mapper.createArrayNode(); pArray.add(property.getName().toLowerCase()); - pArray.add(buildParamsObject(property.getParameters().getAll())); + pArray.add(buildParamsObject(property.getParameters())); pArray.add(getPropertyType(property)); pArray.add(property.getValue()); @@ -94,6 +91,12 @@ private JsonNode buildPropertyArray(Property property) { } private String getPropertyType(Property property) { + // handle property type overrides via VALUE param.. + Value value = property.getParameter(Parameter.VALUE); + if (value != null) { + return value.getValue().toLowerCase(); + } + switch (property.getName()) { case "CALSCALE": case "METHOD": @@ -115,9 +118,8 @@ private String getPropertyType(Property property) { case "UID": case "ACTION": case "REQUEST-STATUS": + case "NAME": return "text"; - case "ATTACH": - return "binary"; case "GEO": return "float"; case "PERCENT-COMPLETE": @@ -145,6 +147,9 @@ private String getPropertyType(Property property) { return "utc-offset"; case "TZURL": case "URL": + case "ATTACH": + case "IMAGE": + case "SOURCE": return "uri"; case "ATTENDEE": case "ORGANIZER": @@ -155,12 +160,12 @@ private String getPropertyType(Property property) { throw new IllegalArgumentException("Unknown property type"); } - private JsonNode buildParamsObject(List parameterList) { + private JsonNode buildParamsObject(ParameterList parameterList) { ObjectMapper mapper = new ObjectMapper(); ObjectNode params = mapper.createObjectNode(); for (Parameter p : parameterList) { - params.put(p.getName().toLowerCase(), p.getValue()); + params.put(p.getName().toLowerCase(), p.getValue().toLowerCase()); } return params; } diff --git a/src/main/java/org/mnode/ical4j/json/JSCalendarSerializer.java b/src/main/java/org/mnode/ical4j/json/JSCalendarSerializer.java index 3215e69..d5cb9b8 100644 --- a/src/main/java/org/mnode/ical4j/json/JSCalendarSerializer.java +++ b/src/main/java/org/mnode/ical4j/json/JSCalendarSerializer.java @@ -20,7 +20,7 @@ public JSCalendarSerializer(Class t) { @Override public void serialize(Calendar value, JsonGenerator gen, SerializerProvider provider) throws IOException { // For calendar objects with a UID we assume a JSGroup is used to represent in jscalendar.. - if (value.getProperties().getFirst(Property.UID).isPresent()) { + if (value.getProperty(Property.UID) != null) { try { gen.writeTree(buildJSGroup(value)); } catch (ConstraintViolationException e) { @@ -28,7 +28,7 @@ public void serialize(Calendar value, JsonGenerator gen, SerializerProvider prov } } // For calendar objects with one or more VEVENTs assume a JSEvent representation.. - else if (value.getComponents().getFirst(Component.VEVENT).isPresent()) { + else if (value.getComponent(Component.VEVENT) != null) { try { gen.writeTree(buildJSEvent(value)); } catch (ConstraintViolationException e) { @@ -36,7 +36,7 @@ else if (value.getComponents().getFirst(Component.VEVENT).isPresent()) { } } // For calendar objects with one or more VTODOs assume a JSTask representation.. - else if (value.getComponents().getFirst(Component.VTODO).isPresent()) { + else if (value.getComponent(Component.VTODO) != null) { try { gen.writeTree(buildJSTask(value)); } catch (ConstraintViolationException e) { @@ -49,20 +49,20 @@ else if (value.getComponents().getFirst(Component.VTODO).isPresent()) { private JsonNode buildJSGroup(Calendar calendar) throws ConstraintViolationException { JSGroupBuilder builder = new JSGroupBuilder() - .uid(calendar.getProperties().getRequired(Property.UID).getValue()); + .uid(calendar.getProperty(Property.UID).getValue()); return builder.build(); } private JsonNode buildJSEvent(Calendar calendar) throws ConstraintViolationException { JSEventBuilder builder = new JSEventBuilder() - .uid(calendar.getComponents().getRequired(Component.VEVENT) - .getProperties().getRequired(Property.UID).getValue()); + .uid(calendar.getComponent(Component.VEVENT) + .getProperty(Property.UID).getValue()); return builder.build(); } private JsonNode buildJSTask(Calendar calendar) throws ConstraintViolationException { JSTaskBuilder builder = new JSTaskBuilder() - .uid(calendar.getProperties().getRequired(Property.UID).getValue()); + .uid(calendar.getProperty(Property.UID).getValue()); return builder.build(); } } diff --git a/src/main/java/org/mnode/ical4j/json/LocationBuilder.java b/src/main/java/org/mnode/ical4j/json/LocationBuilder.java index e0c5dc1..94ae56e 100644 --- a/src/main/java/org/mnode/ical4j/json/LocationBuilder.java +++ b/src/main/java/org/mnode/ical4j/json/LocationBuilder.java @@ -1,5 +1,7 @@ package org.mnode.ical4j.json; +import net.fortuna.ical4j.model.LocationType; + import java.net.URL; import java.time.ZoneId; import java.util.Map; diff --git a/src/main/java/org/mnode/ical4j/json/LocationType.java b/src/main/java/org/mnode/ical4j/json/LocationType.java deleted file mode 100644 index 22937fb..0000000 --- a/src/main/java/org/mnode/ical4j/json/LocationType.java +++ /dev/null @@ -1,29 +0,0 @@ -package org.mnode.ical4j.json; - -/** - * Location types as defined by the Location Types Registry (RFC4589): https://tools.ietf.org/html/rfc4589 - */ -public enum LocationType { - aircraft, airport, arena, automobile, bank, bar, bicycle, bus, bus_station, cafe, classroom, - club, construction, convention_center, government, hospital, hotel, industrial, library, - motorcycle, office, other, outdoors, parking, place_of_worship, prison, public_, public_transport, - residence, restaurant, school, shopping_area, stadium, store, street, theater, train, train_station, - truck, underway, unknown, warehouse, water, watercraft; - - public static LocationType from(String locationTypeString) { - if ("public".equals(locationTypeString)) { - return Enum.valueOf(LocationType.class, "public_"); - } else { - return Enum.valueOf(LocationType.class, locationTypeString.replace("-", "_")); - } - } - - @Override - public String toString() { - if (this == public_) { - return "public"; - } else { - return super.toString().replace("_", "-"); - } - } -} diff --git a/src/test/groovy/org/mnode/ical4j/json/JCalMapperTest.groovy b/src/test/groovy/org/mnode/ical4j/json/JCalMapperTest.groovy index 48d658b..adc348d 100644 --- a/src/test/groovy/org/mnode/ical4j/json/JCalMapperTest.groovy +++ b/src/test/groovy/org/mnode/ical4j/json/JCalMapperTest.groovy @@ -9,7 +9,7 @@ class JCalMapperTest extends Specification { def 'test calendar deserialization'() { given: 'a json string' - String json = '["vcalendar",[["prodid",{},"string","-//Ben Fortuna//iCal4j 1.0//EN"],["version",{},"string","2.0"],["uid",{},"string","123"]],[]]' + String json = '["vcalendar",[["prodid",{},"string","-//Ben Fortuna//iCal4j 1.0//EN"],["version",{},"string","2.0"],["uid",{},"string","123"],["source",{},"uri","https://www.abc.net.au/news/feed/51120/rss.xml"]],[]]' and: 'an object mapper' SimpleModule module = [] @@ -21,6 +21,6 @@ class JCalMapperTest extends Specification { Calendar calendar = mapper.readValue(json, Calendar) then: 'calendar matches expected result' - calendar as String == 'BEGIN:VCALENDAR\r\nPRODID:-//Ben Fortuna//iCal4j 1.0//EN\r\nVERSION:2.0\r\nUID:123\r\nEND:VCALENDAR\r\n' + calendar as String == 'BEGIN:VCALENDAR\r\nPRODID:-//Ben Fortuna//iCal4j 1.0//EN\r\nVERSION:2.0\r\nUID:123\r\nSOURCE;VALUE=URI:https://www.abc.net.au/news/feed/51120/rss.xml\r\nEND:VCALENDAR\r\n' } } diff --git a/src/test/groovy/org/mnode/ical4j/json/JCalSerializerTest.groovy b/src/test/groovy/org/mnode/ical4j/json/JCalSerializerTest.groovy index 164019d..4d86f8c 100644 --- a/src/test/groovy/org/mnode/ical4j/json/JCalSerializerTest.groovy +++ b/src/test/groovy/org/mnode/ical4j/json/JCalSerializerTest.groovy @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.module.SimpleModule import net.fortuna.ical4j.model.Calendar import net.fortuna.ical4j.model.ContentBuilder +import net.fortuna.ical4j.util.Calendars import spock.lang.Specification class JCalSerializerTest extends Specification { @@ -35,6 +36,28 @@ class JCalSerializerTest extends Specification { String serialized = mapper.writeValueAsString(calendar) then: 'serialized string matches expected value' - serialized == '["vcalendar",[["prodid",{},"text","-//Ben Fortuna//iCal4j 1.0//EN"],["version",{},"text","2.0"],["uid",{},"text","123"]],[["vevent",[["uid",{},"text","1"],["dtstart",{"value":"DATE"},"date-time","20090810"]],[]],["vevent",[["uid",{},"text","2"],["dtstart",{"value":"DATE"},"date-time","20100810"]],[]]]]' + serialized == '["vcalendar",[["prodid",{},"text","-//Ben Fortuna//iCal4j 1.0//EN"],["version",{},"text","2.0"],["uid",{},"text","123"]],[["vevent",[["uid",{},"text","1"],["dtstart",{"value":"date"},"date","20090810"]],[]],["vevent",[["uid",{},"text","2"],["dtstart",{"value":"date"},"date","20100810"]],[]]]]' + } + + def 'test calendar serialization 2'() { + given: 'a calendar' + Calendar calendar = Calendars.load(file) + + and: 'an object mapper' + SimpleModule module = [] + module.addSerializer(Calendar, new JCalSerializer()) + ObjectMapper mapper = [] + mapper.registerModule(module) + + when: 'the calendar is serialized' + String serialized = mapper.writeValueAsString(calendar) + + then: 'serialized string matches expected value' + serialized == '["vcalendar",[["version",{},"text","2.0"],["prodid",{},"text","-//ABC Corporation//NONSGML My Product//EN"],["uid",{},"text","1"],["name",{},"text","Just In"],["description",{},"text",""],["source",{},"uri","https://www.abc.net.au/news/feed/51120/rss.xml"],["url",{},"uri","https://www.abc.net.au/news/justin/"],["image",{},"uri","https://www.abc.net.au/news/image/8413416-1x1-144x144.png"],["last-modified",{},"date-time","20210304T055223Z"]],[["vjournal",[["uid",{},"text","https://www.abc.net.au/news/2021-03-04/gold-coast-needs-6,500-new-homes-a-year-housing-crisis/13214856"],["summary",{},"text","The Gold Coast needs 6,500 new homes a year, but where can they be built?"],["description",{},"text","\\n \\n

The famous coastal city is fast running out of greenfield land to house its growing population, but the community is opposed to higher-density developments in the city.

\\n \\n"],["categories",{},"text","Housing Industry,Rental Housing,Housing,Agribusiness"],["dtstamp",{},"date-time","20210304T055223Z"],["url",{},"uri","https://www.abc.net.au/news/2021-03-04/gold-coast-needs-6,500-new-homes-a-year-housing-crisis/13214856"],["contact",{},"text","Dominic Cansdale"],["image",{},"uri","https://www.abc.net.au/news/image/12721466-3x2-940x627.jpg"]],[]]]]' + + where: + file << [ + 'src/test/resources/samples/justin.ics' + ] } } diff --git a/src/test/groovy/org/mnode/ical4j/json/LocationTypeTest.groovy b/src/test/groovy/org/mnode/ical4j/json/LocationTypeTest.groovy deleted file mode 100644 index a7ac305..0000000 --- a/src/test/groovy/org/mnode/ical4j/json/LocationTypeTest.groovy +++ /dev/null @@ -1,26 +0,0 @@ -package org.mnode.ical4j.json - -import spock.lang.Specification - -class LocationTypeTest extends Specification { - - def 'assert string parsing'() { - expect: - LocationType.from(locationTypeString) == expectedType - - where: - locationTypeString | expectedType - "public" | LocationType.public_ - "bus-station" | LocationType.bus_station - } - - def 'assert string formatting'() { - expect: - locationType as String == expectedString - - where: - locationType | expectedString - LocationType.public_ | "public" - LocationType.bus_station | "bus-station" - } -} diff --git a/src/test/resources/log4j.properties b/src/test/resources/log4j.properties new file mode 100644 index 0000000..5d07323 --- /dev/null +++ b/src/test/resources/log4j.properties @@ -0,0 +1,40 @@ +# +# Copyright (c) 2012, Ben Fortuna +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# o Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# o Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# o Neither the name of Ben Fortuna nor the names of any other contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# + +## set root logging preferences.. +log4j.rootLogger=warn, stdout +log4j.logger.org.mnode.ical4j.integration=info, stdout + +## appender: stdout.. +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=[%d] %-5p [%t] %c{1} - %m%n \ No newline at end of file diff --git a/src/test/resources/samples/justin.ics b/src/test/resources/samples/justin.ics new file mode 100644 index 0000000..70e0d01 --- /dev/null +++ b/src/test/resources/samples/justin.ics @@ -0,0 +1,21 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//ABC Corporation//NONSGML My Product//EN +UID:1 +NAME:Just In +DESCRIPTION: +SOURCE:https://www.abc.net.au/news/feed/51120/rss.xml +URL:https://www.abc.net.au/news/justin/ +IMAGE:https://www.abc.net.au/news/image/8413416-1x1-144x144.png +LAST-MODIFIED:20210304T055223Z +BEGIN:VJOURNAL +UID:https://www.abc.net.au/news/2021-03-04/gold-coast-needs-6\,500-new-homes-a-year-housing-crisis/13214856 +SUMMARY:The Gold Coast needs 6\,500 new homes a year\, but where can they be built? +DESCRIPTION:\n \n

The famous coastal city is fast running out of greenfield land to house its growing population\, but the community is opposed to higher-density developments in the city.

\n \n +CATEGORIES:Housing Industry,Rental Housing,Housing,Agribusiness +DTSTAMP:20210304T055223Z +URL:https://www.abc.net.au/news/2021-03-04/gold-coast-needs-6,500-new-homes-a-year-housing-crisis/13214856 +CONTACT:Dominic Cansdale +IMAGE:https://www.abc.net.au/news/image/12721466-3x2-940x627.jpg +END:VJOURNAL +END:VCALENDAR