Java tutorial
/* * 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; import net.fortuna.ical4j.util.MapTimeZoneCache; /** * 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; static { // Disable TimeZone caching through JCache System.setProperty("net.fortuna.ical4j.timezone.cache.impl", MapTimeZoneCache.class.getName()); } @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) { Element e = (Element) o; 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) { 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. * * @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: case CTAG: case 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: case CTAG: case 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() { connmanager.shutdown(); connmanager = null; } }