Java tutorial
/** * Licensed to Apereo under one or more contributor license * agreements. See the NOTICE file distributed with this work * for additional information regarding copyright ownership. * Apereo 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 the following location: * * 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.jasig.portlet.calendar.adapter; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.Set; import javax.portlet.PortletRequest; import net.fortuna.ical4j.model.component.VEvent; import net.sf.ehcache.Cache; import net.sf.ehcache.Element; import org.apache.commons.httpclient.Credentials; import org.apache.commons.httpclient.HttpClient; import org.apache.commons.httpclient.HttpException; import org.apache.commons.httpclient.HttpStatus; import org.apache.commons.httpclient.auth.AuthScope; import org.apache.commons.httpclient.methods.GetMethod; import org.apache.commons.io.IOUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jasig.portlet.calendar.CalendarConfiguration; import org.jasig.portlet.calendar.caching.DefaultCacheKeyGeneratorImpl; import org.jasig.portlet.calendar.caching.ICacheKeyGenerator; import org.jasig.portlet.calendar.credentials.DefaultCredentialsExtractorImpl; import org.jasig.portlet.calendar.credentials.ICredentialsExtractor; import org.jasig.portlet.calendar.processor.ICalendarContentProcessorImpl; import org.jasig.portlet.calendar.processor.IContentProcessor; import org.jasig.portlet.calendar.url.DefaultUrlCreatorImpl; import org.jasig.portlet.calendar.url.IUrlCreator; import org.joda.time.Interval; import org.springframework.beans.factory.annotation.Required; /** * Implementation of {@link ICalendarAdapter} that uses Commons HttpClient * for retrieving {@link CalendarEventSet}s. * * This bean requires an EhCache {@link Cache} be provided. * This bean also depends on instances of 3 different interfaces * (default implementation listed in parenthesis): * <ul> * <li>{@link IUrlCreator} (default configuration: {@link DefaultUrlCreatorImpl})</li> * <li>{@link ICredentialsExtractor} (default: {@link DefaultCredentialsExtractorImpl})</li> * <li>{@link IContentProcessor} (default: {@link ICalendarContentProcessorImpl})</li> * </ul> * * By specifying alternate implementations for these interfaces, multiple instances of * this class can be configured to consume {@link CalendarEventSet}s from a variety of different * end points, for example an RSS feed behind basic auth, a CalendarKey implementation behind * a shared secret, or behind CAS. * * @author Nicholas Blair, nblair@doit.wisc.edu * @version $Header: RefactoredHttpICalendarAdapter.java Exp $ */ public final class ConfigurableHttpCalendarAdapter<T> extends AbstractCalendarAdapter implements ICalendarAdapter { protected final Log log = LogFactory.getLog(this.getClass()); private Cache cache; private IUrlCreator urlCreator = new DefaultUrlCreatorImpl(); private ICredentialsExtractor credentialsExtractor = new DefaultCredentialsExtractorImpl(); private IContentProcessor contentProcessor = new ICalendarContentProcessorImpl(); private ICacheKeyGenerator cacheKeyGenerator = new DefaultCacheKeyGeneratorImpl(); private String cacheKeyPrefix = "default"; /** * @param cache the cache to set */ @Required public void setCache(Cache cache) { this.cache = cache; } /** * @param urlCreator the urlCreator to set */ public void setUrlCreator(IUrlCreator urlCreator) { this.urlCreator = urlCreator; } /** * @param credentialsExtractor the credentialsExtractor to set */ public void setCredentialsExtractor(ICredentialsExtractor credentialsExtractor) { this.credentialsExtractor = credentialsExtractor; } /** * @param contentProcessor the contentProcessor to set */ public void setContentProcessor(IContentProcessor contentProcessor) { this.contentProcessor = contentProcessor; } /** * @param cacheKeyPrefix the cacheKeyPrefix to set */ public void setCacheKeyPrefix(String cacheKeyPrefix) { this.cacheKeyPrefix = cacheKeyPrefix; } public void setCacheKeyGenerator(ICacheKeyGenerator cacheKeyGenerator) { this.cacheKeyGenerator = cacheKeyGenerator; } /** * Workflow for this implementation: * * <ol> * <li>consult the configured {@link IUrlCreator} for the url to request</li> * <li>consult the cache to see if the fetch via HTTP is necessary (if not return the cached events)</li> * <li>if the fetch is necessary, consult the {@link ICredentialsExtractor} for necessary {@link Credentials}</li> * <li>Invoke retrieveCalendarHttp</li> * <li>Pass the returned {@link InputStream} into the configured {@link IContentProcessor}</li> * <li>Return the {@link CalendarEventSet}s</li> * </ol> * * (non-Javadoc) * @see org.jasig.portlet.calendar.adapter.ICalendarAdapter#getEvents(org.jasig.portlet.calendar.CalendarConfiguration, org.joda.time.Interval, javax.portlet.PortletRequest) */ public CalendarEventSet getEvents(CalendarConfiguration calendarConfiguration, Interval interval, PortletRequest request) throws CalendarException { // Some HTTP iCal providers, such as Google, don't allow you to specify // the interval in the RESTful call so you get the whole calendar. To // avoid receiving the entire calendar every time you need a specific // interval, break up the caching into two stages. // // Stage 1 caches the entire calendar (or partial if the REST call supports intervals). // // Stage 2 filters the cached calendar down to the requested interval and // caches the calendar events for that interval. // Stage 1: Try to get the cached calendar. String url = this.urlCreator.constructUrl(calendarConfiguration, interval, request); log.debug("generated url: " + url); String intermediateCacheKey = cacheKeyGenerator.getKey(calendarConfiguration, interval, request, cacheKeyPrefix.concat(".").concat(url)); T calendar; Element cachedCalendar = this.cache.get(intermediateCacheKey); if (cachedCalendar == null) { Credentials credentials = credentialsExtractor.getCredentials(request); // read in the data InputStream stream = retrieveCalendarHttp(url, credentials); // run the stream through the processor try { calendar = (T) contentProcessor.getIntermediateCalendar(interval, stream); } catch (CalendarException e) { log.error("Calendar parsing exception: " + e.getCause().getMessage() + " from calendar at " + url); throw e; } // save the VEvents to the cache cachedCalendar = new Element(intermediateCacheKey, calendar); this.cache.put(cachedCalendar); if (log.isDebugEnabled()) { log.debug("Storing calendar cache, key:" + intermediateCacheKey); } } else { if (log.isDebugEnabled()) { log.debug("Retrieving calendar from cache, key:" + intermediateCacheKey); } calendar = (T) cachedCalendar.getValue(); } // The cache key for retrieving a calendar over HTTP may not include // the interval, so we need to add the current interval to the existing // cache key. This might result in the interval being contained in the // key twice, but that won't hurt anything. String processorCacheKey = getIntervalSpecificCacheKey(intermediateCacheKey, interval); // Stage 2: Get the calendar event set for the requested interval from cache // or generate it from the calendar from stage 1. CalendarEventSet eventSet; Element cachedElement = this.cache.get(processorCacheKey); if (cachedElement == null) { Set<VEvent> events = contentProcessor.getEvents(interval, calendar); log.debug("contentProcessor found " + events.size() + " events"); // Save the calendar event set to the cache. Calculate how long // this event set should survive. We don't want this event set to // survive beyond the expiration of the calendar from stage 1 or you // have the potential of having two sets of events with different // overlapping intervals displaying different data, assuming getting // the calendar in stage 1 returns the whole calendar and not just // the portion of the calendar within the desired interval. // // For instance this inconsistency in calendar event sets can happen // when you get the calendar and display a week, then // near the expiration of the calendar from stage 1 get events for a // month that contains the week. If you then display the week again // after the calendar (stage 1) has expired, you could get a changed // calendar and generate different calendar events than what you'd see // in the month view until the stage-2-month calendar event set expires // and builds a calendar event set based on the same data as the week // was generated with. int timeToLiveInSeconds = -1; long currentTime = System.currentTimeMillis(); if (cachedCalendar.getExpirationTime() > currentTime) { long timeToLiveInMilliseconds = cachedCalendar.getExpirationTime() - currentTime; timeToLiveInSeconds = (int) timeToLiveInMilliseconds / 1000; } eventSet = insertCalendarEventSetIntoCache(this.cache, processorCacheKey, events, timeToLiveInSeconds > 0 ? timeToLiveInSeconds : -1); } else { if (log.isDebugEnabled()) { log.debug("Retrieving calendar event set from cache, key:" + processorCacheKey); } eventSet = (CalendarEventSet) cachedElement.getValue(); } return eventSet; } protected String getIntervalSpecificCacheKey(String baseKey, Interval interval) { StringBuffer buf = new StringBuffer(); buf.append(baseKey); buf.append(interval.toString()); return buf.toString(); } /** * Uses Commons HttpClient to retrieve the specified url (optionally with the provided * {@link Credentials}. * The response body is returned as an {@link InputStream}. * * @param url URL of the calendar to be retrieved * @param credentials {@link Credentials} to use with the request, if necessary (null is ok if credentials not required) * @return the body of the http response as a stream * @throws CalendarException wraps all potential {@link Exception} types */ protected InputStream retrieveCalendarHttp(String url, Credentials credentials) throws CalendarException { HttpClient client = new HttpClient(); if (null != credentials) { client.getState().setCredentials(AuthScope.ANY, credentials); } GetMethod get = null; try { if (log.isDebugEnabled()) { log.debug("Retrieving calendar " + url); } get = new GetMethod(url); int rc = client.executeMethod(get); if (rc == HttpStatus.SC_OK) { // return the response body log.debug("request completed successfully"); InputStream in = get.getResponseBodyAsStream(); ByteArrayOutputStream buffer = new ByteArrayOutputStream(); IOUtils.copyLarge(in, buffer); return new ByteArrayInputStream(buffer.toByteArray()); } else { log.warn("HttpStatus for " + url + ":" + rc); throw new CalendarException( "non successful status code retrieving " + url + ", status code: " + rc); } } catch (HttpException e) { log.warn("Error fetching iCalendar feed", e); throw new CalendarException("Error fetching iCalendar feed", e); } catch (IOException e) { log.warn("Error fetching iCalendar feed", e); throw new CalendarException("Error fetching iCalendar feed", e); } finally { if (get != null) { get.releaseConnection(); } } } }