Skip to content

Commit 5d4ddc8

Browse files
Merge pull request #1 from choonchernlim/feature/filter
Feature/filter
2 parents 5c77a8c + 632157f commit 5d4ddc8

21 files changed

+493
-117
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Change Log
22

3+
## 0.2.0 - 2016-12-13
4+
5+
* Two new configuration options:-
6+
* `include.canceled.events` = Whether to include canceled events or not.
7+
* `include.event.body` = Whether to include event body or not.
8+
9+
* Better console log messages.
10+
311
## 0.1.0 - 2016-12-11
412

513
* Initial.

README.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,59 @@ calsync/
3838
* Edit `calsync.conf`.
3939

4040
* Run `java -jar calsync.jar` again.
41+
42+
43+
## calsync.conf
44+
45+
If `calsync.conf` is missing (ie: you are running it for the first time), the configuration file will be
46+
generated for you.
47+
48+
```
49+
# Environment variable name containing Exchange user name value.
50+
#
51+
# Accepted value: string.
52+
exchange.username.env=CALSYNC_EXCHANGE_USERNAME
53+
54+
# Environment variable name containing Exchange password value.
55+
#
56+
# Accepted value: string.
57+
exchange.password.env=CALSYNC_EXCHANGE_PASSWORD
58+
59+
# Exchange web service URL.
60+
#
61+
# Accepted value: string.
62+
exchange.url=https://[EXCHANGE_SERVER]/ews/exchange.asmx
63+
64+
# File path to Google client_secret.json.
65+
#
66+
# Accepted value: string.
67+
google.client.secret.json.file.path=client_secret.json
68+
69+
# A new Google calendar name that DOESN'T MATCH any existing Google calendar names.
70+
# Because CalSync performs one-way sync from Exchange calendar to Google calendar, it will wipe
71+
# any existing events in Google calendar if they don't match events from Exchange calendar.
72+
#
73+
# Accepted value: string.
74+
google.calendar.name=Outlook
75+
76+
# Total days to sync events from current day.
77+
#
78+
# Accepted value: integer greater than 0.
79+
total.sync.in.days=7
80+
81+
# Next sync in minutes, or 0 to disable next run.
82+
#
83+
# Accepted value: integer.
84+
next.sync.in.minutes=15
85+
86+
# Whether to include events marked as "canceled" or not.
87+
#
88+
# Accepted value: true, false.
89+
include.canceled.events=false
90+
91+
# Whether to include event body or not. When syncing from work Exchange calendar, sometimes it's
92+
# safer NOT to copy the event body, which may include sensitive information, or due to work policy.
93+
#
94+
# Accepted value: true, false.
95+
include.event.body=false
96+
```

pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,11 @@
6161
<artifactId>joda-time</artifactId>
6262
<version>2.9.6</version>
6363
</dependency>
64+
<dependency>
65+
<groupId>org.jsoup</groupId>
66+
<artifactId>jsoup</artifactId>
67+
<version>1.10.1</version>
68+
</dependency>
6469

6570
<dependency>
6671
<groupId>com.google.apis</groupId>

src/main/groovy/com/github/choonchernlim/calsync/core/CalSyncEvent.groovy

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ class CalSyncEvent {
1717
String subject
1818
String location
1919
Integer reminderMinutesBeforeStart
20+
String body
2021

2122
String googleEventId
2223
}

src/main/groovy/com/github/choonchernlim/calsync/core/ExchangeToGoogleService.groovy

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,11 @@ class ExchangeToGoogleService {
3434
googleService.init(userConfig)
3535

3636
// retrieve exchange events
37-
List<CalSyncEvent> exchangeEvents = exchangeService.getEvents(startDateTime, endDateTime)
37+
List<CalSyncEvent> exchangeEvents = exchangeService.getEvents(
38+
startDateTime,
39+
endDateTime,
40+
userConfig.includeCanceledEvents,
41+
userConfig.includeEventBody)
3842

3943
// retrieve google calendar
4044
String calendarId = googleService.getCalendarId(userConfig.googleCalendarName)

src/main/groovy/com/github/choonchernlim/calsync/core/Mapper.groovy

Lines changed: 87 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,25 @@
11
package com.github.choonchernlim.calsync.core
22

3+
import com.github.choonchernlim.calsync.exchange.ExchangeEvent
34
import com.google.api.client.util.DateTime
45
import com.google.api.services.calendar.model.Event
56
import com.google.api.services.calendar.model.EventDateTime
67
import com.google.api.services.calendar.model.EventReminder
78
import microsoft.exchange.webservices.data.core.service.item.Appointment
9+
import microsoft.exchange.webservices.data.property.complex.MessageBody
10+
import org.apache.commons.lang3.StringEscapeUtils
11+
import org.joda.time.format.DateTimeFormat
12+
import org.joda.time.format.DateTimeFormatter
13+
import org.jsoup.Jsoup
14+
import org.jsoup.nodes.Document
15+
import org.jsoup.safety.Whitelist
816

917
/**
1018
* Utility class to map one object type to another.
1119
*/
1220
class Mapper {
21+
static final DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern("MMM dd '@' hh:mm a")
22+
1323
/**
1424
* Maps Google EventDateTime to Joda DateTime.
1525
*
@@ -61,25 +71,29 @@ class Mapper {
6171
endDateTime: toJodaDateTime(event.getEnd()),
6272
subject: event.getSummary(),
6373
location: event.getLocation(),
64-
reminderMinutesBeforeStart: event.getReminders()?.getOverrides()?.get(0)?.getMinutes()
74+
reminderMinutesBeforeStart: event.getReminders()?.getOverrides()?.get(0)?.getMinutes(),
75+
body: event.getDescription() ?: null
6576
)
6677
}
6778

6879
/**
6980
* Maps Exchange Event to CalSyncEvent.
7081
*
71-
* @param appointment Exchange Event
82+
* @param exchangeEvent Exchange Event
83+
* @param includeEventBody Whether to include event body or not
7284
* @return CalSyncEvent
7385
*/
74-
static CalSyncEvent toCalSyncEvent(Appointment appointment) {
75-
assert appointment
86+
static CalSyncEvent toCalSyncEvent(ExchangeEvent exchangeEvent, Boolean includeEventBody) {
87+
assert exchangeEvent
88+
assert includeEventBody != null
7689

7790
return new CalSyncEvent(
78-
startDateTime: new org.joda.time.DateTime(appointment.start),
79-
endDateTime: new org.joda.time.DateTime(appointment.end),
80-
subject: appointment.subject,
81-
location: appointment.location,
82-
reminderMinutesBeforeStart: appointment.reminderMinutesBeforeStart
91+
startDateTime: exchangeEvent.startDateTime,
92+
endDateTime: exchangeEvent.endDateTime,
93+
subject: exchangeEvent.subject,
94+
location: exchangeEvent.location,
95+
reminderMinutesBeforeStart: exchangeEvent.reminderMinutesBeforeStart,
96+
body: includeEventBody ? exchangeEvent.body : null
8397
)
8498
}
8599

@@ -92,7 +106,6 @@ class Mapper {
92106
static Event toGoogleEvent(CalSyncEvent calSyncEvent) {
93107
assert calSyncEvent
94108

95-
// TODO test this!
96109
// only create reminder if there's one
97110
def reminders = calSyncEvent.reminderMinutesBeforeStart ?
98111
new Event.Reminders(
@@ -111,7 +124,70 @@ class Mapper {
111124
end: toGoogleEventDateTime(calSyncEvent.endDateTime),
112125
summary: calSyncEvent.subject,
113126
location: calSyncEvent.location,
114-
reminders: reminders
127+
reminders: reminders,
128+
description: calSyncEvent.body
129+
)
130+
}
131+
132+
/**
133+
* Maps Appointment to ExchangeEvent.
134+
*
135+
* @param appointment Appointment
136+
* @return ExchangeEvent
137+
*/
138+
// TODO not testable ATM, not sure how to mock Appointment to return `body` data
139+
static ExchangeEvent toExchangeEvent(Appointment appointment) {
140+
assert appointment
141+
142+
return new ExchangeEvent(
143+
startDateTime: new org.joda.time.DateTime(appointment.start),
144+
endDateTime: new org.joda.time.DateTime(appointment.end),
145+
subject: appointment.subject,
146+
location: appointment.location,
147+
reminderMinutesBeforeStart: appointment.reminderMinutesBeforeStart,
148+
body: toPlainText(MessageBody.getStringFromMessageBody(appointment.body)),
149+
isCanceled: appointment.isCancelled
115150
)
116151
}
152+
153+
/**
154+
* Transforms HTML text to plain text.
155+
*
156+
* @param html HTML text
157+
* @return Plain text
158+
*/
159+
// http://stackoverflow.com/questions/5640334/how-do-i-preserve-line-breaks-when-using-jsoup-to-convert-html-to-plain-text
160+
static String toPlainText(String html) {
161+
if (!html?.trim()) {
162+
return null
163+
}
164+
165+
Document.OutputSettings outputSettings = new Document.OutputSettings().prettyPrint(false)
166+
167+
Document document = Jsoup.parse(html)
168+
document.outputSettings(outputSettings)
169+
170+
document.select('br').append('\\n')
171+
document.select('p').prepend('\\n\\n')
172+
173+
// Exchange tends to have <div>&nbsp;</div> as line separator
174+
document.select('div:contains( )').prepend('\\n\\n')
175+
176+
String sanitizedHtml = document.html().replaceAll('\\\\n', '\n').replaceAll('&nbsp;', ' ')
177+
178+
return StringEscapeUtils.unescapeHtml4(Jsoup.clean(sanitizedHtml, '', Whitelist.none(), outputSettings)).
179+
trim() ?: null
180+
}
181+
182+
/**
183+
* Returns human readable datetime.
184+
*
185+
* @param dateTime Joda time
186+
* @return Datetime string
187+
*/
188+
static String humanReadableDateTime(org.joda.time.DateTime dateTime) {
189+
assert dateTime
190+
191+
return dateTimeFormatter.print(dateTime)
192+
}
117193
}

src/main/groovy/com/github/choonchernlim/calsync/core/UserConfig.groovy

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,6 @@ class UserConfig {
1414
String googleCalendarName
1515
Integer totalSyncDays
1616
Integer nextSyncInMinutes
17+
Boolean includeCanceledEvents
18+
Boolean includeEventBody
1719
}

src/main/groovy/com/github/choonchernlim/calsync/core/UserConfigReader.groovy

Lines changed: 34 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,17 @@ import groovy.transform.PackageScope
77
*/
88
class UserConfigReader {
99

10+
static final String SAMPLE_CONF = 'calsync-sample.conf'
11+
1012
static final String EXCHANGE_USERNAME_ENV_KEY = 'exchange.username.env'
1113
static final String EXCHANGE_PASSWORD_ENV_KEY = 'exchange.password.env'
1214
static final String EXCHANGE_URL_KEY = 'exchange.url'
1315
static final String GOOGLE_CLIENT_SECRET_JSON_KEY = 'google.client.secret.json.file.path'
1416
static final String GOOGLE_CALENDAR_NAME_KEY = 'google.calendar.name'
1517
static final String TOTAL_SYNC_IN_DAYS_KEY = 'total.sync.in.days'
1618
static final String NEXT_SYNC_IN_MINUTES_KEY = 'next.sync.in.minutes'
19+
static final String INCLUDE_CANCELED_EVENTS_KEY = 'include.canceled.events'
20+
static final String INCLUDE_EVENT_BODY_KEY = 'include.event.body'
1721

1822
/**
1923
* Returns user config.
@@ -41,29 +45,7 @@ class UserConfigReader {
4145
return props
4246
}
4347

44-
propsFile.write('''
45-
# Environment variable name containing Exchange user name value.
46-
exchange.username.env=CALSYNC_EXCHANGE_USERNAME
47-
48-
# Environment variable name containing Exchange password value.
49-
exchange.password.env=CALSYNC_EXCHANGE_PASSWORD
50-
51-
# Exchange web service URL.
52-
exchange.url=https://[EXCHANGE_SERVER]/ews/exchange.asmx
53-
54-
# Google client_secret.json.
55-
google.client.secret.json.file.path=client_secret.json
56-
57-
# Google calendar name. If calendar name matches existing Google calendar names, that will be used.
58-
# Otherwise, a new calendar of that name will be created first.
59-
google.calendar.name=Outlook
60-
61-
# Total days to sync events from current day.
62-
total.sync.in.days=7
63-
64-
# Next sync in minutes, or 0 to disable next run.
65-
next.sync.in.minutes=15
66-
''')
48+
propsFile.write(this.class.classLoader.getResource(SAMPLE_CONF).text)
6749

6850
throw new CalSyncException(
6951
"${Constant.CONFIG_FILE_PATH} not found... creating one at ${propsFile.getAbsoluteFile()}. " +
@@ -96,6 +78,9 @@ next.sync.in.minutes=15
9678

9779
Integer nextSyncInMinutes = validatePropInteger(props, errors, NEXT_SYNC_IN_MINUTES_KEY)
9880

81+
Boolean includeCanceledEvents = validatePropBoolean(props, errors, INCLUDE_CANCELED_EVENTS_KEY)
82+
Boolean includeEventBody = validatePropBoolean(props, errors, INCLUDE_EVENT_BODY_KEY)
83+
9984
if (!errors.isEmpty()) {
10085
throw new CalSyncException(
10186
"The configuration is invalid. Please fix the errors below, then run it again:-" +
@@ -109,7 +94,9 @@ next.sync.in.minutes=15
10994
googleClientSecretJsonFilePath: googleClientSecretJsonFilePath,
11095
googleCalendarName: googleCalendarName,
11196
totalSyncDays: totalSyncDays,
112-
nextSyncInMinutes: nextSyncInMinutes
97+
nextSyncInMinutes: nextSyncInMinutes,
98+
includeCanceledEvents: includeCanceledEvents,
99+
includeEventBody: includeEventBody
113100
)
114101
}
115102

@@ -161,6 +148,29 @@ next.sync.in.minutes=15
161148
return value.toInteger()
162149
}
163150

151+
/**
152+
* Ensures property has boolean value.
153+
*
154+
* @param props Properties
155+
* @param errors Error list
156+
* @param propKey Property key
157+
* @return Boolean value if valid, otherwise null
158+
*/
159+
private Boolean validatePropBoolean(Properties props, List<String> errors, String propKey) {
160+
String value = validatePropString(props, errors, propKey)
161+
162+
if (!value) {
163+
return null
164+
}
165+
166+
if (!value.toLowerCase().matches(/true|false/)) {
167+
errors.add("${propKey}: Must be true or false.")
168+
return null
169+
}
170+
171+
return Boolean.valueOf(value)
172+
}
173+
164174
/**
165175
* Ensures property has string value.
166176
*

0 commit comments

Comments
 (0)