001: /**
002: * $RCSfile$
003: * $Revision: $
004: * $Date: $
005: *
006: * Copyright (C) 2007 Jive Software. All rights reserved.
007: *
008: * This software is published under the terms of the GNU Public License (GPL),
009: * a copy of which is included in this distribution.
010: */package org.jivesoftware.util;
011:
012: import com.sun.syndication.feed.synd.SyndFeed;
013: import com.sun.syndication.fetcher.FetcherEvent;
014: import com.sun.syndication.fetcher.FetcherException;
015: import com.sun.syndication.fetcher.impl.AbstractFeedFetcher;
016: import com.sun.syndication.fetcher.impl.FeedFetcherCache;
017: import com.sun.syndication.fetcher.impl.SyndFeedInfo;
018: import com.sun.syndication.io.FeedException;
019: import com.sun.syndication.io.SyndFeedInput;
020: import com.sun.syndication.io.XmlReader;
021: import org.apache.commons.httpclient.*;
022: import org.apache.commons.httpclient.methods.GetMethod;
023: import org.jivesoftware.openfire.XMPPServer;
024:
025: import java.io.IOException;
026: import java.io.InputStream;
027: import java.net.HttpURLConnection;
028: import java.net.MalformedURLException;
029: import java.net.URL;
030: import java.util.zip.GZIPInputStream;
031:
032: /**
033: * Feed fetcher implementation that times out the HTTP connection after 3 seconds
034: * which fixes a bug where users of the admin console who installed Clearspace
035: * behind a proxy server would have to wait upwards of 5 minutes in order for the
036: * HTTP connection to jivesoftware.com/blog/feed to timeout.
037: * <p/>
038: * See <a href="http://www.jivesoftware.com/issues/browse/CS-669">http://www.jivesoftware.com/issues/browse/CS-669</a>
039: *
040: */
041: public class HttpClientWithTimeoutFeedFetcher extends
042: AbstractFeedFetcher {
043:
044: private FeedFetcherCache feedInfoCache;
045: private CredentialSupplier credentialSupplier;
046:
047: public HttpClientWithTimeoutFeedFetcher() {
048: super ();
049: }
050:
051: /**
052: * @param cache
053: */
054: public HttpClientWithTimeoutFeedFetcher(FeedFetcherCache cache) {
055: this ();
056: setFeedInfoCache(cache);
057: }
058:
059: public HttpClientWithTimeoutFeedFetcher(FeedFetcherCache cache,
060: CredentialSupplier credentialSupplier) {
061: this (cache);
062: setCredentialSupplier(credentialSupplier);
063: }
064:
065: /**
066: * @return the feedInfoCache.
067: */
068: public synchronized FeedFetcherCache getFeedInfoCache() {
069: return feedInfoCache;
070: }
071:
072: /**
073: * @param feedInfoCache the feedInfoCache to set
074: */
075: public synchronized void setFeedInfoCache(
076: FeedFetcherCache feedInfoCache) {
077: this .feedInfoCache = feedInfoCache;
078: }
079:
080: /**
081: * @return Returns the credentialSupplier.
082: */
083: public synchronized CredentialSupplier getCredentialSupplier() {
084: return credentialSupplier;
085: }
086:
087: /**
088: * @param credentialSupplier The credentialSupplier to set.
089: */
090: public synchronized void setCredentialSupplier(
091: CredentialSupplier credentialSupplier) {
092: this .credentialSupplier = credentialSupplier;
093: }
094:
095: /**
096: * @see com.sun.syndication.fetcher.FeedFetcher#retrieveFeed(java.net.URL)
097: */
098: public SyndFeed retrieveFeed(URL feedUrl)
099: throws IllegalArgumentException, IOException,
100: FeedException, FetcherException {
101: if (feedUrl == null) {
102: throw new IllegalArgumentException(
103: "null is not a valid URL");
104: }
105: // TODO Fix this
106: //System.setProperty("org.apache.commons.logging.Log", "org.apache.commons.logging.impl.SimpleLog");
107: HttpClient client = new HttpClient();
108: HttpConnectionManager conManager = client
109: .getHttpConnectionManager();
110: conManager.getParams().setParameter("http.connection.timeout",
111: 3000);
112: conManager.getParams()
113: .setParameter("http.socket.timeout", 3000);
114:
115: if (getCredentialSupplier() != null) {
116: client.getState().setAuthenticationPreemptive(true);
117: // TODO what should realm be here?
118: Credentials credentials = getCredentialSupplier()
119: .getCredentials(null, feedUrl.getHost());
120: if (credentials != null) {
121: client.getState().setCredentials(null,
122: feedUrl.getHost(), credentials);
123: }
124: }
125:
126: System.setProperty("httpclient.useragent",
127: "Openfire Admin Console: v"
128: + XMPPServer.getInstance().getServerInfo()
129: .getVersion().getVersionString());
130: String urlStr = feedUrl.toString();
131: FeedFetcherCache cache = getFeedInfoCache();
132: if (cache != null) {
133: // retrieve feed
134: HttpMethod method = new GetMethod(urlStr);
135: method.addRequestHeader("Accept-Encoding", "gzip");
136: try {
137: if (isUsingDeltaEncoding()) {
138: method.setRequestHeader("A-IM", "feed");
139: }
140:
141: // get the feed info from the cache
142: // Note that syndFeedInfo will be null if it is not in the cache
143: SyndFeedInfo syndFeedInfo = cache.getFeedInfo(feedUrl);
144: if (syndFeedInfo != null) {
145: method.setRequestHeader("If-None-Match",
146: syndFeedInfo.getETag());
147:
148: if (syndFeedInfo.getLastModified() instanceof String) {
149: method
150: .setRequestHeader("If-Modified-Since",
151: (String) syndFeedInfo
152: .getLastModified());
153: }
154: }
155:
156: method.setFollowRedirects(true);
157:
158: int statusCode = client.executeMethod(method);
159: fireEvent(FetcherEvent.EVENT_TYPE_FEED_POLLED, urlStr);
160: handleErrorCodes(statusCode);
161:
162: SyndFeed feed = getFeed(syndFeedInfo, urlStr, method,
163: statusCode);
164:
165: syndFeedInfo = buildSyndFeedInfo(feedUrl, urlStr,
166: method, feed, statusCode);
167:
168: cache.setFeedInfo(new URL(urlStr), syndFeedInfo);
169:
170: // the feed may have been modified to pick up cached values
171: // (eg - for delta encoding)
172: feed = syndFeedInfo.getSyndFeed();
173:
174: return feed;
175: } finally {
176: method.releaseConnection();
177: }
178:
179: } else {
180: // cache is not in use
181: HttpMethod method = new GetMethod(urlStr);
182: try {
183: method.setFollowRedirects(true);
184:
185: int statusCode = client.executeMethod(method);
186: fireEvent(FetcherEvent.EVENT_TYPE_FEED_POLLED, urlStr);
187: handleErrorCodes(statusCode);
188:
189: return getFeed(null, urlStr, method, statusCode);
190: } finally {
191: method.releaseConnection();
192: }
193: }
194: }
195:
196: /**
197: * @param feedUrl
198: * @param urlStr
199: * @param method
200: * @param feed
201: * @return
202: * @throws MalformedURLException
203: */
204: private SyndFeedInfo buildSyndFeedInfo(URL feedUrl, String urlStr,
205: HttpMethod method, SyndFeed feed, int statusCode)
206: throws MalformedURLException {
207: SyndFeedInfo syndFeedInfo;
208: syndFeedInfo = new SyndFeedInfo();
209:
210: // this may be different to feedURL because of 3XX redirects
211: syndFeedInfo.setUrl(new URL(urlStr));
212: syndFeedInfo.setId(feedUrl.toString());
213:
214: Header imHeader = method.getResponseHeader("IM");
215: if (imHeader != null
216: && imHeader.getValue().indexOf("feed") >= 0
217: && isUsingDeltaEncoding()) {
218: FeedFetcherCache cache = getFeedInfoCache();
219: if (cache != null && statusCode == 226) {
220: // client is setup to use http delta encoding and the server supports it and has returned a delta encoded response
221: // This response only includes new items
222: SyndFeedInfo cachedInfo = cache.getFeedInfo(feedUrl);
223: if (cachedInfo != null) {
224: SyndFeed cachedFeed = cachedInfo.getSyndFeed();
225:
226: // set the new feed to be the orginal feed plus the new items
227: feed = combineFeeds(cachedFeed, feed);
228: }
229: }
230: }
231:
232: Header lastModifiedHeader = method
233: .getResponseHeader("Last-Modified");
234: if (lastModifiedHeader != null) {
235: syndFeedInfo.setLastModified(lastModifiedHeader.getValue());
236: }
237:
238: Header eTagHeader = method.getResponseHeader("ETag");
239: if (eTagHeader != null) {
240: syndFeedInfo.setETag(eTagHeader.getValue());
241: }
242:
243: syndFeedInfo.setSyndFeed(feed);
244:
245: return syndFeedInfo;
246: }
247:
248: /**
249: * @param urlStr
250: * @param method
251: * @return
252: * @throws IOException
253: * @throws HttpException
254: * @throws FetcherException
255: * @throws FeedException
256: */
257: private static SyndFeed retrieveFeed(String urlStr,
258: HttpMethod method) throws IOException, HttpException,
259: FetcherException, FeedException {
260:
261: InputStream stream = null;
262: if ((method.getResponseHeader("Content-Encoding") != null)
263: && ("gzip".equalsIgnoreCase(method.getResponseHeader(
264: "Content-Encoding").getValue()))) {
265: stream = new GZIPInputStream(method
266: .getResponseBodyAsStream());
267: } else {
268: stream = method.getResponseBodyAsStream();
269: }
270: try {
271: XmlReader reader = null;
272: if (method.getResponseHeader("Content-Type") != null) {
273: reader = new XmlReader(stream, method
274: .getResponseHeader("Content-Type").getValue(),
275: true);
276: } else {
277: reader = new XmlReader(stream, true);
278: }
279: return new SyndFeedInput().build(reader);
280: } finally {
281: if (stream != null) {
282: stream.close();
283: }
284: }
285: }
286:
287: private SyndFeed getFeed(SyndFeedInfo syndFeedInfo, String urlStr,
288: HttpMethod method, int statusCode) throws IOException,
289: HttpException, FetcherException, FeedException {
290:
291: if (statusCode == HttpURLConnection.HTTP_NOT_MODIFIED
292: && syndFeedInfo != null) {
293: fireEvent(FetcherEvent.EVENT_TYPE_FEED_UNCHANGED, urlStr);
294: return syndFeedInfo.getSyndFeed();
295: }
296:
297: SyndFeed feed = retrieveFeed(urlStr, method);
298: fireEvent(FetcherEvent.EVENT_TYPE_FEED_RETRIEVED, urlStr, feed);
299: return feed;
300: }
301:
302: public interface CredentialSupplier {
303: public Credentials getCredentials(String realm, String host);
304: }
305:
306: }
|