Added: openmeetings/application/trunk/openmeetings-db/src/main/java/org/apache/openmeetings/db/entity/calendar/OmCalendar.java URL: http://svn.apache.org/viewvc/openmeetings/application/trunk/openmeetings-db/src/main/java/org/apache/openmeetings/db/entity/calendar/OmCalendar.java?rev=1763226&view=auto ============================================================================== --- openmeetings/application/trunk/openmeetings-db/src/main/java/org/apache/openmeetings/db/entity/calendar/OmCalendar.java (added) +++ openmeetings/application/trunk/openmeetings-db/src/main/java/org/apache/openmeetings/db/entity/calendar/OmCalendar.java Tue Oct 4 04:03:01 2016 @@ -0,0 +1,163 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License") + you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.openmeetings.db.entity.calendar; + +import org.apache.openjpa.persistence.jdbc.ForeignKey; +import org.apache.openmeetings.db.entity.IDataProviderEntity; +import org.apache.openmeetings.db.entity.user.User; +import org.simpleframework.xml.Element; +import org.simpleframework.xml.Root; + +import javax.persistence.*; + +@Entity +@Table(name = "om_calendar") +@NamedQueries({ + @NamedQuery(name="getCalendars", query="SELECT c FROM OmCalendar c WHERE c.deleted = false ORDER BY c.id") + , @NamedQuery(name="getCalendarbyUser", query="SELECT c FROM OmCalendar c" + + " WHERE c.deleted = false AND c.owner.id = :userId AND c.syncType <> OmCalendar$SyncType.GOOGLE_CALENDAR " + + "ORDER BY c.id") + // OpenJPA has trouble with referencing Subclasses, thus java $ symbol is used + // Comes from the OpenJPA Mailing List. + , @NamedQuery(name="getGoogleCalendars", query="SELECT c FROM OmCalendar c WHERE c.deleted = false AND c.owner.id = :userId " + + "AND c.syncType = OmCalendar$SyncType.GOOGLE_CALENDAR ORDER BY c.id") + , @NamedQuery(name="getAppointmentsbyCalendarinRange", + query="SELECT a FROM Appointment a " + + "WHERE a.deleted = false " + + " AND ( " + + " (a.start BETWEEN :start AND :end) " + + " OR (a.end BETWEEN :start AND :end) " + + " OR (a.start < :start AND a.end > :end) " + + " )" + + " AND a.owner.id = :userId AND a.calendar.id = :calId ") + , @NamedQuery(name="getCalendarbyId", + query="SELECT c FROM OmCalendar c WHERE c.deleted = false AND c.id = :calId") +}) +@Root(name="om_calendar") +public class OmCalendar implements IDataProviderEntity { + private static final long serialVersionUID = 1L; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name="id") + @Element(name="calendarId", data=true) + private Long id; + + @Column(name="title") + @Element(name="calendarTitle", data=true, required=false) + private String title; + + //Can Also act as the Google Calendar ID for Google Calendars + @Column(name="href") + @Element(name="calendarHref", data=true) + private String href; + + //Can also act as the Google API Key for Google Calendars. Note, not always needed. + @Column(name="sync_token") + @Element(name="calendarSyncToken", data=true, required=false) + private String token; + + @Column(name="deleted") + @Element(data=true) + private boolean deleted; + + public enum SyncType { + NONE, + ETAG, + CTAG, + WEBDAV_SYNC, + GOOGLE_CALENDAR + } + + @Column(name="sync_type") + @Enumerated(EnumType.STRING) + @Element(name="sync_type", data=true) + private SyncType syncType = SyncType.NONE; + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name="user_id", nullable=true) + @ForeignKey(enabled=true) + @Element(name="users_id", data=true, required=false) + private User owner; + + //Getters + Setters + @Override + public Long getId() { + return id; + } + + @Override + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getHref() { + return href; + } + + public void setHref(String href) { + this.href = href; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public boolean isDeleted() { + return deleted; + } + + public void setDeleted(boolean deleted) { + this.deleted = deleted; + } + + public SyncType getSyncType() { + return syncType; + } + + public void setSyncType(SyncType syncType) { + this.syncType = syncType; + } + + public User getOwner() { + return owner; + } + + public void setOwner(User owner) { + this.owner = owner; + } + + @Override + public String toString() { + return "Calendar [ id=" + id + ", title=" + title + ", token=" + token + ", href=" + + href + ", SyncType=" + syncType + ", deleted=" + deleted + ", owner=" + owner + " ]"; + } +}
Added: openmeetings/application/trunk/openmeetings-server/src/site/resources/images/CalendarDialog_1.png URL: http://svn.apache.org/viewvc/openmeetings/application/trunk/openmeetings-server/src/site/resources/images/CalendarDialog_1.png?rev=1763226&view=auto ============================================================================== Binary file - no diff available. Propchange: openmeetings/application/trunk/openmeetings-server/src/site/resources/images/CalendarDialog_1.png ------------------------------------------------------------------------------ svn:mime-type = application/octet-stream Added: openmeetings/application/trunk/openmeetings-server/src/site/resources/images/CalendarDialog_2.png URL: http://svn.apache.org/viewvc/openmeetings/application/trunk/openmeetings-server/src/site/resources/images/CalendarDialog_2.png?rev=1763226&view=auto ============================================================================== Binary file - no diff available. Propchange: openmeetings/application/trunk/openmeetings-server/src/site/resources/images/CalendarDialog_2.png ------------------------------------------------------------------------------ svn:mime-type = application/octet-stream Added: openmeetings/application/trunk/openmeetings-server/src/site/xdoc/CalDAVandGCal.xml URL: http://svn.apache.org/viewvc/openmeetings/application/trunk/openmeetings-server/src/site/xdoc/CalDAVandGCal.xml?rev=1763226&view=auto ============================================================================== --- openmeetings/application/trunk/openmeetings-server/src/site/xdoc/CalDAVandGCal.xml (added) +++ openmeetings/application/trunk/openmeetings-server/src/site/xdoc/CalDAVandGCal.xml Tue Oct 4 04:03:01 2016 @@ -0,0 +1,95 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> +<document xmlns="http://maven.apache.org/XDOC/2.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/XDOC/2.0 http://maven.apache.org/xsd/xdoc-2.0.xsd"> + <properties> + <title>CalDAV and Google Calendar integration</title> + <author email="[email protected]">Ankush Mishra</author> + </properties> + <body> + <section name="CalDAV and Google Calendar integration"> + <subsection name="Introduction"> + <p>This integration brings the ability to use an External CalDAV server or a Google Calendar. + Till now the CalDAV server support includes, bidirectional access, + but Google Calendar for now supports only a read-only access.</p> + <p>Two types of external calendar available:</p> + <div> + <ul> + <li>CalDAV Calendar</li> + <li>Google Calendar</li> + </ul> + </div> + </subsection> + <subsection name="CalDAV Calendar"> + <p>On the Calendar Page, after selecting the "Add External Calendar", you should see the Dialog Box as shown.</p> + <img src="images/CalendarDialog_1.png" alt="" width="600"/> + <br/> + <p> + By default it should be as shown. In the Title field, + it'll be Display Title of the Calendar. The External CalDAV URL, will be either the URL to your <b>Calendar</b> or to the <b>User Principal</b>. + Along with this provide the credentials to the Calendar Server, if necessary.</p> + <p>In case the URL is the User Principal, then all the Calendars in the Principal will be added to the Calendar page.</p> + </subsection> + <subsection name="Google Calendar"> + <p>When the Google Calendar Checkbox is selected, then the calendar is a Google Calendar. + The two options, should be about the Google Calendar ID and the API Access Key. To know about how get those, follow the steps below. + </p> + <img src="images/CalendarDialog_2.png" alt="" width="600"/> + <br/> + <p> + First we, need to make a Google Calendar API key. + </p> + <div> + <ul> + <li>Go to the <a href="https://console.developers.google.com/" rel="nofollow">Google Developer Console</a> and create a new project (it might take a second).</li> + <li>Once in the project, go to <b>APIs & auth > APIs</b> on the sidebar.</li> + <li>Find "Calendar API" in the list and turn it ON.</li> + <li>On the sidebar, click <b>APIs & auth > Credentials</b>.</li> + <li>In the "Public API access" section, click "Create new Key".</li> + <li>Choose "Browser key".</li> + <li>If you know what domains will host your calendar, enter them into the box. Otherwise, leave it blank. You can always change it later.</li> + <li>Your new API key will appear. It might take second or two before it starts working.</li> + </ul> + </div> + <p>Make your Google Calendar public:</p> + <div> + <ul> + <li>In the Google Calendar interface, locate the "My calendars" area on the left.</li> + <li>Hover over the calendar you need and click the downward arrow.</li> + <li>A menu will appear. Click "Share this Calendar".</li> + <li>Check "Make this calendar public".</li> + <li>Make sure "Share only my free/busy information" is <b>unchecked</b>.</li> + <li>Click "Save".</li> + </ul> + </div> + <p>Obtain your Google Calendar's ID:</p> + <div> + <ul> + <li>In the Google Calendar interface, locate the "My calendars" area on the left.</li> + <li>Hover over the calendar you need and click the downward arrow.</li> + <li>A menu will appear. Click "Calendar settings".</li> + <li>In the "Calendar Address" section of the screen, you will see your Calendar ID. It will look something like "[email protected]".</li> + </ul> + </div> + <p>Place these two values in the Dialog Box's fields. + Note, at the current state the Google Calendar events, may not be visible on the page, + after adding. If you've done the aforementioned steps correctly, then, + just Reload the page or go out of the Calendar page for a moment and come back.</p> + <p>Note: The Steps mentioned have been taken from <a href="http://fullcalendar.io/docs/google_calendar/" rel="nofollow">here.</a></p> + </subsection> + </section> + </body> +</document> Modified: openmeetings/application/trunk/openmeetings-service/pom.xml URL: http://svn.apache.org/viewvc/openmeetings/application/trunk/openmeetings-service/pom.xml?rev=1763226&r1=1763225&r2=1763226&view=diff ============================================================================== --- openmeetings/application/trunk/openmeetings-service/pom.xml (original) +++ openmeetings/application/trunk/openmeetings-service/pom.xml Tue Oct 4 04:03:01 2016 @@ -43,5 +43,17 @@ <artifactId>wicket-core</artifactId> <version>${wicket.version}</version> </dependency> + <dependency> + <groupId>com.github.caldav4j</groupId> + <artifactId>caldav4j</artifactId> + <version>0.9-SNAPSHOT</version> + <exclusions> + <exclusion> + <!-- Remove the need for ehcache, since we use the Methods directly --> + <groupId>net.sf.ehcache</groupId> + <artifactId>ehcache</artifactId> + </exclusion> + </exclusions> + </dependency> </dependencies> </project> Added: openmeetings/application/trunk/openmeetings-service/src/main/java/org/apache/openmeetings/service/calendar/caldav/AppointmentManager.java URL: http://svn.apache.org/viewvc/openmeetings/application/trunk/openmeetings-service/src/main/java/org/apache/openmeetings/service/calendar/caldav/AppointmentManager.java?rev=1763226&view=auto ============================================================================== --- openmeetings/application/trunk/openmeetings-service/src/main/java/org/apache/openmeetings/service/calendar/caldav/AppointmentManager.java (added) +++ openmeetings/application/trunk/openmeetings-service/src/main/java/org/apache/openmeetings/service/calendar/caldav/AppointmentManager.java Tue Oct 4 04:03:01 2016 @@ -0,0 +1,587 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License") + you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.openmeetings.service.calendar.caldav; + +import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT; +import static javax.servlet.http.HttpServletResponse.SC_OK; +import static org.apache.openmeetings.util.OpenmeetingsVariables.webAppRootKey; + +import java.io.IOException; +import java.net.URI; +import java.util.Collection; +import java.util.List; + +import org.apache.commons.httpclient.Credentials; +import org.apache.commons.httpclient.HttpClient; +import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager; +import org.apache.commons.httpclient.auth.AuthScope; +import org.apache.commons.httpclient.methods.OptionsMethod; +import org.apache.commons.httpclient.params.HttpClientParams; +import org.apache.commons.httpclient.params.HttpConnectionManagerParams; +import org.apache.jackrabbit.webdav.DavConstants; +import org.apache.jackrabbit.webdav.MultiStatusResponse; +import org.apache.jackrabbit.webdav.client.methods.PropFindMethod; +import org.apache.jackrabbit.webdav.property.DavProperty; +import org.apache.jackrabbit.webdav.property.DavPropertyName; +import org.apache.jackrabbit.webdav.property.DavPropertyNameSet; +import org.apache.jackrabbit.webdav.property.DavPropertySet; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.apache.openmeetings.db.dao.calendar.AppointmentDao; +import org.apache.openmeetings.db.dao.calendar.OmCalendarDao; +import org.apache.openmeetings.db.entity.calendar.Appointment; +import org.apache.openmeetings.db.entity.calendar.OmCalendar; +import org.apache.openmeetings.db.entity.calendar.OmCalendar.SyncType; +import org.apache.openmeetings.service.calendar.caldav.handler.CalendarHandler; +import org.apache.openmeetings.service.calendar.caldav.handler.CtagHandler; +import org.apache.openmeetings.service.calendar.caldav.handler.EtagsHandler; +import org.apache.openmeetings.service.calendar.caldav.handler.WebDAVSyncHandler; +import org.apache.wicket.util.string.Strings; +import org.osaf.caldav4j.CalDAVConstants; +import org.red5.logging.Red5LoggerFactory; +import org.slf4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.w3c.dom.Element; + +/** + * Class which does syncing and provides respective API's required for performing CalDAV Operations. + * @author Ankush Mishra + */ +public class AppointmentManager { + private static final Logger log = Red5LoggerFactory.getLogger(AppointmentManager.class, webAppRootKey); + + //HttpClient and ConnectionManager Params + private static final int IDLE_CONNECTION_TIMEOUT = 30000; // 30 seconds + private static final int MAX_HOST_CONNECTIONS = 6; // Number of simultaneous connections to one host + private static final int MAX_TOTAL_CONNECTIONS = 10; // Max Connections, at one time in memory. + private static final int CONNECTION_MANAGER_TIMEOUT = 1000; // Waits for 1 sec if no empty connection exists + + private MultiThreadedHttpConnectionManager connmanager = null; + + @Autowired + private OmCalendarDao calendarDao; + @Autowired + private AppointmentDao appointmentDao; + @Autowired + private iCalUtils utils; + + /** + * Returns a new HttpClient with the inbuilt connection manager in this. + * + * @return HttpClient object that was created. + */ + public HttpClient createHttpClient() { + if (connmanager == null) { + connmanager = new MultiThreadedHttpConnectionManager(); + HttpConnectionManagerParams params = new HttpConnectionManagerParams(); + params.setDefaultMaxConnectionsPerHost(MAX_HOST_CONNECTIONS); + params.setMaxTotalConnections(MAX_TOTAL_CONNECTIONS); + connmanager.setParams(params); + } + + HttpClientParams clientParams = new HttpClientParams(); + clientParams.setConnectionManagerTimeout(CONNECTION_MANAGER_TIMEOUT); + return new HttpClient(connmanager); + } + + /** + * Returns the Path from the Calendar. + * + * @param client Client which makes the connection. + * @param calendar Calendar who's URL is used to get the path from. + * @return Path component of the URL. + */ + public String getPathfromCalendar(HttpClient client, OmCalendar calendar) { + URI temp = URI.create(calendar.getHref()); + client.getHostConfiguration().setHost(temp.getHost(), temp.getPort(), temp.getScheme()); + return temp.getPath(); + } + + /** + * Ensure the URL ends with a, trailing slash, i.e. "/" + * + * @param str String URL to check. + * @return String which has a trailing slash. + */ + private static String ensureTrailingSlash(String str) { + return str.endsWith("/") || str.endsWith("\\") ? str : str + "/"; + } + + /** + * Adds the Credentials provided to the given client on the Calendar's URL. + * + * @param client Client which makes the connection. + * @param calendar Calendar whose Host the Credentials are for. + * @param credentials Credentials to add + */ + public void provideCredentials(HttpClient client, OmCalendar calendar, Credentials credentials) { + if (!Strings.isEmpty(calendar.getHref()) && credentials != null) { + URI temp = URI.create(calendar.getHref()); + client.getHostConfiguration().setHost(temp.getHost(), temp.getPort(), temp.getScheme()); + client.getState().setCredentials(new AuthScope(temp.getHost(), temp.getPort()), credentials); + } + } + + /** + * Tests if the Calendar's URL can be accessed, or not. + * + * @param client Client which makes the connection. + * @param calendar Calendar whose URL is to be accessed. + * @return Returns true for HTTP Status 200, or 204, else false. + */ + public boolean testConnection(HttpClient client, OmCalendar calendar) { + cleanupIdleConnections(); + + OptionsMethod optionsMethod = null; + try { + String path = getPathfromCalendar(client, calendar); + optionsMethod = new OptionsMethod(path); + optionsMethod.setRequestHeader("Accept", "*/*"); + client.executeMethod(optionsMethod); + int status = optionsMethod.getStatusCode(); + if (status == SC_OK || status == SC_NO_CONTENT) { + return true; + } + } catch (IOException e) { + log.error("Error executing OptionsMethod during testConnection.", e); + } catch (Exception e) { + //Should not ever reach here. + log.error("Severe Error in executing OptionsMethod during testConnection.", e); + } finally { + if (optionsMethod != null) { + optionsMethod.releaseConnection(); + } + } + return false; + } + + /** + * Create or Update calendar on the database. + * + * @param calendar Calendar to update or create + */ + public boolean createCalendar(HttpClient client, OmCalendar calendar) { + if (calendar.getId() == null && calendar.getSyncType() != SyncType.GOOGLE_CALENDAR) { + return discoverCalendars(client, calendar); + } + calendarDao.update(calendar); + return true; + } + + /** + * Deletes the calendar from the local database. + * + * @param calendar Calendar to delete + */ + public void deleteCalendar(OmCalendar calendar) { + calendarDao.delete(calendar); + } + + public List<OmCalendar> getCalendars() { + return calendarDao.get(); + } + + /** + * @see OmCalendarDao#get(Long) + */ + public List<OmCalendar> getCalendars(Long userid) { + return calendarDao.get(userid); + } + + /** + * @see OmCalendarDao#getGoogleCalendars(Long) + */ + public List<OmCalendar> getGoogleCalendars(Long userId) { + return calendarDao.getGoogleCalendars(userId); + } + + /** + * Function which when called performs syncing based on the type of Syncing detected. + * + * @param calendar Calendar who's sync has to take place + */ + public void syncItem(HttpClient client, OmCalendar calendar) { + cleanupIdleConnections(); + + if (calendar.getSyncType() != SyncType.NONE) { + CalendarHandler calendarHandler; + String path = getPathfromCalendar(client, calendar); + + switch (calendar.getSyncType()) { + case WEBDAV_SYNC: + calendarHandler = new WebDAVSyncHandler(path, calendar, client, appointmentDao, utils); + break; + case CTAG: + calendarHandler = new CtagHandler(path, calendar, client, appointmentDao, utils); + break; + case ETAG: + default: //Default is the EtagsHandler. + calendarHandler = new EtagsHandler(path, calendar, client, appointmentDao, utils); + break; + } + + calendarHandler.syncItems(); + calendarDao.update(calendar); + } + } + + /** + * Syncs all the calendars currrently present on the DB. + */ + public void syncItems(HttpClient client, Long userId) { + List<OmCalendar> calendars = getCalendars(userId); + for (OmCalendar calendar : calendars) { + syncItem(client, calendar); + } + } + + /** + * Function which finds all the calendars of the Principal URL of the calendar + */ + private boolean discoverCalendars(HttpClient client, OmCalendar calendar) { + cleanupIdleConnections(); + + if (calendar.getSyncType() == SyncType.NONE) { + PropFindMethod propFindMethod = null; + String userPath = null, homepath = null; + + DavPropertyName curUserPrincipal = DavPropertyName.create("current-user-principal"), + calHomeSet = DavPropertyName.create("calendar-home-set", CalDAVConstants.NAMESPACE_CALDAV), + suppCalCompSet = DavPropertyName.create("supported-calendar-component-set", CalDAVConstants.NAMESPACE_CALDAV); + + //Find out whether it's a calendar or if we can find the calendar-home or current-user url + try { + String path = getPathfromCalendar(client, calendar); + + DavPropertyNameSet properties = new DavPropertyNameSet(); + properties.add(curUserPrincipal); + properties.add(calHomeSet); + properties.add(DavPropertyName.RESOURCETYPE); + + propFindMethod = new PropFindMethod(path, properties, CalDAVConstants.DEPTH_0); + client.executeMethod(propFindMethod); + + if (propFindMethod.succeeded()) { + for (MultiStatusResponse response : propFindMethod.getResponseBodyAsMultiStatus().getResponses()) { + DavPropertySet set = response.getProperties(SC_OK); + DavProperty<?> calhome = set.get(calHomeSet), curPrinci = set.get(curUserPrincipal), + resourcetype = set.get(DavPropertyName.RESOURCETYPE); + + if (checkCalendarResourceType(resourcetype)) { + //This is a calendar and thus initialize and return + return initCalendar(client, calendar); + } + + //Else find all the calendars on the Principal and return. + if (calhome != null) { + //Calendar Home Path + homepath = getTextValuefromProperty(calhome); + break; + } else if (curPrinci != null) { + //Current User Principal Path + userPath = getTextValuefromProperty(curPrinci); + break; + } + } + } else { + return false; + } + + if (homepath == null && userPath != null) { + //If calendar home path wasn't set, then we get it + DavPropertyNameSet props = new DavPropertyNameSet(); + props.add(calHomeSet); + propFindMethod = new PropFindMethod(userPath, props, DavConstants.DEPTH_0); + client.executeMethod(propFindMethod); + + if (propFindMethod.succeeded()) { + for (MultiStatusResponse response : propFindMethod.getResponseBodyAsMultiStatus().getResponses()) { + DavPropertySet set = response.getProperties(SC_OK); + DavProperty<?> calhome = set.get(calHomeSet); + + if (calhome != null) { + homepath = getTextValuefromProperty(calhome); + break; + } + } + } else { + return false; + } + } + + if (homepath != null) { + DavPropertyNameSet props = new DavPropertyNameSet(); + props.add(DavPropertyName.RESOURCETYPE); + props.add(suppCalCompSet); + props.add(DavPropertyName.DISPLAYNAME); + + propFindMethod = new PropFindMethod(homepath, props, DavConstants.DEPTH_1); + + client.executeMethod(propFindMethod); + + if (propFindMethod.succeeded()) { + boolean success = false; + for (MultiStatusResponse response : propFindMethod.getResponseBodyAsMultiStatus().getResponses()) { + boolean isVevent = false, isCalendar; + + DavPropertySet set = response.getProperties(SC_OK); + DavProperty<?> p = set.get(suppCalCompSet), + resourcetype = set.get(DavPropertyName.RESOURCETYPE), + displayname = set.get(DavPropertyName.DISPLAYNAME); + + isCalendar = checkCalendarResourceType(resourcetype); + + if (p != null) { + for (Object o : (Collection<?>) p.getValue()) { + if (o instanceof Element) { + Element e = (Element) o; + String name = DomUtil.getAttribute(e, "name", null); + if (name.equals("VEVENT")) { + isVevent = true; + } + } + } + } + + if (isCalendar && isVevent) { + success = true; + //Get New Calendar + OmCalendar tempCalendar = new OmCalendar(); + + if (displayname != null) { + tempCalendar.setTitle(displayname.getValue().toString()); + } + + tempCalendar.setHref(client.getHostConfiguration().getHostURL() + response.getHref()); + + tempCalendar.setDeleted(false); + tempCalendar.setOwner(calendar.getOwner()); + + calendarDao.update(tempCalendar); + initCalendar(client, tempCalendar); + } + } + return success; + } + } + + } catch (IOException e) { + log.error("Error executing PROPFIND Method, during testConnection.", e); + } catch (Exception e) { + log.error("Severe Error in executing PROPFIND Method, during testConnection.", e); + } finally { + if (propFindMethod != null) { + propFindMethod.releaseConnection(); + } + } + } + + return false; + } + + private static String getTextValuefromProperty(DavProperty<?> property) { + String value = null; + + if (property != null) { + for (Object o : (Collection<?>) property.getValue()) { + if (o instanceof Element) { + Element e = (Element) o; + value = DomUtil.getTextTrim(e); + break; + } + } + } + return value; + } + + /** + * Returns true if the resourcetype Property has a Calendar Element under it. + * + * @param resourcetype ResourceType Property + * @return True if, resource is Calendar, else false. + */ + private static boolean checkCalendarResourceType(DavProperty<?> resourcetype) { + boolean isCalendar = false; + + if (resourcetype != null) { + DavPropertyName calProp = DavPropertyName.create("calendar", CalDAVConstants.NAMESPACE_CALDAV); + + for (Object o : (Collection<?>) resourcetype.getValue()) { + if (o instanceof Element) { + Element e = (Element) o; + if (e.getLocalName().equals(calProp.getName())) { + isCalendar = true; + } + } + } + } + return isCalendar; + } + + /** + * Function to initialize the Calendar on the type of syncing and whether it can be used or not. + */ + private boolean initCalendar(HttpClient client, OmCalendar calendar) { + + if (calendar.getToken() == null || calendar.getSyncType() == SyncType.NONE) { + calendarDao.update(calendar); + + PropFindMethod propFindMethod = null; + + try { + String path = getPathfromCalendar(client, calendar); + + DavPropertyNameSet properties = new DavPropertyNameSet(); + properties.add(DavPropertyName.RESOURCETYPE); + properties.add(DavPropertyName.DISPLAYNAME); + properties.add(CtagHandler.DNAME_GETCTAG); + properties.add(WebDAVSyncHandler.DNAME_SYNCTOKEN); + + propFindMethod = new PropFindMethod(path, properties, CalDAVConstants.DEPTH_0); + client.executeMethod(propFindMethod); + + if (propFindMethod.succeeded()) { + + for (MultiStatusResponse response : propFindMethod.getResponseBodyAsMultiStatus().getResponses()) { + DavPropertySet set = response.getProperties(SC_OK); + + if (calendar.getTitle() == null) { + DavProperty<?> property = set.get(DavPropertyName.DISPLAYNAME); + calendar.setTitle(property == null ? null : property.getValue().toString()); + } + + DavProperty<?> ctag = set.get(CtagHandler.DNAME_GETCTAG), + syncToken = set.get(WebDAVSyncHandler.DNAME_SYNCTOKEN); + if (syncToken != null) { + calendar.setSyncType(SyncType.WEBDAV_SYNC); + } else if (ctag != null) { + calendar.setSyncType(SyncType.CTAG); + } else { + calendar.setSyncType(SyncType.ETAG); + } + } + + syncItem(client, calendar); + return true; + } else { + log.error("Error executing PROPFIND Method, with status Code: {}", propFindMethod.getStatusCode()); + calendar.setSyncType(SyncType.NONE); + } + + } catch (IOException e) { + log.error("Error executing OptionsMethod during testConnection.", e); + } catch (Exception e) { + log.error("Severe Error in executing OptionsMethod during testConnection.", e); + } finally { + if (propFindMethod != null) { + propFindMethod.releaseConnection(); + } + } + } + + return false; + } + + /** + * Function for create/updating multiple appointment on the server. + * Performs modification alongside of creation new events on the server. + * + * @param appointment Appointment to create/update. + */ + public boolean updateItem(HttpClient client, Appointment appointment) { + cleanupIdleConnections(); + + OmCalendar calendar = appointment.getCalendar(); + SyncType type = calendar.getSyncType(); + if (type != SyncType.NONE && type != SyncType.GOOGLE_CALENDAR) { + CalendarHandler calendarHandler; + String path = ensureTrailingSlash(getPathfromCalendar(client, calendar)); + + switch (type) { + case WEBDAV_SYNC: + case CTAG: + case ETAG: + calendarHandler = new EtagsHandler(path, calendar, client, appointmentDao, utils); + break; + default: + return false; + } + return calendarHandler.updateItem(appointment); + } + return false; + } + + /** + * Delete Appointment on the CalDAV server. + * Delete's on the Server only if the ETag of the Appointment is the one on the server, + * i.e. only if the Event hasn't changed on the Server. + * + * @param appointment Appointment to Delete + */ + public boolean deleteItem(HttpClient client, Appointment appointment) { + cleanupIdleConnections(); + + OmCalendar calendar = appointment.getCalendar(); + SyncType type = calendar.getSyncType(); + + if (type != SyncType.NONE && type != SyncType.GOOGLE_CALENDAR) { + CalendarHandler calendarHandler; + String path = getPathfromCalendar(client, calendar); + + switch (type) { + case WEBDAV_SYNC: + case CTAG: + case ETAG: + calendarHandler = new EtagsHandler(path, calendar, client, appointmentDao, utils); + break; + default: + return false; + } + + return calendarHandler.deleteItem(appointment); + } + return false; + } + + /** + * Returns the String value of the property, else null. + * + * @param property Property who's string value is to be returned. + * @return String representation of the Property Value. + */ + public static String getTokenFromProperty(DavProperty<?> property) { + return (property == null) ? null : property.getValue().toString(); + } + + /** + * Cleans up unused idle connections. + */ + public void cleanupIdleConnections() { + if (connmanager != null) { + connmanager.closeIdleConnections(IDLE_CONNECTION_TIMEOUT); + } + } + + /** + * Method which is called when the Context is destroyed. + */ + public void destroy() { + MultiThreadedHttpConnectionManager.shutdownAll(); + connmanager = null; + } +} Added: openmeetings/application/trunk/openmeetings-service/src/main/java/org/apache/openmeetings/service/calendar/caldav/handler/AbstractCalendarHandler.java URL: http://svn.apache.org/viewvc/openmeetings/application/trunk/openmeetings-service/src/main/java/org/apache/openmeetings/service/calendar/caldav/handler/AbstractCalendarHandler.java?rev=1763226&view=auto ============================================================================== --- openmeetings/application/trunk/openmeetings-service/src/main/java/org/apache/openmeetings/service/calendar/caldav/handler/AbstractCalendarHandler.java (added) +++ openmeetings/application/trunk/openmeetings-service/src/main/java/org/apache/openmeetings/service/calendar/caldav/handler/AbstractCalendarHandler.java Tue Oct 4 04:03:01 2016 @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License") + you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.openmeetings.service.calendar.caldav.handler; + +import org.apache.commons.httpclient.HttpClient; +import org.apache.openmeetings.db.dao.calendar.AppointmentDao; +import org.apache.openmeetings.db.entity.calendar.Appointment; +import org.apache.openmeetings.db.entity.calendar.OmCalendar; +import org.apache.openmeetings.service.calendar.caldav.iCalUtils; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Abstract Class which contains all the common code for all Handlers. + */ +public abstract class AbstractCalendarHandler implements CalendarHandler { + + //TODO: Check if protected is necessary. + protected HttpClient client; + protected OmCalendar calendar; + protected String path; + protected iCalUtils utils; + + protected AppointmentDao appointmentDao; + + public AbstractCalendarHandler(String path, OmCalendar calendar, HttpClient client, + AppointmentDao appointmentDao, iCalUtils utils) + { + this.path = path; + this.calendar = calendar; + this.client = client; + this.appointmentDao = appointmentDao; + this.utils = utils; + } + + public static Map<String, Appointment> listToMap(List<String> keys, List<Appointment> values) { + Map<String, Appointment> map = new HashMap<>(); + for (int i = 0; i < keys.size(); ++i) { + map.put(keys.get(i), values.get(i)); + } + return map; + } +} Added: openmeetings/application/trunk/openmeetings-service/src/main/java/org/apache/openmeetings/service/calendar/caldav/handler/CalendarHandler.java URL: http://svn.apache.org/viewvc/openmeetings/application/trunk/openmeetings-service/src/main/java/org/apache/openmeetings/service/calendar/caldav/handler/CalendarHandler.java?rev=1763226&view=auto ============================================================================== --- openmeetings/application/trunk/openmeetings-service/src/main/java/org/apache/openmeetings/service/calendar/caldav/handler/CalendarHandler.java (added) +++ openmeetings/application/trunk/openmeetings-service/src/main/java/org/apache/openmeetings/service/calendar/caldav/handler/CalendarHandler.java Tue Oct 4 04:03:01 2016 @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License") + you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.openmeetings.service.calendar.caldav.handler; + +import org.apache.openmeetings.db.entity.calendar.Appointment; +import org.apache.openmeetings.db.entity.calendar.OmCalendar; + +/** + * SyncHandler Interface specifies if the class is Sync Handler. + * The classes which implement this class can be used to + * update items depending on the type of syncing necessary. + * <p> + * This is a helper class to help Syncing of CalDAV calendars. + * <p> + * There are currently three types of Sync Handlers. Namely: + * <li>ETAGs Sync</li> + * <li>CTAGs Sync (Similar to ETAGs but an additional step is necessary)</li> + * <li>WebDAV-Sync Report</li> + * <li>Calendar-Multiget Report</li> + */ +public interface CalendarHandler { + + /** + * Function to update all items in the CalDAV calendar. The owner of the appointments + * created are the same as the owner of the calendar. + * @return Returns the updated calendar after updation of all events. + */ + public OmCalendar syncItems(); + + /** + * Function for create/updating multiple appointment on the server. + * Performs modification alongside of creation new events on the server. + * @param appointment Appointment to create/update. + * @return <code>True</code> when the updation is a success else <code>False</code> + */ + public boolean updateItem(Appointment appointment); + + /** + * Delete Appointment on the server. + * @param appointment Appointment to delete + * @return <code>True</code> when the deletion is a success else <code>False</code> + */ + public boolean deleteItem(Appointment appointment); +} Added: openmeetings/application/trunk/openmeetings-service/src/main/java/org/apache/openmeetings/service/calendar/caldav/handler/CtagHandler.java URL: http://svn.apache.org/viewvc/openmeetings/application/trunk/openmeetings-service/src/main/java/org/apache/openmeetings/service/calendar/caldav/handler/CtagHandler.java?rev=1763226&view=auto ============================================================================== --- openmeetings/application/trunk/openmeetings-service/src/main/java/org/apache/openmeetings/service/calendar/caldav/handler/CtagHandler.java (added) +++ openmeetings/application/trunk/openmeetings-service/src/main/java/org/apache/openmeetings/service/calendar/caldav/handler/CtagHandler.java Tue Oct 4 04:03:01 2016 @@ -0,0 +1,113 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License") + you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.openmeetings.service.calendar.caldav.handler; + +import org.apache.commons.httpclient.HttpClient; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.MultiStatusResponse; +import org.apache.jackrabbit.webdav.property.DavPropertyName; +import org.apache.jackrabbit.webdav.property.DavPropertyNameSet; +import org.apache.jackrabbit.webdav.property.DavPropertySet; +import org.apache.jackrabbit.webdav.xml.Namespace; +import org.apache.openmeetings.db.dao.calendar.AppointmentDao; +import org.apache.openmeetings.db.entity.calendar.Appointment; +import org.apache.openmeetings.db.entity.calendar.OmCalendar; +import org.apache.openmeetings.service.calendar.caldav.AppointmentManager; +import org.apache.openmeetings.service.calendar.caldav.iCalUtils; +import org.osaf.caldav4j.CalDAVConstants; +import org.osaf.caldav4j.methods.PropFindMethod; +import org.red5.logging.Red5LoggerFactory; +import org.slf4j.Logger; + +import java.io.IOException; + +import static org.apache.openmeetings.util.OpenmeetingsVariables.webAppRootKey; +import static org.apache.jackrabbit.webdav.DavServletResponse.SC_OK; + +/** + * Class for Syncing through the help of Ctags. + * It checks if the Ctag of the Calendar has changed. + * If it has then update the events, otherwise leave it as it is. + * + * @see CalendarHandler + */ +public class CtagHandler extends AbstractCalendarHandler { + private static final Logger log = Red5LoggerFactory.getLogger(CtagHandler.class, webAppRootKey); + + public static final Namespace NAMESPACE_CALSERVER = Namespace.getNamespace("cs", "http://calendarserver.org/ns/"); + public static final DavPropertyName DNAME_GETCTAG = DavPropertyName.create("getctag", NAMESPACE_CALSERVER); + + public CtagHandler(String path, OmCalendar calendar, HttpClient client, AppointmentDao appointmentDao, iCalUtils utils) { + super(path, calendar, client, appointmentDao, utils); + } + + @Override + public OmCalendar syncItems() { + //Calendar already inited. + + PropFindMethod propFindMethod = null; + + try { + DavPropertyNameSet properties = new DavPropertyNameSet(); + properties.add(DNAME_GETCTAG); + + propFindMethod = new PropFindMethod(path, properties, CalDAVConstants.DEPTH_0); + client.executeMethod(propFindMethod); + + if (propFindMethod.succeeded()) { + for (MultiStatusResponse response : propFindMethod.getResponseBodyAsMultiStatus().getResponses()) { + DavPropertySet set = response.getProperties(SC_OK); + String ctag = AppointmentManager.getTokenFromProperty(set.get(DNAME_GETCTAG)); + + if (ctag != null && !ctag.equals(calendar.getToken())) { + EtagsHandler etagsHandler = new EtagsHandler(path, calendar, client, appointmentDao, utils); + etagsHandler.syncItems(); + calendar.setToken(ctag); + } + } + } else { + log.error("Error executing PROPFIND Method, with status Code: " + + propFindMethod.getStatusCode()); + } + + } catch (IOException | DavException e) { + log.error("Error during the execution of calendar-multiget Report.", e); + } catch (Exception e) { + log.error("Severe Error during the execution of calendar-multiget Report.", e); + } finally { + if (propFindMethod != null) { + propFindMethod.releaseConnection(); + } + } + + return calendar; + } + + @Override + public boolean updateItem(Appointment appointment) { + EtagsHandler etagsHandler = new EtagsHandler(path, calendar, client, appointmentDao, utils); + return etagsHandler.updateItem(appointment); + } + + @Override + public boolean deleteItem(Appointment appointment) { + EtagsHandler etagsHandler = new EtagsHandler(path, calendar, client, appointmentDao, utils); + return etagsHandler.deleteItem(appointment); + } +} Added: openmeetings/application/trunk/openmeetings-service/src/main/java/org/apache/openmeetings/service/calendar/caldav/handler/EtagsHandler.java URL: http://svn.apache.org/viewvc/openmeetings/application/trunk/openmeetings-service/src/main/java/org/apache/openmeetings/service/calendar/caldav/handler/EtagsHandler.java?rev=1763226&view=auto ============================================================================== --- openmeetings/application/trunk/openmeetings-service/src/main/java/org/apache/openmeetings/service/calendar/caldav/handler/EtagsHandler.java (added) +++ openmeetings/application/trunk/openmeetings-service/src/main/java/org/apache/openmeetings/service/calendar/caldav/handler/EtagsHandler.java Tue Oct 4 04:03:01 2016 @@ -0,0 +1,266 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License") + you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.openmeetings.service.calendar.caldav.handler; + +import net.fortuna.ical4j.data.CalendarOutputter; +import net.fortuna.ical4j.model.Calendar; +import net.fortuna.ical4j.model.Component; + +import org.apache.commons.httpclient.Header; +import org.apache.commons.httpclient.HttpClient; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.MultiStatusResponse; +import org.apache.jackrabbit.webdav.property.DavPropertyName; +import org.apache.jackrabbit.webdav.property.DavPropertyNameSet; +import org.apache.openmeetings.db.dao.calendar.AppointmentDao; +import org.apache.openmeetings.db.entity.calendar.Appointment; +import org.apache.openmeetings.db.entity.calendar.OmCalendar; +import org.apache.openmeetings.db.entity.calendar.OmCalendar.SyncType; +import org.apache.openmeetings.service.calendar.caldav.iCalUtils; +import org.apache.wicket.util.string.Strings; +import org.osaf.caldav4j.CalDAVConstants; +import org.osaf.caldav4j.methods.CalDAVReportMethod; +import org.osaf.caldav4j.methods.DeleteMethod; +import org.osaf.caldav4j.methods.PutMethod; +import org.osaf.caldav4j.model.request.CalendarData; +import org.osaf.caldav4j.model.request.CalendarQuery; +import org.osaf.caldav4j.model.request.CompFilter; +import org.osaf.caldav4j.model.response.CalendarDataProperty; +import org.osaf.caldav4j.util.UrlUtils; +import org.red5.logging.Red5LoggerFactory; +import org.slf4j.Logger; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static org.apache.openmeetings.util.OpenmeetingsVariables.webAppRootKey; +import static org.apache.jackrabbit.webdav.DavServletResponse.SC_OK; +import static org.apache.jackrabbit.webdav.DavServletResponse.SC_CREATED; +import static org.apache.jackrabbit.webdav.DavServletResponse.SC_NO_CONTENT; +import static org.apache.jackrabbit.webdav.DavServletResponse.SC_NOT_FOUND; + +/** + * Class which handles the Syncing through the use of Etags. + * First it sends a Calendar-query, and checks all the etags for events. + * Then, for each href and etag, we get, something like key-value pair. + * <p> + * We then check for three things: + * <li>Addition of a new event.</li> + * <li>Modification of an existing event.</li> + * <li>Deletion of events which are not in the response.</li> + */ +public class EtagsHandler extends AbstractCalendarHandler { + private static final Logger log = Red5LoggerFactory.getLogger(EtagsHandler.class, webAppRootKey); + + public EtagsHandler(String path, OmCalendar calendar, HttpClient client, AppointmentDao appointmentDao, iCalUtils utils) { + super(path, calendar, client, appointmentDao, utils); + } + + @Override + public OmCalendar syncItems() { + Long ownerId = this.calendar.getOwner().getId(); + Map<String, Appointment> map = listToMap(appointmentDao.getHrefsbyCalendar(calendar.getId()), + appointmentDao.getbyCalendar(calendar.getId())); + + DavPropertyNameSet properties = new DavPropertyNameSet(); + properties.add(DavPropertyName.GETETAG); + + CompFilter vcalendar = new CompFilter(Calendar.VCALENDAR); + vcalendar.addCompFilter(new CompFilter(Component.VEVENT)); + + CalDAVReportMethod reportMethod = null; + try { + CalendarQuery query = new CalendarQuery(properties, vcalendar, map.isEmpty() ? new CalendarData() : null, false, false); + reportMethod = new CalDAVReportMethod(path, query, CalDAVConstants.DEPTH_1); + client.executeMethod(reportMethod); + if (reportMethod.succeeded()) { + MultiStatusResponse[] multiStatusResponses = reportMethod.getResponseBodyAsMultiStatus().getResponses(); + if (map.isEmpty()) { + //Initializing the Calendar for the first time. + + //Parse the responses into Appointments + for (MultiStatusResponse response : multiStatusResponses) { + if (response.getStatus()[0].getStatusCode() == SC_OK) { + String etag = CalendarDataProperty.getEtagfromResponse(response); + Calendar ical = CalendarDataProperty.getCalendarfromResponse(response); + Appointment appointments = utils.parseCalendartoAppointment( + ical, response.getHref(), etag, calendar); + + appointmentDao.update(appointments, ownerId); + } + } + } else { + //Calendar has been inited before + List<String> currenthrefs = new ArrayList<String>(); + + for (MultiStatusResponse response : multiStatusResponses) { + if (response.getStatus()[0].getStatusCode() == SC_OK) { + Appointment appointment = map.get(response.getHref()); + + //Event updated + if (appointment != null) { + String origetag = appointment.getEtag(), + currentetag = CalendarDataProperty.getEtagfromResponse(response); + + //If etag is modified + if (!currentetag.equals(origetag)) { + currenthrefs.add(appointment.getHref()); + } + map.remove(response.getHref()); + } else { + // The orig list of events doesn't contain this event. + currenthrefs.add(response.getHref()); + } + } + } + + //Remaining Events have been deleted on the server, thus delete them + for (Map.Entry<String, Appointment> entry : map.entrySet()) { + appointmentDao.delete(entry.getValue(), ownerId); + } + + //Get the rest of the events through a Multiget Handler. + MultigetHandler multigetHandler = new MultigetHandler(currenthrefs, path, + calendar, client, appointmentDao, utils); + return multigetHandler.syncItems(); + } + } else { + log.error("Report Method return Status: {} for calId {} ", reportMethod.getStatusCode(), calendar.getId()); + } + } catch (IOException | DavException e) { + log.error("Error during the execution of calendar-multiget Report.", e); + } catch (Exception e) { + log.error("Severe Error during the execution of calendar-multiget Report.", e); + } finally { + if (reportMethod != null) { + reportMethod.releaseConnection(); + } + } + + return calendar; + } + + @Override + public boolean updateItem(Appointment appointment) { + OmCalendar calendar = appointment.getCalendar(); + String href; + + if (calendar != null && calendar.getSyncType() != SyncType.NONE) { + + //Store new Appointment on the server + PutMethod putMethod = null; + try { + List<String> hrefs = null; + CalendarOutputter calendarOutputter = new CalendarOutputter(); + + Calendar ical = utils.parseAppointmenttoCalendar(appointment); + + putMethod = new PutMethod(); + putMethod.setRequestBody(ical); + putMethod.setCalendarOutputter(calendarOutputter); + + if (Strings.isEmpty(appointment.getHref())) { + String temp = path + appointment.getIcalId() + ".ics"; + temp = UrlUtils.removeDoubleSlashes(temp); + putMethod.setPath(temp); + putMethod.setIfNoneMatch(true); + putMethod.setAllEtags(true); + } else { + putMethod.setPath(appointment.getHref()); + putMethod.setIfMatch(true); + putMethod.addEtag(appointment.getEtag()); + } + + client.executeMethod(putMethod); + + if (putMethod.getStatusCode() == SC_CREATED || + putMethod.getStatusCode() == SC_NO_CONTENT) { + href = putMethod.getPath(); + appointment.setHref(href); + + //Check if the ETag header was returned. + Header etagh = putMethod.getResponseHeader("ETag"); + if (etagh == null) + hrefs = Collections.singletonList(appointment.getHref()); + else { + appointment.setEtag(etagh.getValue()); + appointmentDao.update(appointment, appointment.getOwner().getId()); + } + } else { + //Appointment not created on the server + return false; + } + + //Get new etags for the ones which didn't return an ETag header + MultigetHandler multigetHandler = new MultigetHandler(hrefs, true, path, + calendar, client, appointmentDao, utils); + multigetHandler.syncItems(); + return true; + } catch (IOException e) { + log.error("Error executing OptionsMethod during testConnection.", e); + } catch (Exception e) { + log.error("Severe Error in executing OptionsMethod during testConnection.", e); + } finally { + if (putMethod != null) { + putMethod.releaseConnection(); + } + } + } + + return false; + } + + /** + * @see CalendarHandler#deleteItem(Appointment) + */ + @Override + public boolean deleteItem(Appointment appointment) { + + if (calendar != null && calendar.getSyncType() != SyncType.NONE && !Strings.isEmpty(appointment.getHref())) { + DeleteMethod deleteMethod = null; + try { + deleteMethod = new DeleteMethod(appointment.getHref(), appointment.getEtag()); + + log.info("Deleting at location: {} with ETag: {}", appointment.getHref(), appointment.getEtag()); + + client.executeMethod(deleteMethod); + + int status = deleteMethod.getStatusCode(); + if (status == SC_NO_CONTENT || status == SC_OK || status == SC_NOT_FOUND) { + log.info("Successfully deleted appointment with id: " + appointment.getId()); + return true; + } else { + // Appointment Not deleted + } + } catch (IOException e) { + log.error("Error executing OptionsMethod during testConnection.", e); + } catch (Exception e) { + log.error("Severe Error in executing OptionsMethod during testConnection.", e); + } finally { + if (deleteMethod != null) { + deleteMethod.releaseConnection(); + } + } + } + return false; + } +} Added: openmeetings/application/trunk/openmeetings-service/src/main/java/org/apache/openmeetings/service/calendar/caldav/handler/MultigetHandler.java URL: http://svn.apache.org/viewvc/openmeetings/application/trunk/openmeetings-service/src/main/java/org/apache/openmeetings/service/calendar/caldav/handler/MultigetHandler.java?rev=1763226&view=auto ============================================================================== --- openmeetings/application/trunk/openmeetings-service/src/main/java/org/apache/openmeetings/service/calendar/caldav/handler/MultigetHandler.java (added) +++ openmeetings/application/trunk/openmeetings-service/src/main/java/org/apache/openmeetings/service/calendar/caldav/handler/MultigetHandler.java Tue Oct 4 04:03:01 2016 @@ -0,0 +1,166 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License") + you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.openmeetings.service.calendar.caldav.handler; + +import net.fortuna.ical4j.model.Calendar; +import net.fortuna.ical4j.model.Component; +import org.apache.commons.httpclient.HttpClient; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.MultiStatusResponse; +import org.apache.jackrabbit.webdav.property.DavPropertyName; +import org.apache.jackrabbit.webdav.property.DavPropertyNameSet; +import org.apache.openmeetings.db.dao.calendar.AppointmentDao; +import org.apache.openmeetings.db.entity.calendar.Appointment; +import org.apache.openmeetings.db.entity.calendar.OmCalendar; +import org.apache.openmeetings.db.entity.calendar.OmCalendar.SyncType; +import org.apache.openmeetings.service.calendar.caldav.iCalUtils; +import org.osaf.caldav4j.CalDAVConstants; +import org.osaf.caldav4j.methods.CalDAVReportMethod; +import org.osaf.caldav4j.model.request.CalendarData; +import org.osaf.caldav4j.model.request.CalendarMultiget; +import org.osaf.caldav4j.model.request.CompFilter; +import org.osaf.caldav4j.model.response.CalendarDataProperty; +import org.red5.logging.Red5LoggerFactory; +import org.slf4j.Logger; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import static org.apache.jackrabbit.webdav.DavServletResponse.SC_OK; +import static org.apache.openmeetings.util.OpenmeetingsVariables.webAppRootKey; + +/** + * Class used to sync a given list of hrefs and update or add new Appointments, whenever feasible. + * This class cannot be used to update or delete Appointments, which are handled seperately. + * We use the Calendar-Multiget Report Method to handle this type of query. + * + * @see CalendarHandler + */ +public class MultigetHandler extends AbstractCalendarHandler { + private static final Logger log = Red5LoggerFactory.getLogger(MultigetHandler.class, webAppRootKey); + + private CalendarMultiget query; + private boolean isMultigetDisabled = false, onlyEtag = false; + + public MultigetHandler(List<String> hrefs, boolean onlyEtag, String path, OmCalendar calendar, HttpClient client, + AppointmentDao appointmentDao, iCalUtils utils) { + super(path, calendar, client, appointmentDao, utils); + this.onlyEtag = onlyEtag; + + if (hrefs == null || hrefs.isEmpty() || calendar.getSyncType() == SyncType.NONE) { + isMultigetDisabled = true; + } else { + DavPropertyNameSet properties = new DavPropertyNameSet(); + properties.add(DavPropertyName.GETETAG); + + CalendarData calendarData = null; + if (!onlyEtag) { + calendarData = new CalendarData(); + } + CompFilter vcalendar = new CompFilter(Calendar.VCALENDAR); + vcalendar.addCompFilter(new CompFilter(Component.VEVENT)); + query = new CalendarMultiget(properties, calendarData, false, false); + query.setHrefs(hrefs); + } + } + + public MultigetHandler(List<String> hrefs, String path, OmCalendar calendar, HttpClient client, + AppointmentDao appointmentDao, iCalUtils utils) + { + this(hrefs, false, path, calendar, client, appointmentDao, utils); + } + + @Override + public OmCalendar syncItems() { + Long ownerId = this.calendar.getOwner().getId(); + if (!isMultigetDisabled) { + + CalDAVReportMethod reportMethod = null; + + try { + reportMethod = new CalDAVReportMethod(path, query, CalDAVConstants.DEPTH_1); + + client.executeMethod(reportMethod); + + if (reportMethod.succeeded()) { + //Map for each Href as key and Appointment as Value. + Map<String, Appointment> map = listToMap(appointmentDao.getHrefsbyCalendar(calendar.getId()), + appointmentDao.getbyCalendar(calendar.getId())); + + for (MultiStatusResponse response : reportMethod.getResponseBodyAsMultiStatus().getResponses()) { + if (response.getStatus()[0].getStatusCode() == SC_OK) { + Appointment a = map.get(response.getHref()); + + //Check if it's an updated Appointment + if (a != null) { + String origetag = a.getEtag(), + currentetag = CalendarDataProperty.getEtagfromResponse(response); + + //If etag is modified + if (!currentetag.equals(origetag)) { + if (onlyEtag) { + a.setEtag(currentetag); + } else { + Calendar calendar = CalendarDataProperty.getCalendarfromResponse(response); + a = utils.parseCalendartoAppointment(a, calendar, currentetag); + } + appointmentDao.update(a, ownerId); + } + } else if (!onlyEtag) { + //Else it's a new Appointment + // i.e. parse into a new Appointment + // Only applicable when we get calendar data along with etag. + String etag = CalendarDataProperty.getEtagfromResponse(response); + Calendar ical = CalendarDataProperty.getCalendarfromResponse(response); + Appointment appointments = utils.parseCalendartoAppointment( + ical, response.getHref(), etag, calendar); + appointmentDao.update(appointments, ownerId); + } + } + } + } else { + log.error("Report Method return Status: {} for calId {}", reportMethod.getStatusCode(), calendar.getId()); + } + } catch (IOException | DavException e) { + log.error("Error during the execution of calendar-multiget Report.", e); + } catch (Exception e) { + log.error("Severe Error during the execution of calendar-multiget Report.", e); + } finally { + if (reportMethod != null) { + reportMethod.releaseConnection(); + } + } + } + + return calendar; + } + + // Doesn't handle Creation, Updation and Deletion of events. + // Don't call these for MultigetHandler. + @Override + public boolean updateItem(Appointment appointment) { + return false; + } + + @Override + public boolean deleteItem(Appointment appointment) { + return false; + } +} Added: openmeetings/application/trunk/openmeetings-service/src/main/java/org/apache/openmeetings/service/calendar/caldav/handler/WebDAVSyncHandler.java URL: http://svn.apache.org/viewvc/openmeetings/application/trunk/openmeetings-service/src/main/java/org/apache/openmeetings/service/calendar/caldav/handler/WebDAVSyncHandler.java?rev=1763226&view=auto ============================================================================== --- openmeetings/application/trunk/openmeetings-service/src/main/java/org/apache/openmeetings/service/calendar/caldav/handler/WebDAVSyncHandler.java (added) +++ openmeetings/application/trunk/openmeetings-service/src/main/java/org/apache/openmeetings/service/calendar/caldav/handler/WebDAVSyncHandler.java Tue Oct 4 04:03:01 2016 @@ -0,0 +1,161 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License") + you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.openmeetings.service.calendar.caldav.handler; + + +import org.apache.commons.httpclient.HttpClient; +import org.apache.jackrabbit.webdav.MultiStatusResponse; +import org.apache.jackrabbit.webdav.property.DavPropertyName; +import org.apache.jackrabbit.webdav.property.DavPropertyNameSet; +import org.apache.openmeetings.db.dao.calendar.AppointmentDao; +import org.apache.openmeetings.db.entity.calendar.Appointment; +import org.apache.openmeetings.db.entity.calendar.OmCalendar; +import org.apache.openmeetings.service.calendar.caldav.iCalUtils; +import org.apache.openmeetings.service.calendar.caldav.methods.SyncMethod; +import org.apache.openmeetings.service.calendar.caldav.methods.SyncReportInfo; +import org.osaf.caldav4j.model.response.CalendarDataProperty; +import org.red5.logging.Red5LoggerFactory; +import org.slf4j.Logger; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static org.apache.jackrabbit.webdav.DavServletResponse.SC_INSUFFICIENT_SPACE_ON_RESOURCE; +import static org.apache.jackrabbit.webdav.DavServletResponse.SC_OK; +import static org.apache.jackrabbit.webdav.DavServletResponse.SC_FORBIDDEN; +import static org.apache.jackrabbit.webdav.DavServletResponse.SC_PRECONDITION_FAILED; +import static org.apache.jackrabbit.webdav.DavServletResponse.SC_NOT_FOUND; +import static org.apache.openmeetings.util.OpenmeetingsVariables.webAppRootKey; + +/** + * Class used to sync events using WebDAV-Sync defined in RFC 6578. + * This handles the additional HTTP Status Code 507, which specifies for further sync required. + * For syncing, it gets a Sync Report as response, which specifies which files have been added, + * modified or deleted. + */ +public class WebDAVSyncHandler extends AbstractCalendarHandler { + private static final Logger log = Red5LoggerFactory.getLogger(WebDAVSyncHandler.class, webAppRootKey); + + public static final DavPropertyName DNAME_SYNCTOKEN = DavPropertyName.create(SyncReportInfo.XML_SYNC_TOKEN, + SyncReportInfo.NAMESPACE); + + public WebDAVSyncHandler(String path, OmCalendar calendar, HttpClient client, AppointmentDao appointmentDao, iCalUtils utils) { + super(path, calendar, client, appointmentDao, utils); + } + + @Override + public OmCalendar syncItems() { + boolean additionalSyncNeeded = false; + + SyncMethod syncMethod = null; + + try { + DavPropertyNameSet properties = new DavPropertyNameSet(); + properties.add(DavPropertyName.GETETAG); + + //Create report to get + SyncReportInfo reportInfo = new SyncReportInfo(calendar.getToken(), properties, + SyncReportInfo.SYNC_LEVEL_1); + syncMethod = new SyncMethod(path, reportInfo); + client.executeMethod(syncMethod); + + if (syncMethod.succeeded()) { + List<String> currenthrefs = new ArrayList<>(); + + //Map of Href and the Appointments, belonging to it. + Map<String, Appointment> map = listToMap(appointmentDao.getHrefsbyCalendar(calendar.getId()), + appointmentDao.getbyCalendar(calendar.getId())); + + for (MultiStatusResponse response : syncMethod.getResponseBodyAsMultiStatus().getResponses()) { + int status = response.getStatus()[0].getStatusCode(); + if (status == SC_OK) { + Appointment a = map.get(response.getHref()); + + if (a != null) { + //Old Event to get + String origetag = a.getEtag(), + currentetag = CalendarDataProperty.getEtagfromResponse(response); + + //If event modified, only then get it. + if (!currentetag.equals(origetag)) { + currenthrefs.add(response.getHref()); + } + } else { + //New Event, to get + currenthrefs.add(response.getHref()); + } + } else if (status == SC_NOT_FOUND) { + //Delete the Appointments not found on the server. + Appointment a = map.get(response.getHref()); + + //Only if the event exists on the database, delete it. + if (a != null) { + appointmentDao.delete(a, calendar.getOwner().getId()); + } + } else if (status == SC_INSUFFICIENT_SPACE_ON_RESOURCE) { + additionalSyncNeeded = true; + } + } + + + MultigetHandler multigetHandler = new MultigetHandler(currenthrefs, path, + calendar, client, appointmentDao, utils); + multigetHandler.syncItems(); + + //Set the new token + calendar.setToken(syncMethod.getResponseSynctoken()); + } else if (syncMethod.getStatusCode() == SC_FORBIDDEN || + syncMethod.getStatusCode() == SC_PRECONDITION_FAILED) { + + //Specific case where a server might sometimes forget the sync token + //Thus requiring a full sync needed to be done. + log.info("Sync Token not accepted by server. Doing a full sync again."); + calendar.setToken(null); + additionalSyncNeeded = true; + } else { + log.error("Error in Sync Method Response with status code {}", syncMethod.getStatusCode()); + } + + } catch (IOException e) { + log.error("Error while executing the SyncMethod Report.", e); + } catch (Exception e) { + log.error("Severe Error while executing the SyncMethod Report.", e); + } finally { + if (syncMethod != null) { + syncMethod.releaseConnection(); + } + } + + return additionalSyncNeeded ? syncItems() : calendar; + } + + @Override + public boolean updateItem(Appointment appointment) { + EtagsHandler etagsHandler = new EtagsHandler(path, calendar, client, appointmentDao, utils); + return etagsHandler.updateItem(appointment); + } + + @Override + public boolean deleteItem(Appointment appointment) { + EtagsHandler etagsHandler = new EtagsHandler(path, calendar, client, appointmentDao, utils); + return etagsHandler.deleteItem(appointment); + } +} Added: openmeetings/application/trunk/openmeetings-service/src/main/java/org/apache/openmeetings/service/calendar/caldav/iCalUtils.java URL: http://svn.apache.org/viewvc/openmeetings/application/trunk/openmeetings-service/src/main/java/org/apache/openmeetings/service/calendar/caldav/iCalUtils.java?rev=1763226&view=auto ============================================================================== --- openmeetings/application/trunk/openmeetings-service/src/main/java/org/apache/openmeetings/service/calendar/caldav/iCalUtils.java (added) +++ openmeetings/application/trunk/openmeetings-service/src/main/java/org/apache/openmeetings/service/calendar/caldav/iCalUtils.java Tue Oct 4 04:03:01 2016 @@ -0,0 +1,479 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License") + you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.openmeetings.service.calendar.caldav; + +import net.fortuna.ical4j.model.Calendar; +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.Cn; +import net.fortuna.ical4j.model.parameter.Role; +import net.fortuna.ical4j.model.property.*; +import org.apache.commons.lang3.time.FastDateFormat; +import org.apache.openmeetings.db.dao.user.UserDao; +import org.apache.openmeetings.db.entity.calendar.Appointment; +import org.apache.openmeetings.db.entity.calendar.MeetingMember; +import org.apache.openmeetings.db.entity.calendar.OmCalendar; +import org.apache.openmeetings.db.entity.room.Room; +import org.apache.openmeetings.db.entity.user.User; +import org.apache.openmeetings.db.util.TimezoneUtil; +import org.apache.wicket.protocol.http.WebSession; +import org.red5.logging.Red5LoggerFactory; +import org.slf4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; + +import java.net.URI; +import java.text.ParsePosition; +import java.util.*; +import java.util.Date; +import java.util.TimeZone; + +import static org.apache.openmeetings.util.OpenmeetingsVariables.webAppRootKey; + +/** + * Class which provides iCalendar Utilities. + * This class's functions could be made static, as they are not instantiated anyway. + */ +public class iCalUtils { + private static final Logger log = Red5LoggerFactory.getLogger(iCalUtils.class, webAppRootKey); + public static final String PROD_ID = "-//Events Calendar//Apache Openmeetings//EN"; + + @Autowired + private TimezoneUtil timezoneUtil; + @Autowired + private UserDao userDao; + + /** + * Parses the Calendar from the CalDAV server, to a new Appointment. + * + * @param calendar iCalendar Representation. + * @param href Location of the Calendar on the server + * @param etag ETag of the calendar. + * @param omCalendar The Parent OmCalendar, to which the Appointment belongs. + * @return Appointment after parsing. + */ + public Appointment parseCalendartoAppointment(Calendar calendar, String href, String etag, OmCalendar omCalendar) { + //Note: By RFC 4791 only one event can be stored in one href. + + Appointment a = new Appointment(); + a.setId(null); + a.setDeleted(false); + a.setHref(href); + a.setCalendar(omCalendar); + a.setOwner(omCalendar.getOwner()); + a.setRoom(createDefaultRoom()); + a.setReminder(Appointment.Reminder.none); + + return this.parseCalendartoAppointment(a, calendar, etag); + } + + /** + * Parses a Calendar with multiple VEvents into Appointments + * + * @param calendar Calendar to Parse + * @param ownerId Owner of the Appointments + * @return <code>List</code> of Appointments + */ + public List<Appointment> parseCalendartoAppointments(Calendar calendar, Long ownerId) { + List<Appointment> appointments = new ArrayList<>(); + ComponentList<CalendarComponent> events = calendar.getComponents(Component.VEVENT); + User owner = userDao.get(ownerId); + TimeZone tz = parseTimeZone(calendar, owner); + + for (CalendarComponent event : events) { + Appointment a = new Appointment(); + a.setOwner(owner); + a.setDeleted(false); + a.setRoom(createDefaultRoom()); + a.setReminder(Appointment.Reminder.none); + a = addVEventPropertiestoAppointment(a, event, tz); + appointments.add(a); + } + return appointments; + } + + /** + * Updating Appointments which already exist, by parsing the Calendar. And updating etag. + * Doesn't work with complex Recurrences. + * Note: Hasn't been tested to acknowledge DST, timezones should acknowledge this. + * + * @param a Appointment to be updated. + * @param calendar iCalendar Representation. + * @param etag The ETag of the calendar. + * @return Updated Appointment. + */ + public Appointment parseCalendartoAppointment(Appointment a, Calendar calendar, String etag) { + if (calendar == null) return a; + CalendarComponent event = calendar.getComponent(Component.VEVENT); + if (event != null) { + TimeZone tz = parseTimeZone(calendar, a.getOwner()); + + a.setEtag(etag); + a = addVEventPropertiestoAppointment(a, event, tz); + } + return a; + } + + /** + * Add properties from the Given VEvent Component to the Appointment + * + * @param a Appointment to which the properties are to be added + * @param event VEvent to parse properties from. + * @param tz Timezone of the Event + * @return Updated Appointment + */ + private Appointment addVEventPropertiestoAppointment(Appointment a, CalendarComponent event, TimeZone tz) { + Property dtstart = event.getProperty(Property.DTSTART), + dtend = event.getProperty(Property.DTEND), + uid = event.getProperty(Property.UID), + dtstamp = event.getProperty(Property.DTSTAMP), + description = event.getProperty(Property.DESCRIPTION), + summary = event.getProperty(Property.SUMMARY), + location = event.getProperty(Property.LOCATION), + lastmod = event.getProperty(Property.LAST_MODIFIED), + organizer = event.getProperty(Property.ORGANIZER), + recur = event.getProperty(Property.RRULE); + PropertyList attendees = event.getProperties(Property.ATTENDEE); + + if (uid != null) { + a.setIcalId(uid.getValue()); + } + + Date d = parseDate(dtstart, tz); + a.setStart(d); + if (dtend == null) { + a.setEnd(addTimetoDate(d, java.util.Calendar.HOUR_OF_DAY, 1)); + } else { + a.setEnd(parseDate(dtend, tz)); + } + + a.setInserted(parseDate(dtstamp, tz)); + + if (lastmod != null) { + a.setUpdated(parseDate(lastmod, tz)); + } + + if (description != null) { + a.setDescription(description.getValue()); + } + + if (summary != null) { + a.setTitle(summary.getValue()); + } + + if (location != null) { + a.setLocation(location.getValue()); + } + + if (recur != null) { + Parameter freq = recur.getParameter("FREQ"); + if (freq != null) { + if (freq.getValue().equals(Recur.DAILY)) { + a.setIsDaily(true); + } else if (freq.getValue().equals(Recur.WEEKLY)) { + a.setIsWeekly(true); + } else if (freq.getValue().equals(Recur.MONTHLY)) { + a.setIsMonthly(true); + } else if (freq.getValue().equals(Recur.YEARLY)) { + a.setIsYearly(true); + } + } + } + + List<MeetingMember> attList = a.getMeetingMembers() == null ? new ArrayList<>() : a.getMeetingMembers(); + + //Note this value can be repeated in attendees as well. + if (organizer != null) { + URI uri = URI.create(organizer.getValue()); + + //If the value of the organizer is an email + if (uri.getScheme().equals("mailto")) { + String email = uri.getSchemeSpecificPart(); + //Contact or exist and owner + User org = userDao.getByEmail(email); + if (org == null) { + org = userDao.getContact(email, a.getOwner()); + attList.add(createMeetingMember(a, org)); + } else if (org.getId() != a.getOwner().getId()) { + attList.add(createMeetingMember(a, org)); + } + } + } + + if (attendees != null && !attendees.isEmpty()) { + for (Property attendee : attendees) { + URI uri = URI.create(attendee.getValue()); + if (uri.getScheme().equals("mailto")) { + String email = uri.getSchemeSpecificPart(); + User u = userDao.getByEmail(email); + if (u == null) { + u = userDao.getContact(email, a.getOwner()); + } + attList.add(createMeetingMember(a, u)); + } + } + } + + a.setMeetingMembers(attList.isEmpty() ? null : attList); + + return a; + } + + private static MeetingMember createMeetingMember(Appointment a, User u) { + MeetingMember mm = new MeetingMember(); + mm.setUser(u); + mm.setDeleted(false); + mm.setInserted(a.getInserted()); + mm.setUpdated(a.getUpdated()); + mm.setAppointment(a); + return mm; + } + + private static Room createDefaultRoom() { + Room r = new Room(); + r.setAppointment(true); + if (r.getType() == null) { + r.setType(Room.Type.conference); + } + return r; + } + + /** + * Parses the VTimezone Component of the given Calendar. If no, VTimezone component is found the + * User Timezone is used + * + * @param calendar Calendar to parse + * @param owner Owner of the Calendar + * @return Parsed TimeZone + */ + public TimeZone parseTimeZone(Calendar calendar, User owner) { + if (calendar != null) { + Component timezone = calendar.getComponent(Component.VTIMEZONE); + if (timezone != null) { + Property tzid = timezone.getProperty(Property.TZID); + if (tzid != null) { + return timezoneUtil.getTimeZone(tzid.getValue()); + } + } + } + return timezoneUtil.getTimeZone(owner); + } + + /** + * Convenience function to parse date from {@link net.fortuna.ical4j.model.Property} to + * {@link Date} + * + * @param dt DATE-TIME Property from which we parse. + * @param timeZone Timezone of the Date. + * @return {@link java.util.Date} representation of the iCalendar value. + */ + public Date parseDate(Property dt, TimeZone timeZone) { + if (dt == null || dt.getValue().equals("")) { + return null; + } + + String[] acceptedFormats = {"yyyyMMdd'T'HHmmss", "yyyyMMdd'T'HHmmss'Z'", "yyyyMMdd"}; + Parameter tzid = dt.getParameter(Parameter.TZID); + if (tzid == null) { + return parseDate(dt.getValue(), acceptedFormats, timeZone); + } else { + return parseDate(dt.getValue(), acceptedFormats, timezoneUtil.getTimeZone(tzid.getValue())); + } + } + + /** + * Adapted from DateUtils to support Timezones, and parse ical dates into {@link java.util.Date}. + * Note: Replace FastDateFormat to java.time, when shifting to Java 8 or higher. + * + * @param str Date representation in String. + * @param patterns Patterns to parse the date against + * @param timeZone Timezone of the Date. + * @return <code>java.util.Date</code> representation of string or + * <code>null</code> if the Date could not be parsed. + */ + public Date parseDate(String str, String[] patterns, TimeZone timeZone) { + FastDateFormat parser = null; + Locale locale = WebSession.get().getLocale(); + + if (str.endsWith("Z")) { + timeZone = TimeZone.getTimeZone("UTC"); + } + + ParsePosition pos = new ParsePosition(0); + for (String pattern : patterns) { + parser = FastDateFormat.getInstance(pattern, timeZone, locale); + pos.setIndex(0); + Date date = parser.parse(str, pos); + if (date != null && pos.getIndex() == str.length()) { + return date; + } + } + log.error("Unable to parse the date: " + str + " at " + -1); + return null; + } + + /** + * Adds a specified amount of time to a Date. + * + * @param date Date to which time is added + * @param field Date Field to which the Amount is added + * @param amount Amount to be Added + * @return New Date + */ + public Date addTimetoDate(Date date, int field, int amount) { + java.util.Calendar c = java.util.Calendar.getInstance(); + c.setTime(date); + c.add(field, amount); + return c.getTime(); + } + + /** + * Methods to parse Appointment to iCalendar according RFC 2445 + * + * @param appointment to be converted to iCalendar + * @return iCalendar representation of the Appointment + * @throws Exception + */ + public Calendar parseAppointmenttoCalendar(Appointment appointment) throws Exception { + String tzid = parseTimeZone(null, appointment.getOwner()).getID(); + + TimeZoneRegistry registry = TimeZoneRegistryFactory.getInstance().createRegistry(); + + net.fortuna.ical4j.model.TimeZone timeZone = registry.getTimeZone(tzid); + if (timeZone == null) { + throw new Exception("Unable to get time zone by id provided: " + tzid); + } + + Calendar icsCalendar = new Calendar(); + icsCalendar.getProperties().add(new ProdId("-//Events Calendar//Apache Openmeetings//EN")); + icsCalendar.getProperties().add(Version.VERSION_2_0); + icsCalendar.getProperties().add(CalScale.GREGORIAN); + icsCalendar.getComponents().add(timeZone.getVTimeZone()); + + DateTime start = new DateTime(appointment.getStart()), end = new DateTime(appointment.getEnd()); + + VEvent meeting = new VEvent(start, end, appointment.getTitle()); + meeting = addVEventpropsfromAppointment(appointment, meeting); + icsCalendar.getComponents().add(meeting); + + return icsCalendar; + } + + /** + * Adds the Appointment Properties to the given VEvent + * + * @param appointment Appointment whose properties are taken + * @param meeting VEvent of the Appointment + * @return Updated VEvent + */ + private static VEvent addVEventpropsfromAppointment(Appointment appointment, VEvent meeting) { + + if (appointment.getLocation() != null) { + meeting.getProperties().add(new Location(appointment.getLocation())); + } + + meeting.getProperties().add(new Description(appointment.getDescription())); + meeting.getProperties().add(new Sequence(0)); + meeting.getProperties().add(Transp.OPAQUE); + + String uid = appointment.getIcalId(); + Uid ui = null; + if (uid == null || uid.length() < 1) { + UUID uuid = UUID.randomUUID(); + appointment.setIcalId(uuid.toString()); + ui = new Uid(uuid.toString()); + } else { + ui = new Uid(uid); + } + + meeting.getProperties().add(ui); + + /* RRule rRule = null; + if (appointment.getIsDaily() != null) { + rRule = new RRule("FREQ=DAILY"); + } else if (appointment.getIsWeekly() != null) { + rRule = new RRule("FREQ=WEEKLY"); + } else if (appointment.getIsMonthly() != null) { + rRule = new RRule("FREQ=MONTHLY"); + } else if (appointment.getIsYearly() != null) { + rRule = new RRule("FREQ=YEARLY"); + } + + if (rRule != null) { + meeting.getProperties().add(rRule); + } + */ + + if (appointment.getMeetingMembers() != null) { + for (MeetingMember meetingMember : appointment.getMeetingMembers()) { + Attendee attendee = new Attendee(URI.create("mailto:" + + meetingMember.getUser().getAddress().getEmail())); + attendee.getParameters().add(Role.REQ_PARTICIPANT); + attendee.getParameters().add(new Cn(meetingMember.getUser().getLogin())); + meeting.getProperties().add(attendee); + } + } + URI orgUri = URI.create("mailto:" + appointment.getOwner().getAddress().getEmail()); + Attendee orgAtt = new Attendee(orgUri); + orgAtt.getParameters().add(Role.CHAIR); + Cn orgCn = new Cn(appointment.getOwner().getLogin()); + orgAtt.getParameters().add(orgCn); + meeting.getProperties().add(orgAtt); + + Organizer organizer = new Organizer(orgUri); + organizer.getParameters().add(orgCn); + meeting.getProperties().add(organizer); + + return meeting; + } + + /** + * Parses a List of Appointments into a VCALENDAR component. + * + * @param appointments List of Appointments for the Calendar + * @param ownerId Owner of the Appointments + * @return VCALENDAR representation of the Appointments + * @throws Exception + */ + public Calendar parseAppointmentstoCalendar(List<Appointment> appointments, Long ownerId) throws Exception { + String tzid = parseTimeZone(null, userDao.get(ownerId)).getID(); + + TimeZoneRegistry registry = TimeZoneRegistryFactory.getInstance().createRegistry(); + + net.fortuna.ical4j.model.TimeZone timeZone = registry.getTimeZone(tzid); + if (timeZone == null) { + throw new Exception("Unable to get time zone by id provided: " + tzid); + } + + Calendar icsCalendar = new Calendar(); + icsCalendar.getProperties().add(new ProdId(PROD_ID)); + icsCalendar.getProperties().add(Version.VERSION_2_0); + icsCalendar.getProperties().add(CalScale.GREGORIAN); + icsCalendar.getComponents().add(timeZone.getVTimeZone()); + + for (Appointment appointment : appointments) { + DateTime start = new DateTime(appointment.getStart()), end = new DateTime(appointment.getEnd()); + + VEvent meeting = new VEvent(start, end, appointment.getTitle()); + meeting = addVEventpropsfromAppointment(appointment, meeting); + icsCalendar.getComponents().add(meeting); + } + return icsCalendar; + } +}
