Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prepare release #9

Merged
merged 8 commits into from
Nov 11, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Added xCal serializer
  • Loading branch information
benfortuna committed Nov 10, 2021
commit b667d4af026bf8b38ea2d58520e2434c924ecb1b
181 changes: 181 additions & 0 deletions src/main/java/org/mnode/ical4j/serializer/XCalSerializer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
package org.mnode.ical4j.serializer;

import com.fasterxml.jackson.annotation.JsonRootName;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
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;

@JsonRootName(value = "icalendar")
public class XCalSerializer extends StdSerializer<Calendar> {

private ObjectMapper objectMapper;

public XCalSerializer(Class<Calendar> t) {
super(t);
this.objectMapper = new XmlMapper(); //.builder().enable(SerializationFeature.WRAP_ROOT_VALUE)
}

@Override
public void serialize(Calendar value, JsonGenerator gen, SerializerProvider provider) throws IOException {
gen.writeTree(buildVCalendar(value));
}

private JsonNode buildVCalendar(Calendar calendar) {
ObjectNode icalendar = objectMapper.createObjectNode();
ObjectNode vcalendar = icalendar.putObject("vcalendar");

ObjectNode vcalprops = vcalendar.putObject("properties");
for (Property p : calendar.getProperties()) {
vcalprops.putIfAbsent(p.getName().toLowerCase(), buildPropertyNode(p));
}

ObjectNode vcalcomponents = vcalendar.putObject("components");
for (Component c : calendar.getComponents()) {
vcalcomponents.putIfAbsent(c.getName().toLowerCase(), buildComponentArray(c));
}
return icalendar;
}

private JsonNode buildComponentArray(Component component) {
ObjectNode cArray = objectMapper.createObjectNode();

ObjectNode componentprops = cArray.putObject("properties");
for (Property p : component.getProperties()) {
componentprops.putIfAbsent(p.getName().toLowerCase(), buildPropertyNode(p));
}

ObjectNode subcomponents = cArray.putObject("components");
if (component instanceof VEvent) {
for (Component c : ((VEvent) component).getAlarms()) {
subcomponents.putIfAbsent(c.getName().toLowerCase(), buildComponentArray(c));
}
} else if (component instanceof VToDo) {
for (Component c : ((VToDo) component).getAlarms()) {
subcomponents.putIfAbsent(c.getName().toLowerCase(), buildComponentArray(c));
}
} else if (component instanceof VTimeZone) {
for (Component c : ((VTimeZone) component).getObservances()) {
subcomponents.putIfAbsent(c.getName().toLowerCase(), buildComponentArray(c));
}
}
return cArray;
}

private JsonNode buildPropertyNode(Property property) {
ObjectNode pArray = objectMapper.createObjectNode();
pArray.putIfAbsent("parameters", buildParamsObject(property.getParameters()));

String propertyType = getPropertyType(property);
switch (propertyType) {
case "date":
pArray.put(propertyType, JCalEncoder.DATE.encode(property.getValue()));
break;
case "date-time":
pArray.put(propertyType, JCalEncoder.DATE_TIME.encode(property.getValue()));
break;
case "time":
pArray.put(propertyType, JCalEncoder.TIME.encode(property.getValue()));
break;
case "utc-offset":
pArray.put(propertyType, JCalEncoder.UTCOFFSET.encode(property.getValue()));
break;
case "binary":
case "duration":
case "period":
case "uri":
default:
pArray.put(propertyType, property.getValue());
}
return pArray;
}

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":
case "PRODID":
case "VERSION":
case "CATEGORIES":
case "CLASS":
case "COMMENT":
case "DESCRIPTION":
case "LOCATION":
case "RESOURCES":
case "STATUS":
case "SUMMARY":
case "TRANSP":
case "TZID":
case "TZNAME":
case "CONTACT":
case "RELATED-TO":
case "UID":
case "ACTION":
case "REQUEST-STATUS":
case "NAME":
return "text";
case "GEO":
return "float";
case "PERCENT-COMPLETE":
case "PRIORITY":
case "REPEAT":
case "SEQUENCE":
return "integer";
case "COMPLETED":
case "DTEND":
case "DUE":
case "DTSTAMP":
case "DTSTART":
case "DURATION":
case "RECURRENCE-ID":
case "EXDATE":
case "RDATE":
case "TRIGGER":
case "CREATED":
case "LAST-MODIFIED":
return "date-time";
case "FREEBUSY":
return "period";
case "TZOFFSETFROM":
case "TZOFFSETTO":
return "utc-offset";
case "TZURL":
case "URL":
case "ATTACH":
case "IMAGE":
case "SOURCE":
return "uri";
case "ATTENDEE":
case "ORGANIZER":
return "cal-address";
case "RRULE":
return "recur";
default:
return "unknown";
}
}

private JsonNode buildParamsObject(ParameterList parameterList) {
ObjectNode params = objectMapper.createObjectNode();
for (Parameter p : parameterList) {
params.put(p.getName().toLowerCase(), p.getValue().toLowerCase());
}
return params;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package org.mnode.ical4j.serializer


import com.fasterxml.jackson.databind.MapperFeature
import com.fasterxml.jackson.databind.PropertyName
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.dataformat.xml.XmlMapper
import net.fortuna.ical4j.model.Calendar

class XCalSerializerTest extends AbstractSerializerTest {

def 'test calendar XML serialization'() {
given: 'an object mapper'
SimpleModule module = []
module.addSerializer(Calendar, new XCalSerializer())
XmlMapper mapper = XmlMapper.builder().defaultUseWrapper(true).build()
mapper.setConfig(mapper.getSerializationConfig().withRootName(
PropertyName.construct("icalendar", "urn:ietf:params:xml:ns:icalendar-2.0"))
.with(MapperFeature.USE_WRAPPER_NAME_AS_PROPERTY_NAME))
mapper.registerModule(module)

when: 'the calendar is serialized'
String serialized = mapper.writeValueAsString(calendar)

then: 'serialized string matches expected value'
serialized == expectedSerialized

where:
calendar | expectedSerialized
calendar1 | '<icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0"><vcalendar><properties><prodid><parameters/><text>-//Ben Fortuna//iCal4j 3.1//EN</text></prodid><version><parameters/><text>2.0</text></version><uid><parameters/><text>123</text></uid></properties><components><vevent><properties><uid><parameters/><text>1</text></uid><summary><parameters/><text>Test Event 1</text></summary><organizer><parameters/><cal-address>[email protected]</cal-address></organizer><dtstart><parameters><value>date</value></parameters><date>2009-08-10</date></dtstart><action><parameters/><text>DISPLAY</text></action><attach><parameters><encoding>base64</encoding><value>binary</value></parameters><binary>QlNEIDMtQ2xhdXNlIExpY2Vuc2UKCkNvcHlyaWdodCAoYykgMjAyMSwgaUNhbDRqCkFsbCByaWdodHMgcmVzZXJ2ZWQuCgpSZWRpc3RyaWJ1dGlvbiBhbmQgdXNlIGluIHNvdXJjZSBhbmQgYmluYXJ5IGZvcm1zLCB3aXRoIG9yIHdpdGhvdXQKbW9kaWZpY2F0aW9uLCBhcmUgcGVybWl0dGVkIHByb3ZpZGVkIHRoYXQgdGhlIGZvbGxvd2luZyBjb25kaXRpb25zIGFyZSBtZXQ6CgoxLiBSZWRpc3RyaWJ1dGlvbnMgb2Ygc291cmNlIGNvZGUgbXVzdCByZXRhaW4gdGhlIGFib3ZlIGNvcHlyaWdodCBub3RpY2UsIHRoaXMKICAgbGlzdCBvZiBjb25kaXRpb25zIGFuZCB0aGUgZm9sbG93aW5nIGRpc2NsYWltZXIuCgoyLiBSZWRpc3RyaWJ1dGlvbnMgaW4gYmluYXJ5IGZvcm0gbXVzdCByZXByb2R1Y2UgdGhlIGFib3ZlIGNvcHlyaWdodCBub3RpY2UsCiAgIHRoaXMgbGlzdCBvZiBjb25kaXRpb25zIGFuZCB0aGUgZm9sbG93aW5nIGRpc2NsYWltZXIgaW4gdGhlIGRvY3VtZW50YXRpb24KICAgYW5kL29yIG90aGVyIG1hdGVyaWFscyBwcm92aWRlZCB3aXRoIHRoZSBkaXN0cmlidXRpb24uCgozLiBOZWl0aGVyIHRoZSBuYW1lIG9mIHRoZSBjb3B5cmlnaHQgaG9sZGVyIG5vciB0aGUgbmFtZXMgb2YgaXRzCiAgIGNvbnRyaWJ1dG9ycyBtYXkgYmUgdXNlZCB0byBlbmRvcnNlIG9yIHByb21vdGUgcHJvZHVjdHMgZGVyaXZlZCBmcm9tCiAgIHRoaXMgc29mdHdhcmUgd2l0aG91dCBzcGVjaWZpYyBwcmlvciB3cml0dGVuIHBlcm1pc3Npb24uCgpUSElTIFNPRlRXQVJFIElTIFBST1ZJREVEIEJZIFRIRSBDT1BZUklHSFQgSE9MREVSUyBBTkQgQ09OVFJJQlVUT1JTICJBUyBJUyIKQU5EIEFOWSBFWFBSRVNTIE9SIElNUExJRUQgV0FSUkFOVElFUywgSU5DTFVESU5HLCBCVVQgTk9UIExJTUlURUQgVE8sIFRIRQpJTVBMSUVEIFdBUlJBTlRJRVMgT0YgTUVSQ0hBTlRBQklMSVRZIEFORCBGSVRORVNTIEZPUiBBIFBBUlRJQ1VMQVIgUFVSUE9TRSBBUkUKRElTQ0xBSU1FRC4gSU4gTk8gRVZFTlQgU0hBTEwgVEhFIENPUFlSSUdIVCBIT0xERVIgT1IgQ09OVFJJQlVUT1JTIEJFIExJQUJMRQpGT1IgQU5ZIERJUkVDVCwgSU5ESVJFQ1QsIElOQ0lERU5UQUwsIFNQRUNJQUwsIEVYRU1QTEFSWSwgT1IgQ09OU0VRVUVOVElBTApEQU1BR0VTIChJTkNMVURJTkcsIEJVVCBOT1QgTElNSVRFRCBUTywgUFJPQ1VSRU1FTlQgT0YgU1VCU1RJVFVURSBHT09EUyBPUgpTRVJWSUNFUzsgTE9TUyBPRiBVU0UsIERBVEEsIE9SIFBST0ZJVFM7IE9SIEJVU0lORVNTIElOVEVSUlVQVElPTikgSE9XRVZFUgpDQVVTRUQgQU5EIE9OIEFOWSBUSEVPUlkgT0YgTElBQklMSVRZLCBXSEVUSEVSIElOIENPTlRSQUNULCBTVFJJQ1QgTElBQklMSVRZLApPUiBUT1JUIChJTkNMVURJTkcgTkVHTElHRU5DRSBPUiBPVEhFUldJU0UpIEFSSVNJTkcgSU4gQU5ZIFdBWSBPVVQgT0YgVEhFIFVTRQpPRiBUSElTIFNPRlRXQVJFLCBFVkVOIElGIEFEVklTRUQgT0YgVEhFIFBPU1NJQklMSVRZIE9GIFNVQ0ggREFNQUdFLgo=</binary></attach></properties><components/></vevent></components></vcalendar></icalendar>'
calendar2 | '''<icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0"><vcalendar><properties><version><parameters/><text>2.0</text></version><prodid><parameters/><text>-//ABC Corporation//NONSGML My Product//EN</text></prodid><uid><parameters/><text>1</text></uid><name><parameters/><text>Just In</text></name><description><parameters/><text></text></description><source><parameters/><uri>https://www.abc.net.au/news/feed/51120/rss.xml</uri></source><url><parameters/><uri>https://www.abc.net.au/news/justin/</uri></url><image><parameters/><uri>https://www.abc.net.au/news/image/8413416-1x1-144x144.png</uri></image><last-modified><parameters/><date-time>2021-03-04T05:52:23Z</date-time></last-modified></properties><components><vjournal><properties><uid><parameters/><text>https://www.abc.net.au/news/2021-03-04/gold-coast-needs-6,500-new-homes-a-year-housing-crisis/13214856</text></uid><summary><parameters/><text>The Gold Coast needs 6,500 new homes a year, but where can they be built?</text></summary><styled-description><parameters><value>text</value></parameters><text>

&lt;p>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.&lt;/p>

</text></styled-description><categories><parameters/><text>Housing Industry,Rental Housing,Housing,Agribusiness</text></categories><dtstamp><parameters/><date-time>2021-03-04T05:52:23Z</date-time></dtstamp><url><parameters/><uri>https://www.abc.net.au/news/2021-03-04/gold-coast-needs-6,500-new-homes-a-year-housing-crisis/13214856</uri></url><contact><parameters/><text>Dominic Cansdale</text></contact><image><parameters/><uri>https://www.abc.net.au/news/image/12721466-3x2-940x627.jpg</uri></image></properties><components/></vjournal></components></vcalendar></icalendar>'''
}
}