///////////////////////////////////////////////////////////////////////////// // // Project ProjectForge Community Edition // www.projectforge.org // // Copyright (C) 2001-2014 Kai Reinhard (k.reinhard@micromata.de) // // ProjectForge is dual-licensed. // // This community edition is free software; you can redistribute it and/or // modify it under the terms of the GNU General Public License as published // by the Free Software Foundation; version 3 of the License. // // This community edition is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General // Public License for more details. // // You should have received a copy of the GNU General Public License along // with this program; if not, see http://www.gnu.org/licenses/. // ///////////////////////////////////////////////////////////////////////////// package org.projectforge.plugins.teamcal.externalsubscription; import java.io.ByteArrayInputStream; import java.io.InputStream; import java.io.Serializable; import java.math.BigInteger; import java.security.MessageDigest; import java.util.ArrayList; import java.util.Date; import java.util.List; import net.fortuna.ical4j.data.CalendarBuilder; import net.fortuna.ical4j.model.Calendar; import net.fortuna.ical4j.model.Component; import net.fortuna.ical4j.model.Property; import net.fortuna.ical4j.model.component.VEvent; import org.apache.commons.httpclient.HttpClient; import org.apache.commons.httpclient.HttpStatus; import org.apache.commons.httpclient.methods.GetMethod; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.projectforge.common.DateHelper; import org.projectforge.plugins.teamcal.admin.TeamCalDO; import org.projectforge.plugins.teamcal.admin.TeamCalDao; import org.projectforge.plugins.teamcal.event.TeamEventDO; import org.projectforge.plugins.teamcal.event.TeamEventUtils; import org.projectforge.web.calendar.CalendarFeed; /** * Holds and updates events of a subscribed calendar. * @author Johannes Unterstein (j.unterstein@micromata.de) */ public class TeamEventSubscription implements Serializable { private static final long serialVersionUID = -9200146874015146227L; private static final org.apache.log4j.Logger log = org.apache.log4j.Logger.getLogger(TeamEventSubscription.class); private Integer teamCalId; private SubscriptionHolder subscription; private List<TeamEventDO> recurrenceEvents; private String currentInitializedHash; private Long lastUpdated, lastFailedUpdate; private int numberOfFailedUpdates = 0; private String lastErrorMessage; private static final Long TIME_IN_THE_PAST = 60L * 24 * 60 * 60 * 1000; // 60 days in millis in the past to subscribe public TeamEventSubscription() { } /** * @return the lastErrorMessage */ public String getLastErrorMessage() { return lastErrorMessage; } /** * @return the numberOfFailedUpdates */ public int getNumberOfFailedUpdates() { return numberOfFailedUpdates; } /** * @return the lastFailedUpdate */ public Long getLastFailedUpdate() { return lastFailedUpdate; } /** * We update the cache softly, therefore we create a new instance and replace the old instance in the cached map then creation and update * is therefore the same two lines of code, but semantically different things. */ public void update(final TeamCalDao teamCalDao, final TeamCalDO teamCalDO) { this.teamCalId = teamCalDO.getId(); currentInitializedHash = null; lastUpdated = null; String url = teamCalDO.getExternalSubscriptionUrl(); if (teamCalDO.isExternalSubscription() == false || StringUtils.isEmpty(url) == true) { // No external subscription. clear(); return; } url = StringUtils.replace(url, "webcal", "http"); final String displayUrl = teamCalDO.getExternalSubscriptionUrlAnonymized(); log.info("Getting subscribed calendar #" + teamCalDO.getId() + " from: " + displayUrl); final CalendarBuilder builder = new CalendarBuilder(); byte[] bytes = null; try { // Create a method instance. final GetMethod method = new GetMethod(url); final HttpClient client = new HttpClient(); final int statusCode = client.executeMethod(method); if (statusCode != HttpStatus.SC_OK) { error("Unable to gather subscription calendar #" + teamCalDO.getId() + " information, using database from url '" + displayUrl + "'. Received statusCode: " + statusCode, null); return; } final MessageDigest md = MessageDigest.getInstance("MD5"); // Read the response body. final InputStream stream = method.getResponseBodyAsStream(); bytes = IOUtils.toByteArray(stream); final String md5 = calcHexHash(md.digest(bytes)); if (StringUtils.equals(md5, teamCalDO.getExternalSubscriptionHash()) == false) { teamCalDO.setExternalSubscriptionHash(md5); teamCalDO.setExternalSubscriptionCalendarBinary(bytes); // internalUpdate is valid at this point, because we are calling this method in an async thread teamCalDao.internalUpdate(teamCalDO); } } catch (final Exception e) { bytes = teamCalDO.getExternalSubscriptionCalendarBinary(); error("Unable to gather subscription calendar #" + teamCalDO.getId() + " information, using database from url '" + displayUrl + "': " + e.getMessage(), e); } if (bytes == null) { error("Unable to use database subscription calendar #" + teamCalDO.getId() + " information, quit from url '" + displayUrl + "'.", null); return; } if (currentInitializedHash != null && StringUtils.equals(currentInitializedHash, teamCalDO.getExternalSubscriptionHash()) == true) { // nothing to do here if the hashes are equal log.info("No modification of subscribed calendar #" + teamCalDO.getId() + " found from: " + displayUrl + " (OK, nothing to be done)."); clear(); return; } final SubscriptionHolder newSubscription = new SubscriptionHolder(); final ArrayList<TeamEventDO> newRecurrenceEvents = new ArrayList<TeamEventDO>(); try { final Date timeInPast = new Date(System.currentTimeMillis() - TIME_IN_THE_PAST); final Calendar calendar = builder.build(new ByteArrayInputStream(bytes)); @SuppressWarnings("unchecked") final List<Component> list = calendar.getComponents(Component.VEVENT); final List<VEvent> vEvents = new ArrayList<VEvent>(); for (final Component c : list) { final VEvent event = (VEvent) c; if (event.getSummary() != null && StringUtils.equals(event.getSummary().getValue(), CalendarFeed.SETUP_EVENT) == true) { // skip setup event! continue; } // skip only far gone events, if they have no recurrence if (event.getStartDate().getDate().before(timeInPast) && event.getProperty(Property.RRULE) == null) { continue; } vEvents.add(event); } // the event id must (!) be negative and decrementing (different on each event) Integer startId = -1; for (final VEvent event : vEvents) { final TeamEventDO teamEvent = TeamEventUtils.createTeamEventDO(event); teamEvent.setId(startId); teamEvent.setCalendar(teamCalDO); if (teamEvent.hasRecurrence() == true) { // special treatment for recurrence events .. newRecurrenceEvents.add(teamEvent); } else { newSubscription.add(teamEvent); } startId--; } // OK, update the subscription: recurrenceEvents = newRecurrenceEvents; subscription = newSubscription; lastUpdated = System.currentTimeMillis(); currentInitializedHash = teamCalDO.getExternalSubscriptionHash(); clear(); log.info("Subscribed calendar #" + teamCalDO.getId() + " successfully received from: " + displayUrl); } catch (final Exception e) { error("Unable to instantiate team event list for calendar #" + teamCalDO.getId() + " information, quit from url '" + displayUrl + "': " + e.getMessage(), e); } } private void clear() { this.lastErrorMessage = null; this.lastFailedUpdate = null; this.numberOfFailedUpdates = 0; } private void error(final String errorMessage, final Exception ex) { this.numberOfFailedUpdates++; final StringBuilder sb = new StringBuilder(); sb.append(errorMessage).append(" (").append(this.numberOfFailedUpdates).append(". failed attempts"); if (this.lastUpdated != null) { sb.append(", last successful update ").append(DateHelper.formatAsUTC(new Date(this.lastUpdated))).append(" UTC"); } sb.append(".)"); this.lastErrorMessage = sb.toString(); if (ex != null) { log.error(this.lastErrorMessage, ex); } else { log.error(this.lastErrorMessage); } this.lastFailedUpdate = System.currentTimeMillis(); } /** * calculates hexadecimal representation of * @param md5 * @return */ private String calcHexHash(final byte[] md5) { String result = null; if (md5 != null) { result = new BigInteger(1, md5).toString(16); } return result; } public List<TeamEventDO> getEvents(final Long startTime, final Long endTime, final boolean minimalAccess) { if (subscription == null) { return new ArrayList<TeamEventDO>(); } // final Long perfStart = System.currentTimeMillis(); final List<TeamEventDO> result = subscription.getResultList(startTime, endTime, minimalAccess); // final Long perfDuration = System.currentTimeMillis() - perfStart; // log.info("calculation of team events took " // + perfDuration // + " ms for " // + result.size() // + " events of " // + eventDurationAccess.size() // + " in total from calendar #" // + teamCalId // + "."); return result; } public Integer getTeamCalId() { return teamCalId; } /** * @return Time of last update (successfully). */ public Long getLastUpdated() { return lastUpdated; } public List<TeamEventDO> getRecurrenceEvents() { return recurrenceEvents; } }