package services.export;
import helpers.JsonLdConstants;
import models.Record;
import models.Resource;
import models.ResourceList;
import org.apache.commons.lang3.StringUtils;
import org.elasticsearch.common.Strings;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.joda.time.format.ISODateTimeFormat;
import java.time.Instant;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Created by fo and pvb
*/
public class CalendarExporter implements Exporter {
// The following iCalendar fields are currently not in use:
// - SEQUENCE
// - STATUS
// - TRANSP
// - RRULE
// - CATEGORIES
private static final String HEADER = //
"BEGIN:VCALENDAR\n" + //
"VERSION:2.0\n" + //
"PRODID:https://oerworldmap.org/\n" + //
"CALSCALE:GREGORIAN\n" + //
"METHOD:PUBLISH\n";
private static final String FOOTER = "END:VCALENDAR\n";
private static final String KEY_SEPARATOR = ":";
private static final String VALUE_SEPARATOR = ";";
private static final String EVENT_BEGIN = "BEGIN:VEVENT\n";
private static final String EVENT_END = "END:VEVENT\n";
private static final String UID = "UID:";
private static final String ORGANIZER = "ORGANIZER";
private static final String COMMON_NAME = ";CN="; // in line ORGANIZER
private static final String MAILTO = "MAILTO:"; // in line ORGANIZER
private static final String LOCATION = "LOCATION:";
private static final String SUMMARY = "SUMMARY:";
private static final String DESCRIPTION = "DESCRIPTION:";
private static final String CAL_CLASS = "CLASS:PUBLIC\n"; // no non-public events up to now
private static final String GEO = "GEO:";
private static final String URL = "URL:";
private static final String DATE_START = "DTSTART:";
private static final String DATE_END = "DTEND:";
private static final String DATE_STAMP = "DTSTAMP:";
private static final String DEFAULT_TIME_START = "T000000";
private static final String DEFAULT_TIME_END = "T235959";
private static final String SIMPLE_DATE_REGEX = "^([\\d]{4})-([\\d]{2})-([\\d]{2})$";
private static final Pattern mSimpleDatePattern = Pattern.compile(SIMPLE_DATE_REGEX);
private static final Map<String, String> mFieldMap = new HashMap<>();
static{
mFieldMap.put(UID, JsonLdConstants.ID);
mFieldMap.put(SUMMARY, "name.@value");
mFieldMap.put(URL, "url");
mFieldMap.put(LOCATION, "location.address.streetAddress".concat(VALUE_SEPARATOR)
.concat("location.address.postalCode").concat(VALUE_SEPARATOR)
.concat("location.address.addressLocality").concat(VALUE_SEPARATOR)
.concat("location.address.addressRegion").concat(VALUE_SEPARATOR)
.concat("location.address.addressCountry").concat(VALUE_SEPARATOR));
mFieldMap.put(GEO, "location.geo.lat".concat(VALUE_SEPARATOR).concat("location.geo.lon"));
}
private final Locale mPreferredLocale;
public CalendarExporter(Locale aPreferredLocale){
mPreferredLocale = aPreferredLocale;
}
@Override
public String export(Resource aResource) {
if (!aResource.getAsResource(Record.RESOURCE_KEY).getType().equals("Event")) {
return null;
}
return HEADER.concat(exportResourceWithoutHeader(aResource.getAsResource(Record.RESOURCE_KEY))).concat(FOOTER);
}
@Override
public String export(ResourceList aResourceList) {
StringBuilder result = new StringBuilder(HEADER);
aResourceList.getItems().stream().filter(resource -> resource.getAsResource(Record.RESOURCE_KEY).getType()
.equals("Event")).forEach(resource ->
result.append(exportResourceWithoutHeader(resource.getAsResource(Record.RESOURCE_KEY)))
);
result.append(FOOTER);
return result.toString();
}
private String exportResourceWithoutHeader(final Resource aResource){
final String startDate = parseStartDate(aResource);
final String dateStamp = getTimeStamp();
// Check required fields according to https://tools.ietf.org/html/rfc5545#section-3.6.1
if (StringUtils.isEmpty(startDate) || StringUtils.isEmpty(dateStamp)){
// Missing UID is not checked here because it can never be null for an existing resource.
return "";
}
final StringBuilder result = new StringBuilder(EVENT_BEGIN);
for (Map.Entry<String, String> mapping : mFieldMap.entrySet()){
boolean hasAppendedSomething = false;
String[] mappingValues = mapping.getValue().split(VALUE_SEPARATOR);
StringBuilder subResult = new StringBuilder();
for (String mappingValue : mappingValues) {
final String value = aResource.getNestedFieldValue(mappingValue, mPreferredLocale);
if (value != null && !Strings.isEmpty(value)) {
if (subResult.length() == 0) {
subResult.append(mapping.getKey());
} //
else {
if (subResult.length() > mapping.getKey().length()) {
subResult.append(VALUE_SEPARATOR);
}
}
subResult.append(value);
hasAppendedSomething = true;
}
}
result.append(subResult);
if (hasAppendedSomething){
result.append("\n");
}
}
result.append(getDescription(aResource));
result.append(getExportedOrganizer(aResource));
result.append(startDate);
result.append(parseEndDate(aResource));
result.append(dateStamp);
result.append(CAL_CLASS);
completeFields(result, aResource);
result.append(EVENT_END);
return result.toString();
}
private void completeFields(final StringBuilder aStringBuilder, final Resource aResource){
if (aResource.getNestedFieldValue("name.@value", mPreferredLocale) == null){
// no summary could be added --> append empty summary to comply to ical specification
aStringBuilder.append("SUMMARY:\n");
}
}
private String getExportedOrganizer(Resource aResource){
StringBuilder result = new StringBuilder();
List<Resource> organizers = aResource.getAsList("organizer");
result.append(ORGANIZER);
boolean hasOrganizer = false;
if (organizers != null && !organizers.isEmpty()){
for (Resource organizer : organizers) {
String name = organizer.getNestedFieldValue("name.@value", mPreferredLocale);
String email = organizer.getAsString("email");
boolean hasName = false;
if (name != null) {
if (email == null){
result.append(":").append(name);
}
else {
result.append(COMMON_NAME).append("\"").append(name).append("\"");
}
hasOrganizer = true;
hasName = true;
}
if (email == null) {
email = aResource.getAsString("email");
}
if (email != null) {
if (hasName) {
result.append(KEY_SEPARATOR);
}
result.append(MAILTO).append(email);
}
if (hasName){
break;
}
}
result.append("\n");
}
if (!hasOrganizer){
result.append(":\n");
}
return result.toString();
}
private String parseStartDate(final Resource aResource) {
final String originalStartDate = aResource.getAsString("startDate");
StringBuilder result = new StringBuilder();
if (originalStartDate == null || Strings.isEmpty(originalStartDate)){
return "";
}
Matcher matcher = mSimpleDatePattern.matcher(originalStartDate);
if (matcher.find()) {
result.append(DATE_START)
.append(formatSimpleDate(matcher, DEFAULT_TIME_START));
}
else {
final DateTime dateTime = parseISO8601toUTC(originalStartDate);
if (dateTime == null) {
return "";
}
result.append(dateTimeToIcalDate(DATE_START, dateTime));
}
return result.append("\n").toString();
}
private static String parseEndDate(final Resource aResource) {
final String originalEndDate = aResource.getAsString("endDate");
StringBuilder result = new StringBuilder();
if (originalEndDate == null || Strings.isEmpty(originalEndDate)){
return "";
}
Matcher matcher = mSimpleDatePattern.matcher(originalEndDate);
if (matcher.find()) {
result.append(DATE_END)
.append(formatSimpleDate(matcher, DEFAULT_TIME_END));
}
else {
final DateTime dateTime = parseISO8601toUTC(originalEndDate);
if (dateTime == null) {
return "";
}
result.append(dateTimeToIcalDate(DATE_END, dateTime));
}
return result.append("\n").toString();
}
private static String formatSimpleDate(final Matcher aMatcher, final String aTime){
return aMatcher.group(1).concat(aMatcher.group(2)).concat(aMatcher.group(3)).concat(aTime);
}
private static StringBuilder dateTimeToIcalDate(final String aField, final DateTime aDateTime) {
final StringBuilder result = new StringBuilder(aField);
result.append(String.format("%04d", aDateTime.getYear()))
.append(String.format("%02d", aDateTime.getMonthOfYear()))
.append(String.format("%02d", aDateTime.getDayOfMonth()))
.append("T")
.append(String.format("%02d", aDateTime.getHourOfDay()))
.append(String.format("%02d", aDateTime.getMinuteOfHour()))
.append(String.format("%02d", aDateTime.getSecondOfMinute()));
return result;
}
/**
* found on: http://www.javased.com/?api=org.joda.time.format.ISODateTimeFormat
*
* From project Carolina-Digital-Repository, under directory /metadata/src/main/java/edu/unc/lib/dl/util/
*
* Parse a date in any ISO 8601 format. Default TZ is based on Locale.
* @param isoDate ISO8601 date/time string with or without TZ offset
* @return a Joda DateTime object in UTC (call toString() to print)
*/
private static DateTime parseISO8601toUTC(String isoDate){
DateTime result;
DateTimeFormatter fmt= ISODateTimeFormat.dateTimeParser().withOffsetParsed();
DateTime isoDT=fmt.parseDateTime(isoDate);
if (isoDT.year().get() > 9999) {
try {
fmt= DateTimeFormat.forPattern("yyyyMMdd");
fmt=fmt.withZone(DateTimeZone.getDefault());
isoDT=fmt.parseDateTime(isoDate);
}
catch (IllegalArgumentException e) {
try {
fmt=DateTimeFormat.forPattern("yyyyMM");
fmt=fmt.withZone(DateTimeZone.getDefault());
isoDT=fmt.parseDateTime(isoDate);
}
catch (IllegalArgumentException e1) {
try {
fmt=DateTimeFormat.forPattern("yyyy");
fmt=fmt.withZone(DateTimeZone.getDefault());
isoDT=fmt.parseDateTime(isoDate);
}
catch (IllegalArgumentException ignored) {
}
}
}
}
result=isoDT.withZoneRetainFields(DateTimeZone.getDefault());
return result;
}
private String getTimeStamp() {
return DATE_STAMP.concat(Instant.now().toString().replaceAll("[-:\\.]", "").substring(0, 15)).concat("Z\n");
}
private String getDescription(Resource aResource){
String description = aResource.getNestedFieldValue("description.@value", mPreferredLocale);
if (description == null || Strings.isEmpty(description)){
return "";
}
return DESCRIPTION.concat(description.replaceAll("\r\n|\n|\r", "\\\\r\\\\n")).concat("\n");
}
}