| /* |
| * 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 java.io.IOException; |
| import java.net.URI; |
| import java.util.Collection; |
| import java.util.List; |
| import java.util.concurrent.TimeUnit; |
| |
| import javax.annotation.PreDestroy; |
| |
| import org.apache.http.HttpResponse; |
| import org.apache.http.auth.AuthScope; |
| import org.apache.http.auth.Credentials; |
| import org.apache.http.client.HttpClient; |
| import org.apache.http.client.methods.HttpOptions; |
| import org.apache.http.client.protocol.HttpClientContext; |
| import org.apache.http.impl.client.HttpClients; |
| import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; |
| import org.apache.jackrabbit.webdav.DavConstants; |
| import org.apache.jackrabbit.webdav.MultiStatusResponse; |
| import org.apache.jackrabbit.webdav.client.methods.HttpPropfind; |
| 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.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| import org.springframework.beans.factory.annotation.Autowired; |
| import org.springframework.stereotype.Component; |
| import org.w3c.dom.Element; |
| |
| import com.github.caldav4j.CalDAVConstants; |
| |
| /** |
| * Class which does syncing and provides respective API's required for performing CalDAV Operations. |
| * @author Ankush Mishra (ankushmishra9@gmail.com) |
| */ |
| @Component |
| public class AppointmentManager { |
| private static final Logger log = LoggerFactory.getLogger(AppointmentManager.class); |
| |
| //HttpClient and ConnectionManager Params |
| private static final int IDLE_CONNECTION_TIMEOUT = 30; // 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 PoolingHttpClientConnectionManager 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 PoolingHttpClientConnectionManager(); |
| connmanager.setDefaultMaxPerRoute(MAX_HOST_CONNECTIONS); |
| connmanager.setMaxTotal(MAX_TOTAL_CONNECTIONS); |
| } |
| |
| return HttpClients.custom() |
| .setConnectionManager(connmanager) |
| .build(); |
| } |
| |
| /** |
| * 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 context Context of the Client which makes the connection. |
| * @param calendar Calendar whose Host the Credentials are for. |
| * @param credentials Credentials to add |
| */ |
| public void provideCredentials(HttpClientContext context, OmCalendar calendar, Credentials credentials) { |
| // Done through creating a new Local context |
| if (!Strings.isEmpty(calendar.getHref()) && credentials != null) { |
| URI temp = URI.create(calendar.getHref()); |
| context.getCredentialsProvider().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 context http context |
| * @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, HttpClientContext context, OmCalendar calendar) { |
| cleanupIdleConnections(); |
| |
| HttpOptions optionsMethod = null; |
| try { |
| String path = calendar.getHref(); |
| optionsMethod = new HttpOptions(path); |
| optionsMethod.setHeader("Accept", "*/*"); |
| HttpResponse response = client.execute(optionsMethod, context); |
| int status = response.getStatusLine().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.reset(); |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Create or Update calendar on the database. |
| * |
| * @param client - {@link HttpClient} to discover calendar |
| * @param context http context |
| * @param calendar - calendar to be created |
| * @return <code>true</code> if calendar was created/updated |
| */ |
| public boolean createCalendar(HttpClient client, HttpClientContext context, OmCalendar calendar) { |
| if (calendar.getId() == null && calendar.getSyncType() != SyncType.GOOGLE_CALENDAR) { |
| return discoverCalendars(client, context, 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(); |
| } |
| |
| /** |
| * Method to get user's calendars |
| * please see {@link OmCalendarDao#getByUser(Long)} |
| * |
| * @param userid - id of the user |
| * @return the list of the calendars |
| */ |
| public List<OmCalendar> getCalendars(Long userid) { |
| return calendarDao.getByUser(userid); |
| } |
| |
| /** |
| * Method to get user's google calendars |
| * please see {@link OmCalendarDao#getGoogleCalendars(Long)} |
| * |
| * @param userId - id of the user |
| * @return the list of the calendars |
| */ |
| public List<OmCalendar> getGoogleCalendars(Long userId) { |
| return calendarDao.getGoogleCalendars(userId); |
| } |
| |
| /** |
| * Function which when called performs syncing based on the type of Syncing detected. |
| * |
| * @param client - {@link HttpClient} to discover calendar |
| * @param context http context |
| * @param calendar Calendar who's sync has to take place |
| */ |
| public void syncItem(HttpClient client, HttpClientContext context, OmCalendar calendar) { |
| cleanupIdleConnections(); |
| |
| if (calendar.getSyncType() != SyncType.NONE) { |
| CalendarHandler calendarHandler; |
| String path = calendar.getHref(); |
| |
| switch (calendar.getSyncType()) { |
| case WEBDAV_SYNC: |
| calendarHandler = new WebDAVSyncHandler(path, calendar, client, context, appointmentDao, utils); |
| break; |
| case CTAG: |
| calendarHandler = new CtagHandler(path, calendar, client, context, appointmentDao, utils); |
| break; |
| case ETAG: |
| default: //Default is the EtagsHandler. |
| calendarHandler = new EtagsHandler(path, calendar, client, context, appointmentDao, utils); |
| break; |
| } |
| |
| calendarHandler.syncItems(); |
| calendarDao.update(calendar); |
| } |
| } |
| |
| /** |
| * Syncs all the calendars currrently present on the DB. |
| * |
| * @param client - {@link HttpClient} to discover calendar |
| * @param context http context |
| * @param userId - id of the user |
| */ |
| public void syncItems(HttpClient client, HttpClientContext context, Long userId) { |
| List<OmCalendar> calendars = getCalendars(userId); |
| for (OmCalendar calendar : calendars) { |
| syncItem(client, context, calendar); |
| } |
| } |
| |
| /** |
| * Function which finds all the calendars of the Principal URL of the calendar |
| * |
| * @param client - {@link HttpClient} to discover calendar |
| * @param context http context |
| * @param calendar - calendar to get principal URL from |
| * @return - <code>true</code> in case calendar was discovered successfully |
| */ |
| private boolean discoverCalendars(HttpClient client, HttpClientContext context, OmCalendar calendar) { |
| cleanupIdleConnections(); |
| |
| if (calendar.getSyncType() != SyncType.NONE) { |
| return false; |
| } |
| HttpPropfind 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 = calendar.getHref(); |
| |
| DavPropertyNameSet properties = new DavPropertyNameSet(); |
| properties.add(curUserPrincipal); |
| properties.add(calHomeSet); |
| properties.add(DavPropertyName.RESOURCETYPE); |
| |
| propFindMethod = new HttpPropfind(path, properties, CalDAVConstants.DEPTH_0); |
| HttpResponse httpResponse = client.execute(propFindMethod, context); |
| |
| if (!propFindMethod.succeeded(httpResponse)) { |
| return false; |
| } |
| for (MultiStatusResponse response : propFindMethod.getResponseBodyAsMultiStatus(httpResponse).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, context, 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; |
| } |
| } |
| |
| 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 HttpPropfind(userPath, props, DavConstants.DEPTH_0); |
| httpResponse = client.execute(propFindMethod, context); |
| |
| if (!propFindMethod.succeeded(httpResponse)) { |
| return false; |
| } |
| for (MultiStatusResponse response : propFindMethod.getResponseBodyAsMultiStatus(httpResponse).getResponses()) { |
| DavPropertySet set = response.getProperties(SC_OK); |
| DavProperty<?> calhome = set.get(calHomeSet); |
| |
| if (calhome != null) { |
| homepath = getTextValuefromProperty(calhome); |
| break; |
| } |
| } |
| } |
| |
| if (homepath != null) { |
| DavPropertyNameSet props = new DavPropertyNameSet(); |
| props.add(DavPropertyName.RESOURCETYPE); |
| props.add(suppCalCompSet); |
| props.add(DavPropertyName.DISPLAYNAME); |
| |
| propFindMethod = new HttpPropfind(homepath, props, DavConstants.DEPTH_1); |
| |
| httpResponse = client.execute(propFindMethod, context); |
| |
| if (propFindMethod.succeeded(httpResponse)) { |
| boolean success = false; |
| |
| URI resourceUri = propFindMethod.getURI(); |
| String host = resourceUri.getScheme() + "://" + resourceUri.getHost() + ((resourceUri.getPort() != -1)? ":" + resourceUri.getPort() : ""); |
| for (MultiStatusResponse response : propFindMethod.getResponseBodyAsMultiStatus(httpResponse).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 e) { |
| String name = DomUtil.getAttribute(e, "name", null); |
| if ("VEVENT".equals(name)) { |
| isVevent = true; |
| } |
| } |
| } |
| } |
| |
| if (isCalendar && isVevent) { |
| success = true; |
| //Get New Calendar |
| OmCalendar tempCalendar = new OmCalendar(); |
| |
| if (displayname != null) { |
| tempCalendar.setTitle(displayname.getValue().toString()); |
| } |
| |
| tempCalendar.setHref(host + response.getHref()); |
| |
| tempCalendar.setDeleted(false); |
| tempCalendar.setOwner(calendar.getOwner()); |
| |
| calendarDao.update(tempCalendar); |
| initCalendar(client, context, 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.reset(); |
| } |
| } |
| |
| return false; |
| } |
| |
| private static String getTextValuefromProperty(DavProperty<?> property) { |
| String value = null; |
| |
| if (property != null) { |
| for (Object o : (Collection<?>) property.getValue()) { |
| if (o instanceof Element e) { |
| 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 e && 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. |
| * |
| * @param client - {@link HttpClient} to discover calendar |
| * @param context http context |
| * @param calendar - calendar to be inited |
| * @return <code>true</code> in case calendar was inited |
| */ |
| private boolean initCalendar(HttpClient client, HttpClientContext context, OmCalendar calendar) { |
| |
| if (calendar.getToken() == null || calendar.getSyncType() == SyncType.NONE) { |
| calendarDao.update(calendar); |
| |
| HttpPropfind propFindMethod = null; |
| |
| try { |
| String path = calendar.getHref(); |
| |
| DavPropertyNameSet properties = new DavPropertyNameSet(); |
| properties.add(DavPropertyName.RESOURCETYPE); |
| properties.add(DavPropertyName.DISPLAYNAME); |
| properties.add(CtagHandler.DNAME_GETCTAG); |
| properties.add(WebDAVSyncHandler.DNAME_SYNCTOKEN); |
| |
| propFindMethod = new HttpPropfind(path, properties, CalDAVConstants.DEPTH_0); |
| HttpResponse httpResponse = client.execute(propFindMethod, context); |
| |
| if (propFindMethod.succeeded(httpResponse)) { |
| |
| for (MultiStatusResponse response : propFindMethod.getResponseBodyAsMultiStatus(httpResponse).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, context, calendar); |
| return true; |
| } else { |
| log.error("Error executing PROPFIND Method, with status Code: {}", httpResponse.getStatusLine().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.reset(); |
| } |
| } |
| } |
| |
| return false; |
| } |
| |
| /** |
| * Function for create/updating multiple appointment on the server. |
| * Performs modification alongside of creation new events on the server. |
| * |
| * @param client - {@link HttpClient} to discover calendar |
| * @param context http context |
| * @param appointment Appointment to create/update. |
| * @return <code>true</code> in case item was updated |
| */ |
| public boolean updateItem(HttpClient client, HttpClientContext context, Appointment appointment) { |
| cleanupIdleConnections(); |
| |
| OmCalendar calendar = appointment.getCalendar(); |
| SyncType type = calendar.getSyncType(); |
| if (type != SyncType.NONE && type != SyncType.GOOGLE_CALENDAR) { |
| CalendarHandler calendarHandler; |
| String path = ensureTrailingSlash(calendar.getHref()); |
| |
| switch (type) { |
| case WEBDAV_SYNC, CTAG, ETAG: |
| calendarHandler = new EtagsHandler(path, calendar, client, context, 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 client - {@link HttpClient} to discover calendar |
| * @param context http context |
| * @param appointment Appointment to Delete |
| * @return <code>true</code> in case item was deleted |
| */ |
| public boolean deleteItem(HttpClient client, HttpClientContext context, Appointment appointment) { |
| cleanupIdleConnections(); |
| |
| OmCalendar calendar = appointment.getCalendar(); |
| SyncType type = calendar.getSyncType(); |
| |
| if (type != SyncType.NONE && type != SyncType.GOOGLE_CALENDAR) { |
| CalendarHandler calendarHandler; |
| String path = calendar.getHref(); |
| |
| switch (type) { |
| case WEBDAV_SYNC, CTAG, ETAG: |
| calendarHandler = new EtagsHandler(path, calendar, client, context, 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, TimeUnit.SECONDS); |
| } |
| } |
| |
| /** |
| * Method which is called when the Context is destroyed. |
| */ |
| @PreDestroy |
| public void destroy() { |
| if (connmanager != null) { |
| connmanager.shutdown(); |
| connmanager = null; |
| } |
| } |
| } |